From 5c5bb46e2e85bfa92760e9d91e53610f09c7d3e2 Mon Sep 17 00:00:00 2001 From: Owen Carey <37121709+owenthcarey@users.noreply.github.com> Date: Fri, 28 Aug 2026 15:45:06 -0700 Subject: [PATCH 1/3] feat!: adopt consumer-owned async results and a resolved IR boundary --- CONTRIBUTING.md | 6 +- README.md | 23 +- conformance/c/async_demo.c | 161 ++++++ conformance/c/producer.c | 4 + conformance/cpp/async_demo.cpp | 63 +++ conformance/dart/async_demo.dart | 54 ++ conformance/dotnet/AsyncDemo.cs | 78 +++ conformance/go/async_demo.go | 63 +++ conformance/kotlin/async_demo.kt | 63 +++ conformance/node/async_demo.js | 71 +++ conformance/python/async_demo.py | 63 +++ conformance/ruby/async_demo.rb | 52 ++ conformance/ruby/contacts.rb | 2 +- conformance/ruby/kvstore.rb | 2 +- conformance/run.sh | 127 +++-- conformance/swift/async_demo.swift | 66 +++ conformance/swift/contacts.swift | 2 +- conformance/swift/kvstore.swift | 2 +- conformance/wasm/async_demo.mjs | 74 +++ conformance/wasm/kvstore.mjs | 2 +- crates/weaveffi-abi/src/lib.rs | 17 + crates/weaveffi-abi/src/macros.rs | 6 + crates/weaveffi-bridge/src/lib.rs | 2 - crates/weaveffi-cli/benches/generate_bench.rs | 32 +- crates/weaveffi-cli/src/commands/diff.rs | 2 +- crates/weaveffi-cli/src/commands/format.rs | 2 +- crates/weaveffi-cli/src/commands/generate.rs | 7 +- crates/weaveffi-cli/src/commands/mod.rs | 10 +- crates/weaveffi-cli/src/commands/new.rs | 6 +- crates/weaveffi-cli/src/commands/validate.rs | 6 +- crates/weaveffi-cli/src/config.rs | 20 +- crates/weaveffi-cli/src/extract.rs | 29 +- crates/weaveffi-cli/src/scaffold.rs | 65 ++- crates/weaveffi-cli/tests/cli_check.rs | 6 +- crates/weaveffi-cli/tests/cli_diagnostics.rs | 8 +- crates/weaveffi-cli/tests/cli_errors.rs | 10 +- crates/weaveffi-cli/tests/cli_format.rs | 6 +- .../tests/cli_inline_generators.rs | 6 +- crates/weaveffi-cli/tests/cli_lint.rs | 2 +- .../weaveffi-cli/tests/cli_schema_version.rs | 2 +- crates/weaveffi-cli/tests/determinism.rs | 13 +- crates/weaveffi-cli/tests/docs_examples.rs | 18 +- .../weaveffi-cli/tests/extract_roundtrip.rs | 2 +- .../tests/fixtures/01_calculator.yml | 2 +- .../tests/fixtures/02_contacts.yml | 2 +- .../tests/fixtures/03_inventory.yml | 2 +- .../tests/fixtures/04_async_demo.yml | 2 +- .../weaveffi-cli/tests/fixtures/05_events.yml | 2 +- .../tests/fixtures/06_kitchen_sink.yml | 10 +- .../tests/fixtures/07_docs_everywhere.yml | 2 +- .../tests/fixtures/08_kvstore.yml | 2 +- .../tests/fixtures/09_nested_modules.yml | 2 +- .../weaveffi-cli/tests/fixtures/10_shapes.yml | 2 +- .../tests/fixtures/kitchen_sink_annotated.rs | 1 - crates/weaveffi-cli/tests/safety_audit.rs | 2 +- crates/weaveffi-cli/tests/sample_drift.rs | 2 - crates/weaveffi-cli/tests/snapshots.rs | 15 +- ...ync_demo__src_main_cpp_weaveffi_jni_c.snap | 14 +- ...erywhere__src_main_cpp_weaveffi_jni_c.snap | 4 +- ...hen_sink__src_main_cpp_weaveffi_jni_c.snap | 10 +- ..._kvstore__src_main_cpp_weaveffi_jni_c.snap | 4 +- .../snapshots/c_async_demo__weaveffi_h.snap | 5 + .../snapshots/c_calculator__weaveffi_h.snap | 5 + .../snapshots/c_contacts__weaveffi_h.snap | 5 + .../c_docs_everywhere__weaveffi_h.snap | 5 + .../tests/snapshots/c_events__weaveffi_h.snap | 5 + .../snapshots/c_inventory__weaveffi_h.snap | 5 + .../snapshots/c_kitchen_sink__weaveffi_h.snap | 5 + .../snapshots/c_kvstore__weaveffi_h.snap | 5 + .../c_nested_modules__weaveffi_h.snap | 5 + .../tests/snapshots/c_shapes__weaveffi_h.snap | 5 + .../cpp_async_demo__weaveffi_hpp.snap | 10 + .../cpp_calculator__weaveffi_hpp.snap | 5 + .../snapshots/cpp_contacts__weaveffi_hpp.snap | 5 + .../cpp_docs_everywhere__weaveffi_hpp.snap | 6 + .../snapshots/cpp_events__weaveffi_hpp.snap | 5 + .../cpp_inventory__weaveffi_hpp.snap | 5 + .../cpp_kitchen_sink__weaveffi_hpp.snap | 15 +- .../snapshots/cpp_kvstore__weaveffi_hpp.snap | 6 + .../cpp_nested_modules__weaveffi_hpp.snap | 5 + .../snapshots/cpp_shapes__weaveffi_hpp.snap | 5 + .../dart_async_demo__lib_weaveffi_dart.snap | 13 +- .../dart_calculator__lib_weaveffi_dart.snap | 5 + .../dart_contacts__lib_weaveffi_dart.snap | 5 + ...rt_docs_everywhere__lib_weaveffi_dart.snap | 7 +- .../dart_events__lib_weaveffi_dart.snap | 5 + .../dart_inventory__lib_weaveffi_dart.snap | 5 + .../dart_kitchen_sink__lib_weaveffi_dart.snap | 17 +- .../dart_kvstore__lib_weaveffi_dart.snap | 7 +- ...art_nested_modules__lib_weaveffi_dart.snap | 5 + .../dart_shapes__lib_weaveffi_dart.snap | 5 + .../dotnet_async_demo__WeaveFFI_cs.snap | 8 + .../dotnet_calculator__WeaveFFI_cs.snap | 3 + .../dotnet_contacts__WeaveFFI_cs.snap | 3 + .../dotnet_docs_everywhere__WeaveFFI_cs.snap | 4 + .../snapshots/dotnet_events__WeaveFFI_cs.snap | 3 + .../dotnet_inventory__WeaveFFI_cs.snap | 3 + .../dotnet_kitchen_sink__WeaveFFI_cs.snap | 13 +- .../dotnet_kvstore__WeaveFFI_cs.snap | 4 + .../dotnet_nested_modules__WeaveFFI_cs.snap | 3 + .../snapshots/dotnet_shapes__WeaveFFI_cs.snap | 3 + .../snapshots/go_async_demo__weaveffi_go.snap | 26 +- .../snapshots/go_calculator__weaveffi_go.snap | 16 + .../snapshots/go_contacts__weaveffi_go.snap | 16 + .../go_docs_everywhere__weaveffi_go.snap | 18 +- .../snapshots/go_events__weaveffi_go.snap | 16 + .../snapshots/go_inventory__weaveffi_go.snap | 16 + .../go_kitchen_sink__weaveffi_go.snap | 22 +- .../snapshots/go_kvstore__weaveffi_go.snap | 18 +- .../go_nested_modules__weaveffi_go.snap | 16 + .../snapshots/go_shapes__weaveffi_go.snap | 16 + .../node_async_demo__weaveffi_addon_c.snap | 5 + ...ode_docs_everywhere__weaveffi_addon_c.snap | 1 + .../node_kitchen_sink__weaveffi_addon_c.snap | 4 + .../node_kvstore__weaveffi_addon_c.snap | 1 + ...thon_async_demo__weaveffi_weaveffi_py.snap | 26 +- ...thon_calculator__weaveffi_weaveffi_py.snap | 4 + ...python_contacts__weaveffi_weaveffi_py.snap | 4 + ...docs_everywhere__weaveffi_weaveffi_py.snap | 10 +- .../python_events__weaveffi_weaveffi_py.snap | 4 + ...ython_inventory__weaveffi_weaveffi_py.snap | 4 + ...on_kitchen_sink__weaveffi_weaveffi_py.snap | 24 +- .../python_kvstore__weaveffi_weaveffi_py.snap | 10 +- ..._nested_modules__weaveffi_weaveffi_py.snap | 4 + .../python_shapes__weaveffi_weaveffi_py.snap | 4 + .../ruby_async_demo__lib_weaveffi_rb.snap | 9 +- .../ruby_calculator__lib_weaveffi_rb.snap | 1 + .../ruby_contacts__lib_weaveffi_rb.snap | 1 + ...ruby_docs_everywhere__lib_weaveffi_rb.snap | 3 +- .../ruby_events__lib_weaveffi_rb.snap | 1 + .../ruby_inventory__lib_weaveffi_rb.snap | 1 + .../ruby_kitchen_sink__lib_weaveffi_rb.snap | 13 +- .../ruby_kvstore__lib_weaveffi_rb.snap | 3 +- .../ruby_nested_modules__lib_weaveffi_rb.snap | 1 + .../ruby_shapes__lib_weaveffi_rb.snap | 1 + ...demo__Sources_WeaveFFI_WeaveFFI_swift.snap | 5 + ...here__Sources_WeaveFFI_WeaveFFI_swift.snap | 1 + ...sink__Sources_WeaveFFI_WeaveFFI_swift.snap | 10 +- ...tore__Sources_WeaveFFI_WeaveFFI_swift.snap | 1 + .../wasm_async_demo__weaveffi_wasm_js.snap | 15 +- ...asm_docs_everywhere__weaveffi_wasm_js.snap | 13 +- .../wasm_kitchen_sink__weaveffi_wasm_js.snap | 25 +- .../wasm_kvstore__weaveffi_wasm_js.snap | 13 +- crates/weaveffi-core/benches/codegen_bench.rs | 27 +- crates/weaveffi-core/src/backend/mod.rs | 56 ++- crates/weaveffi-core/src/cabi.rs | 6 +- crates/weaveffi-core/src/cache.rs | 33 +- crates/weaveffi-core/src/capabilities.rs | 2 +- crates/weaveffi-core/src/codegen/common.rs | 82 +--- crates/weaveffi-core/src/codegen/mod.rs | 60 ++- crates/weaveffi-core/src/errors.rs | 2 +- crates/weaveffi-core/src/lib.rs | 3 + crates/weaveffi-core/src/model/mod.rs | 48 +- crates/weaveffi-core/src/pkg.rs | 2 +- crates/weaveffi-core/src/plan.rs | 31 +- crates/weaveffi-core/src/resolved.rs | 86 ++++ crates/weaveffi-core/src/utils.rs | 28 +- crates/weaveffi-core/src/validate.rs | 27 +- crates/weaveffi-core/src/validate/tests.rs | 462 +++++++++--------- crates/weaveffi-core/tests/codegen_hooks.rs | 14 +- .../fuzz/seeds/fuzz_parse_json/minimal.json | 2 +- .../fuzz/seeds/fuzz_parse_toml/minimal.toml | 2 +- .../fuzz/seeds/fuzz_parse_yaml/minimal.yml | 2 +- .../fuzz/seeds/fuzz_validate/minimal.yml | 2 +- crates/weaveffi-fuzz/tests/seeds.rs | 4 +- crates/weaveffi-gen-android/src/jni.rs | 22 +- crates/weaveffi-gen-android/src/lib.rs | 4 +- crates/weaveffi-gen-android/src/tests.rs | 59 +-- crates/weaveffi-gen-c/src/header.rs | 15 +- crates/weaveffi-gen-c/src/lib.rs | 11 +- crates/weaveffi-gen-c/src/tests.rs | 21 +- crates/weaveffi-gen-c/tests/render_samples.rs | 4 +- crates/weaveffi-gen-cpp/src/calls.rs | 32 +- crates/weaveffi-gen-cpp/src/lib.rs | 9 +- crates/weaveffi-gen-cpp/src/tests.rs | 32 +- crates/weaveffi-gen-dart/src/calls.rs | 28 +- crates/weaveffi-gen-dart/src/lib.rs | 13 +- crates/weaveffi-gen-dart/src/runtime.rs | 12 + crates/weaveffi-gen-dart/src/tests.rs | 37 +- crates/weaveffi-gen-dotnet/src/calls.rs | 30 +- crates/weaveffi-gen-dotnet/src/lib.rs | 6 +- crates/weaveffi-gen-dotnet/src/pinvoke.rs | 6 +- crates/weaveffi-gen-dotnet/src/tests.rs | 133 ++--- crates/weaveffi-gen-go/src/calls.rs | 24 +- crates/weaveffi-gen-go/src/lib.rs | 9 +- crates/weaveffi-gen-go/src/package.rs | 4 +- crates/weaveffi-gen-go/src/runtime.rs | 29 +- crates/weaveffi-gen-go/src/tests.rs | 47 +- crates/weaveffi-gen-node/src/addon.rs | 12 +- crates/weaveffi-gen-node/src/lib.rs | 6 +- crates/weaveffi-gen-node/src/tests.rs | 19 +- crates/weaveffi-gen-python/src/calls.rs | 55 ++- crates/weaveffi-gen-python/src/codec.rs | 12 + crates/weaveffi-gen-python/src/entities.rs | 78 +-- crates/weaveffi-gen-python/src/lib.rs | 8 +- crates/weaveffi-gen-python/src/runtime.rs | 4 + crates/weaveffi-gen-python/src/stubs.rs | 19 +- crates/weaveffi-gen-python/src/tests.rs | 169 +------ crates/weaveffi-gen-ruby/src/calls.rs | 47 +- crates/weaveffi-gen-ruby/src/lib.rs | 6 +- crates/weaveffi-gen-ruby/src/runtime.rs | 4 +- crates/weaveffi-gen-ruby/src/tests.rs | 82 +--- crates/weaveffi-gen-swift/src/calls.rs | 45 +- crates/weaveffi-gen-swift/src/lib.rs | 9 +- crates/weaveffi-gen-swift/src/tests.rs | 50 +- crates/weaveffi-gen-wasm/src/calls.rs | 34 +- crates/weaveffi-gen-wasm/src/lib.rs | 4 +- crates/weaveffi-gen-wasm/src/runtime.rs | 24 +- crates/weaveffi-gen-wasm/src/tests.rs | 95 ++-- crates/weaveffi-ir/src/ir.rs | 125 ++--- crates/weaveffi-ir/src/parse.rs | 58 +-- .../weaveffi-macros/src/codegen/async_fns.rs | 79 +-- crates/weaveffi-macros/src/codegen/mod.rs | 4 + crates/weaveffi/tests/runtime.rs | 12 +- docs/src/architecture.md | 69 +-- docs/src/generators/android.md | 18 +- docs/src/generators/c.md | 17 +- docs/src/generators/cpp.md | 16 +- docs/src/generators/dart.md | 31 +- docs/src/generators/dotnet.md | 14 +- docs/src/generators/go.md | 17 +- docs/src/generators/node.md | 24 +- docs/src/generators/python.md | 17 +- docs/src/generators/ruby.md | 25 +- docs/src/generators/swift.md | 24 +- docs/src/getting-started.md | 2 +- docs/src/guides/async.md | 82 ++-- docs/src/guides/config.md | 2 +- docs/src/guides/errors.md | 2 +- docs/src/guides/memory.md | 39 +- docs/src/guides/producer-macro.md | 5 +- docs/src/reference/idl.md | 55 +-- docs/src/reference/memory-error.md | 18 +- docs/src/reference/value-buffers.md | 10 +- docs/src/stability.md | 4 +- docs/src/tutorials/android.md | 2 +- docs/src/tutorials/node.md | 2 +- docs/src/tutorials/python.md | 2 +- docs/src/tutorials/swift.md | 2 +- samples/async-demo/async_demo.yml | 2 +- samples/calculator/calculator.yml | 2 +- samples/contacts/contacts.yml | 2 +- samples/events/events.yml | 2 +- samples/inventory/inventory.yml | 2 +- samples/kvstore/kvstore.yml | 2 +- samples/shapes/shapes.yml | 2 +- weaveffi.schema.json | 2 +- 247 files changed, 3180 insertions(+), 1938 deletions(-) create mode 100644 conformance/c/async_demo.c create mode 100644 conformance/cpp/async_demo.cpp create mode 100644 conformance/dart/async_demo.dart create mode 100644 conformance/dotnet/AsyncDemo.cs create mode 100644 conformance/go/async_demo.go create mode 100644 conformance/kotlin/async_demo.kt create mode 100644 conformance/node/async_demo.js create mode 100644 conformance/python/async_demo.py create mode 100644 conformance/ruby/async_demo.rb create mode 100644 conformance/swift/async_demo.swift create mode 100644 conformance/wasm/async_demo.mjs create mode 100644 crates/weaveffi-core/src/resolved.rs diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 630aa4c4..6486b3fb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -287,7 +287,11 @@ fix/android-jni-crash ## CI -- **CI** (`ci.yml`): runs `cargo fmt --check`, `cargo clippy`, `cargo test`, and build verification on macOS and Linux for every push and PR. +- **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. diff --git a/README.md b/README.md index f513f23f..1b859ae8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # WeaveFFI -[![CI](https://github.com/weavefoundry/weaveffi/actions/workflows/ci.yml/badge.svg)](https://github.com/weavefoundry/weaveffi/actions/workflows/ci.yml) [![License: MIT OR Apache-2.0](https://img.shields.io/badge/license-MIT%2FApache--2.0-blue)](LICENSE-MIT) [![crates.io](https://img.shields.io/crates/v/weaveffi-cli.svg)](https://crates.io/crates/weaveffi-cli) [![Schema](https://img.shields.io/badge/schema-0.6.0-orange)](./weaveffi.schema.json) [![downloads](https://img.shields.io/crates/d/weaveffi-cli.svg)](https://crates.io/crates/weaveffi-cli) +[![CI](https://github.com/weavefoundry/weaveffi/actions/workflows/ci.yml/badge.svg)](https://github.com/weavefoundry/weaveffi/actions/workflows/ci.yml) [![License: MIT OR Apache-2.0](https://img.shields.io/badge/license-MIT%2FApache--2.0-blue)](LICENSE-MIT) [![crates.io](https://img.shields.io/crates/v/weaveffi-cli.svg)](https://crates.io/crates/weaveffi-cli) [![Schema](https://img.shields.io/badge/schema-0.7.0-orange)](./weaveffi.schema.json) [![downloads](https://img.shields.io/crates/d/weaveffi-cli.svg)](https://crates.io/crates/weaveffi-cli) WeaveFFI generates type-safe bindings for 11 languages for any native library that exposes a C ABI, whether it's written in Rust, C, C++, Zig, or anything @@ -29,7 +29,7 @@ declares. An interface is a real object with methods; an error domain plus `throws: true` gives its fallible members typed errors: ```yaml -version: "0.6.0" +version: "0.7.0" modules: - name: kv errors: @@ -215,10 +215,10 @@ the module's error domain as a typed error or exception. `dart:ffi`, all from the same definition. - **The whole IDL surface, on every target.** Interfaces, typed error domains, async functions, iterators, callbacks, and event listeners work - across all eleven languages (Wasm excepts callbacks/listeners and says so - loudly). Generators declare their capabilities and `weaveffi generate` - fails with a clear error (never a silent skip) if a target can't deliver - a feature you use. See the + 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, callbacks, and listeners). See the [feature matrix](docs/src/generators/README.md#feature-support-matrix). ## How does it compare? @@ -260,7 +260,7 @@ Verify the install: ```bash weaveffi --version -weaveffi schema-version # prints 0.6.0 +weaveffi schema-version # prints 0.7.0 ``` ## CLI reference @@ -277,7 +277,7 @@ weaveffi schema-version # prints 0.6.0 | `weaveffi format ` | Rewrite an IDL file in canonical form (sorted keys); `--check` for CI | | `weaveffi watch ` | Re-run `generate` whenever the IDL file changes | | `weaveffi schema --format json-schema` | Print the JSON Schema for the IDL | -| `weaveffi schema-version` | Print the current IR schema version (`0.6.0`) | +| `weaveffi schema-version` | Print the current IR schema version (`0.7.0`) | | `weaveffi doctor` | Check for required toolchains; `--target swift` to scope to one language, `--format json` for CI | | `weaveffi completions ` | Print shell completion scripts (`bash`, `zsh`, `fish`, `powershell`, `elvish`) | @@ -285,7 +285,7 @@ Reference the JSON Schema from your IDL for editor autocompletion: ```yaml # yaml-language-server: $schema=./weaveffi.schema.json -version: "0.6.0" +version: "0.7.0" modules: ... ``` @@ -316,7 +316,10 @@ recommended `weaveffi diff --check` CI workflow. 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. +`weaveffi diff --check` on every sample) runs in CI on every PR, plus a +conformance harness that builds real producers and runs generated +consumers end to end in all eleven languages, including dedicated async +lanes. Releases are fully automated by [semantic-release](https://semantic-release.gitbook.io/) on merge to `main`. diff --git a/conformance/c/async_demo.c b/conformance/c/async_demo.c new file mode 100644 index 00000000..d77b4058 --- /dev/null +++ b/conformance/c/async_demo.c @@ -0,0 +1,161 @@ +// Conformance consumer: async-demo sample, C target. +// +// 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 +#include +#include +#include +#include + +#include "weaveffi.h" +#include "wvbuf.h" + +// A decoded TaskResult record: id, value, success. +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); +} + +// 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)) { + } +} + +// --- 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); +} + +// --- 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); +} + +// --- 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); +} + +// --- 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_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); +} + +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); + 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); + } + + // 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); + + // Sync functions beside the async ones. + assert(weaveffi_tasks_cancel_task(1, &err) == false); + assert(err.code == 0); + + // Every spawned task body has completed by the time its callback fires. + assert(weaveffi_tasks_active_callbacks(&err) == 0); + assert(err.code == 0); + + printf("c async-demo conformance: OK\n"); + return 0; +} diff --git a/conformance/c/producer.c b/conformance/c/producer.c index 6ecfd853..89aeadf2 100644 --- a/conformance/c/producer.c +++ b/conformance/c/producer.c @@ -42,6 +42,10 @@ void weaveffi_error_clear(weaveffi_error* err) { } } +void weaveffi_error_free(weaveffi_error* err) { + (void)err; +} + void weaveffi_free_string(const char* ptr) { (void)ptr; } diff --git a/conformance/cpp/async_demo.cpp b/conformance/cpp/async_demo.cpp new file mode 100644 index 00000000..10657817 --- /dev/null +++ b/conformance/cpp/async_demo.cpp @@ -0,0 +1,63 @@ +// 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 +#include +#include +#include +#include + +#include "weaveffi.hpp" + +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); + 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); + } + + // Direct scalar through the async callback. + assert(weaveffi::tasks::run_n_tasks(7).get() == 7); + + // Sync functions beside the async ones. + assert(!weaveffi::tasks::cancel_task(1)); + + // Every spawned task body has completed by the time its callback fires. + assert(weaveffi::tasks::active_callbacks() == 0); + + std::printf("cpp async-demo conformance: OK\n"); + return 0; +} diff --git a/conformance/dart/async_demo.dart b/conformance/dart/async_demo.dart new file mode 100644 index 00000000..1d741e29 --- /dev/null +++ b/conformance/dart/async_demo.dart @@ -0,0 +1,54 @@ +// 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'); +} + +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'); +} diff --git a/conformance/dotnet/AsyncDemo.cs b/conformance/dotnet/AsyncDemo.cs new file mode 100644 index 00000000..b6c9fc86 --- /dev/null +++ b/conformance/dotnet/AsyncDemo.cs @@ -0,0 +1,78 @@ +// 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. + +using System; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using WeaveFFI; + +internal static class Program +{ + static void Expect(bool cond, string msg) + { + if (!cond) + { + Console.Error.WriteLine($"assertion failed: {msg}"); + Environment.Exit(1); + } + } + + static async Task 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; + }); + + // Async record return: the Task resolves with a TaskResult decoded + // from the completion callback's value buffer. + 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. + try + { + await Tasks.RunTask(""); + Expect(false, "expected TaskException for empty name"); + } + catch (TaskException e) + { + Expect(e.Code == TaskException.InvalidName, $"InvalidName code == 1 (got {e.Code})"); + } + + // Buffered list-of-records both ways. + var batch = await Tasks.RunBatch(new[] { "a", "b", "c" }); + Expect( + 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"); + + // Direct scalar through the async callback. + Expect(await Tasks.RunNTasks(7) == 7, "RunNTasks echoes n"); + + // Sync functions beside the async ones. + Expect(!Tasks.CancelTask(1), "CancelTask reports not cancelled"); + + // Every spawned task body has completed by the time its callback fires. + Expect(Tasks.ActiveCallbacks() == 0, "ActiveCallbacks settles to zero"); + + Console.WriteLine("dotnet async-demo conformance: OK"); + return 0; + } +} diff --git a/conformance/go/async_demo.go b/conformance/go/async_demo.go new file mode 100644 index 00000000..bf73e3ed --- /dev/null +++ b/conformance/go/async_demo.go @@ -0,0 +1,63 @@ +// 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. + +package main + +import ( + "errors" + "fmt" + "os" + + 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") + 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") + 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))) + 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") + + // Sync functions beside the async ones. + expect(!wv.CancelTask(1), "cancel_task reports not cancelled") + + // Every spawned task body has completed by the time its callback fires. + expect(wv.ActiveCallbacks() == 0, "active_callbacks settles to zero") + + fmt.Println("go async-demo conformance: OK") +} diff --git a/conformance/kotlin/async_demo.kt b/conformance/kotlin/async_demo.kt new file mode 100644 index 00000000..bfd21e45 --- /dev/null +++ b/conformance/kotlin/async_demo.kt @@ -0,0 +1,63 @@ +// Conformance consumer: async-demo sample, Android/Kotlin (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. +@file:JvmName("Main") + +import com.weaveffi.TaskException +import com.weaveffi.WeaveFFI +import com.weaveffi.WeaveFFIException +import kotlinx.coroutines.runBlocking +import kotlin.system.exitProcess + +fun expect(cond: Boolean, msg: String) { + if (!cond) { + System.err.println("assertion failed: $msg") + exitProcess(1) + } +} + +fun main() = runBlocking { + // Async record return: the suspend fun resumes with a TaskResult decoded + // from the completion callback's value buffer. + val result = WeaveFFI.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") + } 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") + } + + // Buffered list-of-records both ways. + val batch = WeaveFFI.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") + + // Sync functions beside the async ones. + expect(!WeaveFFI.cancelTask(1L), "cancelTask reports not cancelled") + + // Every spawned task body has completed by the time its callback fires. + expect(WeaveFFI.activeCallbacks() == 0L, "activeCallbacks settles to zero") + + println("kotlin async-demo conformance: OK") +} diff --git a/conformance/node/async_demo.js b/conformance/node/async_demo.js new file mode 100644 index 00000000..7d6b69b3 --- /dev/null +++ b/conformance/node/async_demo.js @@ -0,0 +1,71 @@ +// 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(result.id > 0, 'runTask assigns an 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. + expect(wv.activeCallbacks() === 0, 'activeCallbacks settles to zero'); + + console.log('node async-demo conformance: OK'); +})().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/conformance/python/async_demo.py b/conformance/python/async_demo.py new file mode 100644 index 00000000..a94e7b52 --- /dev/null +++ b/conformance/python/async_demo.py @@ -0,0 +1,63 @@ +"""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/ruby/async_demo.rb b/conformance/ruby/async_demo.rb new file mode 100644 index 00000000..029429cf --- /dev/null +++ b/conformance/ruby/async_demo.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true +# Conformance consumer: async-demo sample, Ruby target. +# +# Drives the blocking async bridge end to end: `run_task` blocks the calling +# thread until the producer's completion callback fires and decodes the +# 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. + +$LOAD_PATH.unshift(File.join(ENV.fetch("WV_RB"), "lib")) +require "async_demo" + +def expect(cond, msg) + raise "assertion failed: #{msg}" unless cond +end + +# 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") +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") + +# Direct scalar through the async callback. +expect(WeaveFFI.run_n_tasks(7) == 7, "run_n_tasks echoes n") + +# Sync functions beside the async ones. +expect(WeaveFFI.cancel_task(1) == false, "cancel_task reports not cancelled") + +# Every spawned task body has completed by the time its callback fires. +expect(WeaveFFI.active_callbacks.zero?, "active_callbacks settles to zero") + +puts "ruby async-demo conformance: OK" diff --git a/conformance/ruby/contacts.rb b/conformance/ruby/contacts.rb index 465fe83a..de97fe1b 100644 --- a/conformance/ruby/contacts.rb +++ b/conformance/ruby/contacts.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # Conformance consumer: contacts sample, Ruby target. # -# Drives the 0.6.0 value-type surface: Contact is a plain Ruby value class +# 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 stays an # owned interface handle released through FFI::AutoPointer. Throwing methods diff --git a/conformance/ruby/kvstore.rb b/conformance/ruby/kvstore.rb index df82b835..8898e30a 100644 --- a/conformance/ruby/kvstore.rb +++ b/conformance/ruby/kvstore.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # Conformance consumer: kvstore sample, Ruby target. # -# Full-surface drive of the 0.6.0 wrapper: the Store interface (the open(path) +# Full-surface drive of the 0.7.0 wrapper: the Store interface (the open(path) # factory constructor, sync methods, the iterator-backed list_keys, the # blocking compact bridge over the async ABI, the deprecated legacy_put, and # the default_capacity static), the typed KvError domain (codes 1001-1004) diff --git a/conformance/run.sh b/conformance/run.sh index 61944c8e..15f2c84f 100755 --- a/conformance/run.sh +++ b/conformance/run.sh @@ -135,6 +135,18 @@ 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" \ @@ -172,11 +184,14 @@ c_producer_exports() { done } -# Resolve the platform-specific cdylib path for a sample crate. +# 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$1.dylib" ;; - *) echo "$LIBDIR/lib$1.so" ;; + Darwin) echo "$LIBDIR/lib$n.dylib" ;; + *) echo "$LIBDIR/lib$n.so" ;; esac } @@ -190,10 +205,11 @@ py_consumer() { 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_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; } # Run a Ruby consumer; same library-selection story as Python. rb_consumer() { @@ -203,10 +219,11 @@ rb_consumer() { 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_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; } # Run a Dart consumer from inside the generated package so `package:weaveffi` # and the cached `ffi` dependency resolve. The cdylib comes via WEAVEFFI_LIBRARY. @@ -225,10 +242,11 @@ dart_consumer() { ( 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_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; } # A directory containing `libweaveffi.` symlinked to the sample cdylib, so # build-time `-lweaveffi` / `#cgo -lweaveffi` resolve. On Linux the consumer then @@ -270,10 +288,11 @@ EOF 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_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; } # Swift: assemble a throwaway SwiftPM package that vendors the generated # WeaveFFI module plus a C shim whose module map points at the generated header @@ -296,7 +315,7 @@ swift_consumer() { cat > "$pkg/Sources/C$mod/module.modulemap" <&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_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; } # .NET: compile the generated P/Invoke source together with the consumer into # one console app. The generated file is named after the resolved identity @@ -357,10 +377,11 @@ EOF 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_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; } # 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), @@ -389,10 +410,11 @@ EOF 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_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; } # Kotlin/Android: compile the generated JNI bridge into `libweaveffi.` (what # `System.loadLibrary("weaveffi")` expects), linked against the producer cdylib; @@ -433,7 +455,7 @@ kotlin_consumer() { [ -n "$coro" ] || { echo "kotlinx-coroutines-core-jvm.jar not found" >&2; return 1; } cc -shared -fPIC "$GENROOT/$sample/android/src/main/cpp/weaveffi_jni.c" \ -I"$jh/include" -I"$jni_os_inc" -I"$GENROOT/$sample/c" \ - -L"$LIBDIR" -l"$sample" -Wl,-rpath,"$LIBDIR" \ + -L"$LIBDIR" -l"${sample//-/_}" -Wl,-rpath,"$LIBDIR" \ -o "$b/libweaveffi.$EXT" || { echo "JNI bridge compile failed" >&2; return 1; } kotlinc "$GENROOT/$sample/android/src/main/kotlin/com/weaveffi/WeaveFFI.kt" \ "$ROOT/conformance/kotlin/$src" -cp "$coro" -include-runtime -d "$b/app.jar" \ @@ -443,10 +465,11 @@ kotlin_consumer() { 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_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; } # Wasm: compile the producer to wasm32-unknown-unknown and drive the generated # ESM bindings from Node. The generated JS glue expects weaveffi_alloc/dealloc @@ -465,16 +488,17 @@ wasm_consumer() { 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" + 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_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; } # --------------------------------------------------------------------------- # Producers + generation @@ -487,13 +511,17 @@ build_producer kvstore generate kvstore samples/kvstore/kvstore.yml build_producer shapes generate shapes samples/shapes/shapes.yml +build_producer async-demo +generate async-demo samples/async-demo/async_demo.yml # Header-only: the producer-export lane implements this in C, no Rust cdylib. generate calculator samples/calculator/calculator.yml # --------------------------------------------------------------------------- # Run matrix: every language runs the events lane (callbacks/listeners + -# iterators) and the kvstore lane (handles, structs, builders, async, eviction -# listener); contacts covers the original struct/enum/optional surface. +# iterators), the kvstore lane (handles, structs, builders, async, eviction +# listener), and the async-demo lane (module-level async functions, typed +# async errors, buffered async list-of-record returns); contacts covers the +# original struct/enum/optional surface. # --------------------------------------------------------------------------- check c-contacts c_contacts check c-events c_events @@ -540,6 +568,17 @@ 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 echo echo "conformance: $PASS passed, $FAIL failed" diff --git a/conformance/swift/async_demo.swift b/conformance/swift/async_demo.swift new file mode 100644 index 00000000..b72139b4 --- /dev/null +++ b/conformance/swift/async_demo.swift @@ -0,0 +1,66 @@ +// 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. + +import Foundation +import AsyncDemo + +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) } +} + +// 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 .invalidName = e else { + fail("expected .invalidName (got \(e))") + } + expect(e.errorCode == 1, "invalidName carries code 1 (got \(e.errorCode))") +} catch { + fail("expected TaskError (got \(error))") +} + +// 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") + +// Direct scalar through the async callback. +let n = await Tasks.runNTasks(n: 7) +expect(n == 7, "runNTasks echoes n") + +// Sync functions beside the async ones. +expect(!Tasks.cancelTask(id: 1), "cancelTask reports not cancelled") + +// Every spawned task body has completed by the time its callback fires. +expect(Tasks.activeCallbacks() == 0, "activeCallbacks settles to zero") + +print("swift async-demo conformance: OK") diff --git a/conformance/swift/contacts.swift b/conformance/swift/contacts.swift index b496a780..270f8658 100644 --- a/conformance/swift/contacts.swift +++ b/conformance/swift/contacts.swift @@ -1,6 +1,6 @@ // Conformance consumer: contacts sample, Swift target. // -// Binds through the generated `Contacts` module and asserts the 0.6.0 +// 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 // `ContactsError` domain enum, non-throwing methods (`list`, `remove`, diff --git a/conformance/swift/kvstore.swift b/conformance/swift/kvstore.swift index ebf68852..3102ec72 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 0.6.0 +// Binds through the generated `Kvstore` module and exercises the 0.7.0 // interface surface: `Store` as a 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 without `try` diff --git a/conformance/wasm/async_demo.mjs b/conformance/wasm/async_demo.mjs new file mode 100644 index 00000000..71dc4cce --- /dev/null +++ b/conformance/wasm/async_demo.mjs @@ -0,0 +1,74 @@ +// 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. +// Unlike the cdylib lanes the .wasm is fully self-contained; the async +// completion fires on the same thread via the exported function table. +// +// 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); +} + +function expect(cond, msg) { + if (!cond) { + console.error('assertion failed: ' + msg); + process.exit(1); + } +} + +globalThis.fetch = async (url) => ({ arrayBuffer: async () => fs.readFileSync(url) }); + +const mod = await import(JS); +const api = await mod.loadWeaveffiWasm(WASM); + +// Async record return: the Promise resolves with a plain object whose i64 +// field decodes as BigInt. +const result = await api.tasks.runTask('alpha'); +expect(result.id > 0n, 'runTask assigns an 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 the InvalidName subclass. +try { + await api.tasks.runTask(''); + expect(false, 'expected InvalidName for empty name'); +} catch (e) { + expect(e instanceof mod.InvalidName, `typed subclass (got ${e.constructor.name})`); + expect(e instanceof mod.TaskError, 'subclass of TaskError'); + expect(e instanceof mod.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 api.tasks.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 api.tasks.runNTasks(7)) === 7, 'runNTasks echoes n'); + +// Sync functions beside the async ones. +expect(api.tasks.cancelTask(1) === false, 'cancelTask reports not cancelled'); + +// Every spawned task body has completed by the time its callback fires. +expect(api.tasks.activeCallbacks() === 0n, 'activeCallbacks settles to zero'); + +console.log('wasm async-demo conformance: OK'); diff --git a/conformance/wasm/kvstore.mjs b/conformance/wasm/kvstore.mjs index f1380386..5c15e105 100644 --- a/conformance/wasm/kvstore.mjs +++ b/conformance/wasm/kvstore.mjs @@ -1,7 +1,7 @@ // Conformance consumer: kvstore sample, Wasm (wasm32-unknown-unknown) target. // // Drives the generated ESM bindings (loadWeaveffiWasm) against the real producer -// compiled to wasm. Exercises the 0.6.0 surface end to end: the `Store` +// compiled to wasm. Exercises the 0.7.0 surface end to end: the `Store` // interface class (static `open` factory, instance methods passing the handle // as the implicit self argument, the `defaultCapacity` static, and `free()` // through the destroy symbol), the per-module typed error domain (`KvError` diff --git a/crates/weaveffi-abi/src/lib.rs b/crates/weaveffi-abi/src/lib.rs index 4c8fac36..943225d2 100644 --- a/crates/weaveffi-abi/src/lib.rs +++ b/crates/weaveffi-abi/src/lib.rs @@ -359,6 +359,23 @@ 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; + } + 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 diff --git a/crates/weaveffi-abi/src/macros.rs b/crates/weaveffi-abi/src/macros.rs index d91dd0b1..c55ccbe5 100644 --- a/crates/weaveffi-abi/src/macros.rs +++ b/crates/weaveffi-abi/src/macros.rs @@ -22,6 +22,7 @@ /// - `weaveffi_free_string` /// - `weaveffi_free_bytes` /// - `weaveffi_error_clear` +/// - `weaveffi_error_free` /// - `weaveffi_cancel_token_create` /// - `weaveffi_cancel_token_cancel` /// - `weaveffi_cancel_token_is_cancelled` @@ -83,6 +84,11 @@ macro_rules! export_runtime { $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() diff --git a/crates/weaveffi-bridge/src/lib.rs b/crates/weaveffi-bridge/src/lib.rs index f94b0cce..fba8c658 100644 --- a/crates/weaveffi-bridge/src/lib.rs +++ b/crates/weaveffi-bridge/src/lib.rs @@ -541,7 +541,6 @@ fn extract_struct(item: &syn::ItemStruct) -> syn::Result { name: field_name, ty: type_ref_from_syn(&f.ty)?, doc: extract_doc(&f.attrs), - default: None, }) }) .collect::>()?, @@ -575,7 +574,6 @@ fn extract_variant_fields(fields: &syn::FieldsNamed) -> syn::Result Api { +fn build_large_api() -> ResolvedApi { let modules = (0..10) .map(|m| { let structs: Vec = (0..5) @@ -35,19 +36,16 @@ fn build_large_api() -> Api { name: "id".into(), ty: TypeRef::I32, doc: None, - default: None, }, StructField { name: "name".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }, StructField { name: "active".into(), ty: TypeRef::Bool, doc: None, - default: None, }, ], }) @@ -132,12 +130,14 @@ fn build_large_api() -> Api { }) .collect(); - Api { - version: "0.6.0".into(), + // The synthetic API already uses resolved type references (see the + // `Record` comment above), so it skips the validate/resolve pass. + ResolvedApi::assume_resolved(Api { + version: "0.7.0".into(), modules, generators: None, package: None, - } + }) } /// Construct the same fan-out of generators used by the CLI, each with its @@ -233,31 +233,27 @@ fn bench_generate_all_large_api(c: &mut Criterion) { /// 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() -> Api { +fn load_kitchen_sink_api() -> ResolvedApi { let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/06_kitchen_sink.yml"); let contents = std::fs::read_to_string(&path) .unwrap_or_else(|e| panic!("read fixture {}: {e}", path.display())); - let mut api = parse_api_str(&contents, "yaml") + let api = parse_api_str(&contents, "yaml") .unwrap_or_else(|e| panic!("parse fixture {}: {e}", path.display())); - validate_api(&mut api, None) - .unwrap_or_else(|e| panic!("validate fixture {}: {e}", path.display())); - api + validate_api(api, None).unwrap_or_else(|e| panic!("validate fixture {}: {e}", path.display())) } /// Parse and validate the calculator sample IDL. -fn load_calculator_api() -> Api { +fn load_calculator_api() -> ResolvedApi { let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../samples/calculator/calculator.yml"); let contents = std::fs::read_to_string(&path) .unwrap_or_else(|e| panic!("read sample {}: {e}", path.display())); - let mut api = parse_api_str(&contents, "yaml") + let api = parse_api_str(&contents, "yaml") .unwrap_or_else(|e| panic!("parse sample {}: {e}", path.display())); - validate_api(&mut api, None) - .unwrap_or_else(|e| panic!("validate sample {}: {e}", path.display())); - api + validate_api(api, None).unwrap_or_else(|e| panic!("validate sample {}: {e}", path.display())) } -fn run_all_generators(api: &Api) { +fn run_all_generators(api: &ResolvedApi) { let generators = all_default_generators(); let mut orchestrator = Orchestrator::new(); for g in &generators { diff --git a/crates/weaveffi-cli/src/commands/diff.rs b/crates/weaveffi-cli/src/commands/diff.rs index 39e5ea39..b8dad7cc 100644 --- a/crates/weaveffi-cli/src/commands/diff.rs +++ b/crates/weaveffi-cli/src/commands/diff.rs @@ -152,7 +152,7 @@ mod tests { std::fs::write( &yml, concat!( - "version: \"0.6.0\"\n", + "version: \"0.7.0\"\n", "modules:\n", " - name: math\n", " functions:\n", diff --git a/crates/weaveffi-cli/src/commands/format.rs b/crates/weaveffi-cli/src/commands/format.rs index c94e569c..d4df5d25 100644 --- a/crates/weaveffi-cli/src/commands/format.rs +++ b/crates/weaveffi-cli/src/commands/format.rs @@ -12,7 +12,7 @@ pub(crate) fn cmd_format(input: &str, check: bool, quiet: bool) -> Result<()> { let formatted = match format { "yaml" => format_api_yaml(&api)?, "json" => format_api_json(&api)?, - "toml" => toml::to_string_pretty(&api) + "toml" => toml::to_string_pretty(api.api()) .into_diagnostic() .wrap_err("failed to serialize API as TOML")?, _ => unreachable!(), diff --git a/crates/weaveffi-cli/src/commands/generate.rs b/crates/weaveffi-cli/src/commands/generate.rs index 8181a30b..46f07399 100644 --- a/crates/weaveffi-cli/src/commands/generate.rs +++ b/crates/weaveffi-cli/src/commands/generate.rs @@ -99,7 +99,7 @@ mod tests { std::fs::write( &yml, concat!( - "version: \"0.6.0\"\n", + "version: \"0.7.0\"\n", "modules:\n", " - name: math\n", " functions:\n", @@ -122,9 +122,8 @@ mod tests { let api = { let contents = std::fs::read_to_string(&yml).unwrap(); - let mut api = weaveffi_ir::parse::parse_api_str(&contents, "yaml").unwrap(); - weaveffi_core::validate::validate_api(&mut api, None).unwrap(); - api + let api = weaveffi_ir::parse::parse_api_str(&contents, "yaml").unwrap(); + weaveffi_core::validate::validate_api(api, None).unwrap() }; let out_dir = Utf8Path::new(out_str); diff --git a/crates/weaveffi-cli/src/commands/mod.rs b/crates/weaveffi-cli/src/commands/mod.rs index 9c317119..ba1606d2 100644 --- a/crates/weaveffi-cli/src/commands/mod.rs +++ b/crates/weaveffi-cli/src/commands/mod.rs @@ -13,6 +13,7 @@ use crate::report::with_named_source; use camino::Utf8Path; use miette::{bail, IntoDiagnostic, Report, Result, WrapErr}; use weaveffi_core::validate::validate_api; +use weaveffi_core::ResolvedApi; use weaveffi_ir::ir::Api; use weaveffi_ir::parse::parse_api_str; @@ -58,9 +59,10 @@ pub(crate) fn load_api(input: &str) -> Result<(Api, String)> { } /// [`load_api`] plus validation, the shared front half of `generate`, -/// `lint`, `diff`, and `format`. -pub(crate) fn load_validated_api(input: &str) -> Result<(Api, String)> { - let (mut api, contents) = load_api(input)?; - validate_api(&mut api, Some((input, &contents))).map_err(Report::new)?; +/// `lint`, `diff`, and `format`. Returns the [`ResolvedApi`] that every +/// downstream consumer (orchestrator, scaffold, packagers) requires. +pub(crate) fn load_validated_api(input: &str) -> Result<(ResolvedApi, String)> { + let (api, contents) = load_api(input)?; + let api = validate_api(api, Some((input, &contents))).map_err(Report::new)?; Ok((api, contents)) } diff --git a/crates/weaveffi-cli/src/commands/new.rs b/crates/weaveffi-cli/src/commands/new.rs index 420be8e2..6027f920 100644 --- a/crates/weaveffi-cli/src/commands/new.rs +++ b/crates/weaveffi-cli/src/commands/new.rs @@ -24,7 +24,7 @@ pub(crate) fn cmd_new(name: &str, quiet: bool) -> Result<()> { let idl_path = project_dir.join("weaveffi.yml"); let idl_contents = format!( concat!( - "version: \"0.6.0\"\n", + "version: \"0.7.0\"\n", "package:\n", " name: {name}\n", " version: \"0.1.0\"\n", @@ -61,8 +61,8 @@ pub(crate) fn cmd_new(name: &str, quiet: bool) -> Result<()> { .into_diagnostic() .wrap_err_with(|| format!("failed to write {}", idl_path))?; - let mut api = parse_api_str(&idl_contents, "yaml").wrap_err("failed to parse generated IDL")?; - validate_api(&mut api, None).wrap_err("generated IDL failed validation")?; + let api = parse_api_str(&idl_contents, "yaml").wrap_err("failed to parse generated IDL")?; + let api = validate_api(api, None).wrap_err("generated IDL failed validation")?; let cargo_toml_path = project_dir.join("Cargo.toml"); let cargo_toml = format!( diff --git a/crates/weaveffi-cli/src/commands/validate.rs b/crates/weaveffi-cli/src/commands/validate.rs index 48c013c3..96e7e1f2 100644 --- a/crates/weaveffi-cli/src/commands/validate.rs +++ b/crates/weaveffi-cli/src/commands/validate.rs @@ -11,10 +11,10 @@ pub(crate) fn cmd_validate( quiet: bool, ) -> Result<()> { let json_mode = format == Some("json"); - let (mut api, contents) = super::load_api(input)?; + let (api, contents) = super::load_api(input)?; - match validate_api(&mut api, Some((input, &contents))) { - Ok(()) => { + match validate_api(api, Some((input, &contents))) { + Ok(api) => { if warn && !quiet { for w in collect_warnings(&api) { eprintln!("warning: {w}"); diff --git a/crates/weaveffi-cli/src/config.rs b/crates/weaveffi-cli/src/config.rs index 940fc872..b03f42db 100644 --- a/crates/weaveffi-cli/src/config.rs +++ b/crates/weaveffi-cli/src/config.rs @@ -271,7 +271,7 @@ mod tests { #[test] fn inline_generator_config() { let yaml = concat!( - "version: \"0.6.0\"\n", + "version: \"0.7.0\"\n", "modules:\n", " - name: math\n", " functions:\n", @@ -295,7 +295,7 @@ mod tests { #[test] fn inline_dart_package_name_merges() { let yaml = concat!( - "version: \"0.6.0\"\n", + "version: \"0.7.0\"\n", "modules:\n", " - name: m\n", " functions: []\n", @@ -312,7 +312,7 @@ mod tests { #[test] fn inline_go_module_path_merges() { let yaml = concat!( - "version: \"0.6.0\"\n", + "version: \"0.7.0\"\n", "modules:\n", " - name: m\n", " functions: []\n", @@ -329,7 +329,7 @@ mod tests { #[test] fn inline_ruby_module_name_merges() { let yaml = concat!( - "version: \"0.6.0\"\n", + "version: \"0.7.0\"\n", "modules:\n", " - name: m\n", " functions: []\n", @@ -346,7 +346,7 @@ mod tests { #[test] fn inline_ruby_gem_name_merges() { let yaml = concat!( - "version: \"0.6.0\"\n", + "version: \"0.7.0\"\n", "modules:\n", " - name: m\n", " functions: []\n", @@ -363,7 +363,7 @@ mod tests { #[test] fn inline_global_strip_module_prefix_merges() { let yaml = concat!( - "version: \"0.6.0\"\n", + "version: \"0.7.0\"\n", "modules:\n", " - name: m\n", " functions: []\n", @@ -390,7 +390,7 @@ mod tests { #[test] fn inline_global_pre_generate_merges() { let yaml = concat!( - "version: \"0.6.0\"\n", + "version: \"0.7.0\"\n", "modules:\n", " - name: m\n", " functions: []\n", @@ -407,7 +407,7 @@ mod tests { #[test] fn inline_unknown_target_silently_ignored() { let yaml = concat!( - "version: \"0.6.0\"\n", + "version: \"0.7.0\"\n", "modules:\n", " - name: m\n", " functions: []\n", @@ -426,7 +426,7 @@ mod tests { #[test] fn inline_unknown_key_silently_ignored() { let yaml = concat!( - "version: \"0.6.0\"\n", + "version: \"0.7.0\"\n", "modules:\n", " - name: m\n", " functions: []\n", @@ -444,7 +444,7 @@ mod tests { #[test] fn inline_generator_config_overrides_file() { let yaml = concat!( - "version: \"0.6.0\"\n", + "version: \"0.7.0\"\n", "modules:\n", " - name: math\n", " functions:\n", diff --git a/crates/weaveffi-cli/src/extract.rs b/crates/weaveffi-cli/src/extract.rs index a21e5fb8..70662289 100644 --- a/crates/weaveffi-cli/src/extract.rs +++ b/crates/weaveffi-cli/src/extract.rs @@ -34,20 +34,27 @@ pub(crate) fn cmd_extract( .into_diagnostic() .wrap_err_with(|| format!("failed to read source file: {input}"))?; - let mut api = weaveffi_bridge::api_from_src_stringly(&source) + let api = weaveffi_bridge::api_from_src_stringly(&source) .map_err(|e| miette!("failed to extract API from Rust source {input}:\n{e}"))?; - if let Err(e) = weaveffi_core::validate::validate_api(&mut api, None) { - if warn { - eprintln!("warning: {e}"); - } 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." - )); + // Serialize the resolved form when the API validates (so cross-module + // references are emitted qualified, matching `weaveffi format`); fall + // back to the raw extracted document under `--warn`. + let api = match weaveffi_core::validate::validate_api(api.clone(), None) { + Ok(resolved) => resolved.api().clone(), + Err(e) => { + if warn { + 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." + )); + } } - } + }; let serialized = match format { "yaml" | "yml" => serde_yaml::to_string(&api) diff --git a/crates/weaveffi-cli/src/scaffold.rs b/crates/weaveffi-cli/src/scaffold.rs index 2c48287e..03f2b4e8 100644 --- a/crates/weaveffi-cli/src/scaffold.rs +++ b/crates/weaveffi-cli/src/scaffold.rs @@ -1,8 +1,8 @@ use std::fmt::Write; use weaveffi_core::abi::AbiParam; +use weaveffi_core::resolved::ResolvedApi; use weaveffi_core::model::{AbiFn, BindingModel, CallShape, ModuleBinding}; -use weaveffi_ir::ir::Api; /// The body every generated stub carries until the producer fills it in. const TODO_BODY: &str = " todo!()\n"; @@ -50,7 +50,7 @@ fn emit_abi_fn(out: &mut String, f: &AbiFn, prefix: &str, body: &str) { /// [`render_rust`](weaveffi_core::abi::CType::render_rust), so a scaffolded /// producer matches the generated header (and therefore every language binding) /// by construction; there is no second, drift-prone ABI lowering here. -pub fn render_scaffold(api: &Api, c_prefix: &str) -> String { +pub fn render_scaffold(api: &ResolvedApi, c_prefix: &str) -> String { let model = BindingModel::build(api, c_prefix); let mut out = String::new(); out.push_str("#![allow(unsafe_code)]\n"); @@ -185,9 +185,9 @@ mod tests { use super::*; use weaveffi_ir::ir::{Api, Function, Module, Param, StructDef, StructField, TypeRef}; - fn minimal_api(functions: Vec, structs: Vec) -> Api { - Api { - version: "0.6.0".to_string(), + fn minimal_api(functions: Vec, structs: Vec) -> ResolvedApi { + ResolvedApi::assume_resolved(Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "calc".to_string(), functions, @@ -201,13 +201,13 @@ mod tests { }], generators: None, package: None, - } + }) } #[test] fn scaffold_interface_emits_type_members_and_destroy() { use weaveffi_ir::ir::InterfaceDef; - let mut api = minimal_api(vec![], vec![]); + let mut api = minimal_api(vec![], vec![]).api().clone(); api.modules[0].interfaces.push(InterfaceDef { name: "Store".into(), doc: None, @@ -240,6 +240,7 @@ mod tests { }], statics: vec![], }); + let api = ResolvedApi::assume_resolved(api); let out = render_scaffold(&api, "weaveffi"); assert!(out.contains("pub struct weaveffi_calc_Store {")); assert!(out.contains("fn weaveffi_calc_Store_open(")); @@ -314,7 +315,6 @@ mod tests { name: "x".into(), ty: TypeRef::F64, doc: None, - default: None, }], }], ); @@ -469,13 +469,11 @@ mod tests { name: "x".into(), ty: TypeRef::F64, doc: None, - default: None, }, StructField { name: "y".into(), ty: TypeRef::F64, doc: None, - default: None, }, ], }], @@ -513,7 +511,6 @@ mod tests { name: "x".into(), ty: TypeRef::F64, doc: None, - default: None, }], }], ); @@ -531,8 +528,8 @@ mod tests { #[test] fn scaffold_rich_enum_emits_no_producer_surface() { use weaveffi_ir::ir::{EnumDef, EnumVariant}; - let api = Api { - version: "0.6.0".into(), + let api = ResolvedApi::assume_resolved(Api { + version: "0.7.0".into(), modules: vec![Module { name: "shapes".into(), functions: vec![], @@ -555,7 +552,6 @@ mod tests { name: "radius".into(), ty: TypeRef::F64, doc: None, - default: None, }], }, ], @@ -568,7 +564,7 @@ mod tests { }], generators: None, package: None, - }; + }); let out = render_scaffold(&api, "weaveffi"); // Rich enums are value types crossing the ABI as serialized buffers: // no opaque object, per-variant constructors, getters, tag reader, or @@ -582,8 +578,8 @@ mod tests { #[test] fn scaffold_c_style_enum_emits_no_producer_surface() { use weaveffi_ir::ir::{EnumDef, EnumVariant}; - let api = Api { - version: "0.6.0".into(), + let api = ResolvedApi::assume_resolved(Api { + version: "0.7.0".into(), modules: vec![Module { name: "shapes".into(), functions: vec![], @@ -614,7 +610,7 @@ mod tests { }], generators: None, package: None, - }; + }); let out = render_scaffold(&api, "weaveffi"); assert!( !out.contains("weaveffi_shapes_Channel"), @@ -912,8 +908,8 @@ mod tests { // A nested submodule's functions/structs must get stubs, and their C // symbols must use the underscore-joined module path so they line up // with the generated bindings. - let api = Api { - version: "0.6.0".into(), + let api = ResolvedApi::assume_resolved(Api { + version: "0.7.0".into(), modules: vec![Module { name: "graphics".into(), functions: vec![], @@ -943,7 +939,6 @@ mod tests { name: "sides".into(), ty: TypeRef::I32, doc: None, - default: None, }], }], enums: vec![], @@ -956,7 +951,7 @@ mod tests { }], generators: None, package: None, - }; + }); let out = render_scaffold(&api, "weaveffi"); assert!( out.contains("pub extern \"C\" fn weaveffi_graphics_shapes_make("), @@ -974,8 +969,8 @@ mod tests { // After resolution, a cross-module struct reference is dot-qualified // (e.g. `shared.Token`). The scaffold must flatten it to the owning // module's C symbol, never embed the dot or the referrer's module. - let api = Api { - version: "0.6.0".into(), + let api = ResolvedApi::assume_resolved(Api { + version: "0.7.0".into(), modules: vec![Module { name: "kitchen".into(), functions: vec![Function { @@ -999,7 +994,7 @@ mod tests { }], generators: None, package: None, - }; + }); let out = render_scaffold(&api, "weaveffi"); // A cross-module record return is a value buffer like any other; the // stub must not leak the dotted name or invent a referrer-module tag. @@ -1019,8 +1014,8 @@ mod tests { #[test] fn scaffold_multiple_modules() { - let api = Api { - version: "0.6.0".into(), + let api = ResolvedApi::assume_resolved(Api { + version: "0.7.0".into(), modules: vec![ Module { name: "math".into(), @@ -1067,7 +1062,7 @@ mod tests { ], generators: None, package: None, - }; + }); let out = render_scaffold(&api, "weaveffi"); assert!(out.contains("weaveffi_math_add"), "missing math module"); assert!(out.contains("weaveffi_io_read"), "missing io module"); @@ -1077,9 +1072,9 @@ mod tests { functions: Vec, callbacks: Vec, listeners: Vec, - ) -> Api { - Api { - version: "0.6.0".into(), + ) -> ResolvedApi { + ResolvedApi::assume_resolved(Api { + version: "0.7.0".into(), modules: vec![Module { name: "events".into(), functions, @@ -1093,7 +1088,7 @@ mod tests { }], generators: None, package: None, - } + }) } #[test] @@ -1173,8 +1168,8 @@ mod tests { #[test] fn scaffold_cancellable_async_threads_cancel_token() { - let api = Api { - version: "0.6.0".into(), + let api = ResolvedApi::assume_resolved(Api { + version: "0.7.0".into(), modules: vec![Module { name: "net".into(), functions: vec![Function { @@ -1203,7 +1198,7 @@ mod tests { }], generators: None, package: None, - }; + }); let out = render_scaffold(&api, "weaveffi"); // The completion callback prefix is (context, err, result). assert!( diff --git a/crates/weaveffi-cli/tests/cli_check.rs b/crates/weaveffi-cli/tests/cli_check.rs index cdbc3528..1a360830 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.6.0\"\n", + "version: \"0.7.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.6.0\"\n", + "version: \"0.7.0\"\n", "modules:\n", " - name: calc\n", " functions:\n", @@ -189,7 +189,7 @@ fn lint_json_format_outputs_warnings_array() { write_file( &path, concat!( - "version: \"0.6.0\"\n", + "version: \"0.7.0\"\n", "modules:\n", " - name: nodocs\n", " functions:\n", diff --git a/crates/weaveffi-cli/tests/cli_diagnostics.rs b/crates/weaveffi-cli/tests/cli_diagnostics.rs index 1d68b264..66c54b36 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.6.0\"\nmodules:\n - name: [bad\n"; + let yaml = "version: \"0.7.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.6.0" + let yaml = r#"version: "0.7.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.6.0" + let yaml = r#"version: "0.7.0" modules: - name: alpha functions: @@ -134,7 +134,7 @@ modules: #[test] fn parse_json_error_includes_line() { let dir = tempfile::tempdir().unwrap(); - let json = "{\n \"version\": \"0.6.0\",\n \"modules\": [ broken\n}\n"; + let json = "{\n \"version\": \"0.7.0\",\n \"modules\": [ broken\n}\n"; let path = write_temp_file(&dir, "broken.json", json); let output = cargo_bin() diff --git a/crates/weaveffi-cli/tests/cli_errors.rs b/crates/weaveffi-cli/tests/cli_errors.rs index b8217812..77807f8b 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.6.0" +version: "0.7.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.6.0" +version: "0.7.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.6.0" +version: "0.7.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.6.0" +version: "0.7.0" modules: - name: dup functions: @@ -249,7 +249,7 @@ modules: fn validate_with_warnings() { let dir = tempfile::tempdir().unwrap(); let yaml = r#" -version: "0.6.0" +version: "0.7.0" modules: - name: nodocs functions: diff --git a/crates/weaveffi-cli/tests/cli_format.rs b/crates/weaveffi-cli/tests/cli_format.rs index 63f030cf..4125d278 100644 --- a/crates/weaveffi-cli/tests/cli_format.rs +++ b/crates/weaveffi-cli/tests/cli_format.rs @@ -16,7 +16,7 @@ fn format_check_passes_for_canonical_input() { write( &path, concat!( - "version: \"0.6.0\"\n", + "version: \"0.7.0\"\n", "modules:\n", " - name: math\n", " functions:\n", @@ -51,7 +51,7 @@ fn format_rewrites_unsorted_keys() { " - { type: i32, name: b }\n", " name: add\n", " name: math\n", - "version: \"0.6.0\"\n", + "version: \"0.7.0\"\n", ), ); let original = std::fs::read_to_string(&path).unwrap(); @@ -108,7 +108,7 @@ fn format_check_fails_for_unsorted_input() { " params: []\n", " name: noop\n", " name: math\n", - "version: \"0.6.0\"\n", + "version: \"0.7.0\"\n", ), ); let original = std::fs::read_to_string(&path).unwrap(); diff --git a/crates/weaveffi-cli/tests/cli_inline_generators.rs b/crates/weaveffi-cli/tests/cli_inline_generators.rs index ad5033c3..a6fbed95 100644 --- a/crates/weaveffi-cli/tests/cli_inline_generators.rs +++ b/crates/weaveffi-cli/tests/cli_inline_generators.rs @@ -7,7 +7,7 @@ fn inline_dart_package_name_used() { fs::write( &yml, concat!( - "version: \"0.6.0\"\n", + "version: \"0.7.0\"\n", "modules:\n", " - name: math\n", " functions:\n", @@ -47,7 +47,7 @@ fn inline_dart_package_name_used() { } const LISTENER_IDL: &str = concat!( - "version: \"0.6.0\"\n", + "version: \"0.7.0\"\n", "modules:\n", " - name: events\n", " callbacks:\n", @@ -129,7 +129,7 @@ fn wasm_emscripten_mode_generates_module_loader() { fs::write( &yml, concat!( - "version: \"0.6.0\"\n", + "version: \"0.7.0\"\n", "modules:\n", " - name: math\n", " functions:\n", diff --git a/crates/weaveffi-cli/tests/cli_lint.rs b/crates/weaveffi-cli/tests/cli_lint.rs index e97416a1..325e7ad4 100644 --- a/crates/weaveffi-cli/tests/cli_lint.rs +++ b/crates/weaveffi-cli/tests/cli_lint.rs @@ -27,7 +27,7 @@ fn lint_clean_calculator() { fn lint_warns_on_undocumented() { let dir = tempfile::tempdir().unwrap(); let yaml = r#" -version: "0.6.0" +version: "0.7.0" modules: - name: nodocs functions: diff --git a/crates/weaveffi-cli/tests/cli_schema_version.rs b/crates/weaveffi-cli/tests/cli_schema_version.rs index 7ebb32b0..4852926d 100644 --- a/crates/weaveffi-cli/tests/cli_schema_version.rs +++ b/crates/weaveffi-cli/tests/cli_schema_version.rs @@ -11,5 +11,5 @@ fn schema_version_command_prints_0_4_0() { String::from_utf8_lossy(&output.stderr) ); let stdout = String::from_utf8_lossy(&output.stdout); - assert_eq!(stdout.trim(), "0.6.0"); + assert_eq!(stdout.trim(), "0.7.0"); } diff --git a/crates/weaveffi-cli/tests/determinism.rs b/crates/weaveffi-cli/tests/determinism.rs index 3b72ace8..f2af347c 100644 --- a/crates/weaveffi-cli/tests/determinism.rs +++ b/crates/weaveffi-cli/tests/determinism.rs @@ -10,6 +10,7 @@ use std::path::{Path, PathBuf}; use camino::Utf8Path; use weaveffi_core::codegen::{ConfiguredGenerator, DynGenerator}; +use weaveffi_core::resolved::ResolvedApi; use weaveffi_core::validate::validate_api; use weaveffi_gen_android::{AndroidConfig, AndroidGenerator}; use weaveffi_gen_c::{CConfig, CGenerator}; @@ -22,22 +23,20 @@ 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; use weaveffi_ir::parse::parse_api_str; const KITCHEN_SINK: &str = "06_kitchen_sink.yml"; -fn load_kitchen_sink() -> Api { +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 mut api = parse_api_str(&contents, "yaml") + let api = parse_api_str(&contents, "yaml") .unwrap_or_else(|e| panic!("parse fixture {}: {e}", path.display())); - validate_api(&mut api, None) - .unwrap_or_else(|e| panic!("validate fixture {}: {e}", path.display())); - api + validate_api(api, None) + .unwrap_or_else(|e| panic!("validate fixture {}: {e}", path.display())) } fn collect_files_sorted(root: &Path) -> Vec { @@ -62,7 +61,7 @@ fn walk(dir: &Path, out: &mut Vec) { } } -fn run_into_tempdir(gen: &dyn DynGenerator, api: &Api) -> (tempfile::TempDir, PathBuf) { +fn run_into_tempdir(gen: &dyn DynGenerator, 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"); diff --git a/crates/weaveffi-cli/tests/docs_examples.rs b/crates/weaveffi-cli/tests/docs_examples.rs index 926aa165..92037dab 100644 --- a/crates/weaveffi-cli/tests/docs_examples.rs +++ b/crates/weaveffi-cli/tests/docs_examples.rs @@ -1,5 +1,5 @@ const GENERATOR_DOCS_YAML: &str = r#" -version: "0.6.0" +version: "0.7.0" modules: - name: contacts enums: @@ -327,7 +327,7 @@ fn summary_md_all_links_resolve() { /// The README quickstart IDL, kept in sync with the `kvstore.yml` snippet in /// `README.md` step 2 (trimmed `Store` interface plus `KvError` domain). const README_QUICKSTART_YAML: &str = r#" -version: "0.6.0" +version: "0.7.0" modules: - name: kv errors: @@ -356,9 +356,9 @@ modules: #[test] fn readme_quickstart_yaml_parses_and_validates() { - let mut api = weaveffi_ir::parse::parse_api_str(README_QUICKSTART_YAML, "yaml") + let api = weaveffi_ir::parse::parse_api_str(README_QUICKSTART_YAML, "yaml") .expect("README quickstart YAML should parse"); - weaveffi_core::validate::validate_api(&mut api, None) + let api = weaveffi_core::validate::validate_api(api, None) .expect("README quickstart YAML should validate"); assert_eq!(api.modules.len(), 1); @@ -436,7 +436,7 @@ fn readme_quickstart_generates_c_header() { } const GETTING_STARTED_YAML: &str = r#" -version: "0.6.0" +version: "0.7.0" modules: - name: math structs: @@ -454,9 +454,9 @@ modules: #[test] fn getting_started_yaml_parses_and_validates() { - let mut api = weaveffi_ir::parse::parse_api_str(GETTING_STARTED_YAML, "yaml") + let api = weaveffi_ir::parse::parse_api_str(GETTING_STARTED_YAML, "yaml") .expect("getting-started YAML should parse"); - weaveffi_core::validate::validate_api(&mut api, None) + let api = weaveffi_core::validate::validate_api(api, None) .expect("getting-started YAML should validate"); assert_eq!(api.modules.len(), 1); @@ -583,8 +583,8 @@ fn readme_states_value_proposition() { fn readme_uses_only_current_schema_version() { let readme = read_workspace_file("README.md"); assert!( - readme.contains("\"0.6.0\""), - "README should reference schema version 0.6.0" + readme.contains("\"0.7.0\""), + "README should reference schema version 0.7.0" ); for old in ["\"0.1.0\"", "\"0.2.0\"", "\"0.3.0\"", "\"0.4.0\""] { assert!( diff --git a/crates/weaveffi-cli/tests/extract_roundtrip.rs b/crates/weaveffi-cli/tests/extract_roundtrip.rs index 7afeeeab..50d43389 100644 --- a/crates/weaveffi-cli/tests/extract_roundtrip.rs +++ b/crates/weaveffi-cli/tests/extract_roundtrip.rs @@ -3,7 +3,7 @@ //! `roundtrip_kitchen_sink` proves the extractor recovers the same shape //! as the original kitchen-sink IDL when run on the hand-annotated Rust //! file at `crates/weaveffi-cli/tests/fixtures/kitchen_sink_annotated.rs`, -//! including the 0.6.0 surface: the `Gadget` interface, the `KitchenErrors` +//! including the 0.7.0 surface: the `Gadget` interface, the `KitchenErrors` //! domain, and per-function `throws`. Lossy fields (struct field defaults, //! iterator returns, standalone `since` without `#[deprecated]`, callback //! param docs, error-code `doc:` separate from `message:`) are documented diff --git a/crates/weaveffi-cli/tests/fixtures/01_calculator.yml b/crates/weaveffi-cli/tests/fixtures/01_calculator.yml index bbf7d887..f2240bf0 100644 --- a/crates/weaveffi-cli/tests/fixtures/01_calculator.yml +++ b/crates/weaveffi-cli/tests/fixtures/01_calculator.yml @@ -1,4 +1,4 @@ -version: "0.6.0" +version: "0.7.0" modules: - name: calculator errors: diff --git a/crates/weaveffi-cli/tests/fixtures/02_contacts.yml b/crates/weaveffi-cli/tests/fixtures/02_contacts.yml index 6c129c2a..70952201 100644 --- a/crates/weaveffi-cli/tests/fixtures/02_contacts.yml +++ b/crates/weaveffi-cli/tests/fixtures/02_contacts.yml @@ -1,4 +1,4 @@ -version: "0.6.0" +version: "0.7.0" modules: - name: contacts enums: diff --git a/crates/weaveffi-cli/tests/fixtures/03_inventory.yml b/crates/weaveffi-cli/tests/fixtures/03_inventory.yml index 889ccd81..67fe726d 100644 --- a/crates/weaveffi-cli/tests/fixtures/03_inventory.yml +++ b/crates/weaveffi-cli/tests/fixtures/03_inventory.yml @@ -1,4 +1,4 @@ -version: "0.6.0" +version: "0.7.0" modules: - name: products enums: diff --git a/crates/weaveffi-cli/tests/fixtures/04_async_demo.yml b/crates/weaveffi-cli/tests/fixtures/04_async_demo.yml index 0326094a..35f5e0b6 100644 --- a/crates/weaveffi-cli/tests/fixtures/04_async_demo.yml +++ b/crates/weaveffi-cli/tests/fixtures/04_async_demo.yml @@ -1,4 +1,4 @@ -version: "0.6.0" +version: "0.7.0" modules: - name: tasks structs: diff --git a/crates/weaveffi-cli/tests/fixtures/05_events.yml b/crates/weaveffi-cli/tests/fixtures/05_events.yml index 7c3f7548..772a2bb8 100644 --- a/crates/weaveffi-cli/tests/fixtures/05_events.yml +++ b/crates/weaveffi-cli/tests/fixtures/05_events.yml @@ -1,4 +1,4 @@ -version: "0.6.0" +version: "0.7.0" modules: - name: events callbacks: diff --git a/crates/weaveffi-cli/tests/fixtures/06_kitchen_sink.yml b/crates/weaveffi-cli/tests/fixtures/06_kitchen_sink.yml index 47cad718..1804ff6b 100644 --- a/crates/weaveffi-cli/tests/fixtures/06_kitchen_sink.yml +++ b/crates/weaveffi-cli/tests/fixtures/06_kitchen_sink.yml @@ -1,4 +1,4 @@ -version: "0.6.0" +version: "0.7.0" modules: - name: shared structs: @@ -26,10 +26,10 @@ modules: fields: - { name: id, type: i64, doc: Stable identifier } - { name: name, type: string, doc: Display name } - - { name: count, type: i32, default: 0, doc: Initial count } - - { name: enabled, type: bool, default: true, doc: Whether the item is enabled } - - { name: ratio, type: f64, default: 1.0, doc: Scaling ratio } - - { name: nick, type: u32, default: 0 } + - { name: count, type: i32, doc: Initial count } + - { name: enabled, type: bool, doc: Whether the item is enabled } + - { name: ratio, type: f64, doc: Scaling ratio } + - { name: nick, type: u32 } - { name: payload, type: bytes } - { name: tags, type: "[string]" } - { name: attrs, type: "{string:string}" } diff --git a/crates/weaveffi-cli/tests/fixtures/07_docs_everywhere.yml b/crates/weaveffi-cli/tests/fixtures/07_docs_everywhere.yml index 1f88f6b2..297184b9 100644 --- a/crates/weaveffi-cli/tests/fixtures/07_docs_everywhere.yml +++ b/crates/weaveffi-cli/tests/fixtures/07_docs_everywhere.yml @@ -1,4 +1,4 @@ -version: "0.6.0" +version: "0.7.0" modules: - name: docs enums: diff --git a/crates/weaveffi-cli/tests/fixtures/08_kvstore.yml b/crates/weaveffi-cli/tests/fixtures/08_kvstore.yml index 737a7ffb..8cc09c5f 100644 --- a/crates/weaveffi-cli/tests/fixtures/08_kvstore.yml +++ b/crates/weaveffi-cli/tests/fixtures/08_kvstore.yml @@ -1,4 +1,4 @@ -version: "0.6.0" +version: "0.7.0" modules: - name: kv structs: diff --git a/crates/weaveffi-cli/tests/fixtures/09_nested_modules.yml b/crates/weaveffi-cli/tests/fixtures/09_nested_modules.yml index b970da00..5122288a 100644 --- a/crates/weaveffi-cli/tests/fixtures/09_nested_modules.yml +++ b/crates/weaveffi-cli/tests/fixtures/09_nested_modules.yml @@ -1,4 +1,4 @@ -version: "0.6.0" +version: "0.7.0" # Exercises multi-level nested modules and type references that cross nesting # boundaries in every direction: # * parent -> nested child (graphics.Canvas.root : shapes.Shape) diff --git a/crates/weaveffi-cli/tests/fixtures/10_shapes.yml b/crates/weaveffi-cli/tests/fixtures/10_shapes.yml index da01f98c..8fd89244 100644 --- a/crates/weaveffi-cli/tests/fixtures/10_shapes.yml +++ b/crates/weaveffi-cli/tests/fixtures/10_shapes.yml @@ -1,4 +1,4 @@ -version: "0.6.0" +version: "0.7.0" modules: - name: shapes doc: Rich (algebraic) enums and the expanded numeric primitives. diff --git a/crates/weaveffi-cli/tests/fixtures/kitchen_sink_annotated.rs b/crates/weaveffi-cli/tests/fixtures/kitchen_sink_annotated.rs index 2fa14281..fe92d9ee 100644 --- a/crates/weaveffi-cli/tests/fixtures/kitchen_sink_annotated.rs +++ b/crates/weaveffi-cli/tests/fixtures/kitchen_sink_annotated.rs @@ -10,7 +10,6 @@ //! Round-trip gaps documented in `docs/src/guides/extract.md`: //! * The original `stream_items` IDL function (an `iter` return) is //! omitted from this fixture. -//! * Struct field `default:` values: dropped. //! * Standalone `since:` without `#[deprecated]`: dropped, so `new_op` //! loses its `since: "0.3.0"`. //! * An error code's yml `doc:` cannot be expressed separately from its diff --git a/crates/weaveffi-cli/tests/safety_audit.rs b/crates/weaveffi-cli/tests/safety_audit.rs index f659f4d1..7eac37a1 100644 --- a/crates/weaveffi-cli/tests/safety_audit.rs +++ b/crates/weaveffi-cli/tests/safety_audit.rs @@ -1,5 +1,5 @@ fn audit_api_yaml() -> &'static str { - r#"version: "0.6.0" + r#"version: "0.7.0" modules: - name: inventory structs: diff --git a/crates/weaveffi-cli/tests/sample_drift.rs b/crates/weaveffi-cli/tests/sample_drift.rs index 38f44690..164db6d3 100644 --- a/crates/weaveffi-cli/tests/sample_drift.rs +++ b/crates/weaveffi-cli/tests/sample_drift.rs @@ -63,8 +63,6 @@ fn canon_function(f: &mut Function) { /// Strip everything the gate doesn't compare from one field. fn canon_field(field: &mut StructField) { field.doc = None; - // Rust struct syntax can't carry an IDL field default. - field.default = None; field.ty = normalize_type(&field.ty); } diff --git a/crates/weaveffi-cli/tests/snapshots.rs b/crates/weaveffi-cli/tests/snapshots.rs index cb11cf11..22eaaf34 100644 --- a/crates/weaveffi-cli/tests/snapshots.rs +++ b/crates/weaveffi-cli/tests/snapshots.rs @@ -10,6 +10,7 @@ use std::path::{Path, PathBuf}; use camino::Utf8Path; use weaveffi_core::codegen::{ConfiguredGenerator, DynGenerator, Generator}; +use weaveffi_core::resolved::ResolvedApi; use weaveffi_core::validate::validate_api; use weaveffi_gen_android::{AndroidConfig, AndroidGenerator}; use weaveffi_gen_c::{CConfig, CGenerator}; @@ -22,7 +23,6 @@ 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; use weaveffi_ir::parse::parse_api_str; fn fixture_path(file: &str) -> PathBuf { @@ -31,15 +31,14 @@ fn fixture_path(file: &str) -> PathBuf { .join(file) } -fn load_api(file: &str) -> Api { +fn load_api(file: &str) -> ResolvedApi { let path = fixture_path(file); let contents = fs::read_to_string(&path) .unwrap_or_else(|e| panic!("read fixture {}: {e}", path.display())); - let mut api = parse_api_str(&contents, "yaml") + let api = parse_api_str(&contents, "yaml") .unwrap_or_else(|e| panic!("parse fixture {}: {e}", path.display())); - validate_api(&mut api, None) - .unwrap_or_else(|e| panic!("validate fixture {}: {e}", path.display())); - api + validate_api(api, None) + .unwrap_or_else(|e| panic!("validate fixture {}: {e}", path.display())) } fn collect_files_sorted(root: &Path) -> Vec { @@ -69,7 +68,7 @@ fn sanitize(rel: &Path) -> String { } /// Replace the concrete crate version in every WeaveFFI prelude -/// (`Generated by WeaveFFI 0.6.0 from ...`) with a stable `[VERSION]` +/// (`Generated by WeaveFFI 0.7.0 from ...`) with a stable `[VERSION]` /// placeholder. Without this, bumping the crate version invalidates all /// 300+ generator snapshots even though no generated logic changed. fn redact_version(s: &str) -> String { @@ -120,7 +119,7 @@ where let contents = fs::read_to_string(&file) .unwrap_or_else(|e| panic!("read {}: {e}", file.display())); // Snapshot the version-redacted form so a release bump (e.g. - // 0.6.0 → 0.6.0) does not invalidate every snapshot; assert the + // 0.7.0 → 0.7.0) does not invalidate every snapshot; assert the // real prelude (with the concrete version) against the original. insta::assert_snapshot!(name, redact_version(&contents)); assert_prelude_present(&contents, &file); diff --git a/crates/weaveffi-cli/tests/snapshots/android_async_demo__src_main_cpp_weaveffi_jni_c.snap b/crates/weaveffi-cli/tests/snapshots/android_async_demo__src_main_cpp_weaveffi_jni_c.snap index 6d52e05b..1959b2e9 100644 --- a/crates/weaveffi-cli/tests/snapshots/android_async_demo__src_main_cpp_weaveffi_jni_c.snap +++ b/crates/weaveffi-cli/tests/snapshots/android_async_demo__src_main_cpp_weaveffi_jni_c.snap @@ -75,12 +75,15 @@ static void weaveffi_tasks_run_task_jni_cb(void* context, weaveffi_error* err, c 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, (jint)err->code, jmsg, jpayload); + (*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); @@ -117,12 +120,15 @@ static void weaveffi_tasks_run_batch_jni_cb(void* context, weaveffi_error* err, 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, (jint)err->code, jmsg, jpayload); + (*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); @@ -170,9 +176,11 @@ static void weaveffi_tasks_run_n_tasks_jni_cb(void* context, weaveffi_error* err 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, (jint)err->code, jmsg, jpayload); + (*env)->CallVoidMethod(env, ctx->callback, mid, jcode, jmsg, jpayload); } else { jclass boxCls = (*env)->FindClass(env, "java/lang/Integer"); jmethodID valueOf = (*env)->GetStaticMethodID(env, boxCls, "valueOf", "(I)Ljava/lang/Integer;"); diff --git a/crates/weaveffi-cli/tests/snapshots/android_docs_everywhere__src_main_cpp_weaveffi_jni_c.snap b/crates/weaveffi-cli/tests/snapshots/android_docs_everywhere__src_main_cpp_weaveffi_jni_c.snap index 2222a947..8c7a5ad4 100644 --- a/crates/weaveffi-cli/tests/snapshots/android_docs_everywhere__src_main_cpp_weaveffi_jni_c.snap +++ b/crates/weaveffi-cli/tests/snapshots/android_docs_everywhere__src_main_cpp_weaveffi_jni_c.snap @@ -193,9 +193,11 @@ static void weaveffi_docs_publish_async_jni_cb(void* context, weaveffi_error* er 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, (jint)err->code, jmsg, jpayload); + (*env)->CallVoidMethod(env, ctx->callback, mid, jcode, jmsg, jpayload); } else { jclass boxCls = (*env)->FindClass(env, "java/lang/Boolean"); jmethodID valueOf = (*env)->GetStaticMethodID(env, boxCls, "valueOf", "(Z)Ljava/lang/Boolean;"); diff --git a/crates/weaveffi-cli/tests/snapshots/android_kitchen_sink__src_main_cpp_weaveffi_jni_c.snap b/crates/weaveffi-cli/tests/snapshots/android_kitchen_sink__src_main_cpp_weaveffi_jni_c.snap index 28e9c17c..3fc50425 100644 --- a/crates/weaveffi-cli/tests/snapshots/android_kitchen_sink__src_main_cpp_weaveffi_jni_c.snap +++ b/crates/weaveffi-cli/tests/snapshots/android_kitchen_sink__src_main_cpp_weaveffi_jni_c.snap @@ -371,11 +371,14 @@ static void weaveffi_kitchen_do_async_jni_cb(void* context, weaveffi_error* err, 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, (jint)err->code, jmsg, jpayload); + (*env)->CallVoidMethod(env, ctx->callback, mid, jcode, jmsg, jpayload); } else { jobject boxed = result ? (jobject)(*env)->NewStringUTF(env, result) : (jobject)(*env)->NewStringUTF(env, ""); + weaveffi_free_string(result); jclass cls = (*env)->GetObjectClass(env, ctx->callback); jmethodID mid = (*env)->GetMethodID(env, cls, "onSuccess", "(Ljava/lang/Object;)V"); (*env)->CallVoidMethod(env, ctx->callback, mid, boxed); @@ -412,11 +415,14 @@ static void weaveffi_kitchen_do_cancellable_jni_cb(void* context, weaveffi_error 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, (jint)err->code, jmsg, jpayload); + (*env)->CallVoidMethod(env, ctx->callback, mid, jcode, jmsg, jpayload); } else { jobject boxed = result ? (jobject)(*env)->NewStringUTF(env, result) : (jobject)(*env)->NewStringUTF(env, ""); + weaveffi_free_string(result); jclass cls = (*env)->GetObjectClass(env, ctx->callback); jmethodID mid = (*env)->GetMethodID(env, cls, "onSuccess", "(Ljava/lang/Object;)V"); (*env)->CallVoidMethod(env, ctx->callback, mid, boxed); diff --git a/crates/weaveffi-cli/tests/snapshots/android_kvstore__src_main_cpp_weaveffi_jni_c.snap b/crates/weaveffi-cli/tests/snapshots/android_kvstore__src_main_cpp_weaveffi_jni_c.snap index cfaee225..0fd18c3d 100644 --- a/crates/weaveffi-cli/tests/snapshots/android_kvstore__src_main_cpp_weaveffi_jni_c.snap +++ b/crates/weaveffi-cli/tests/snapshots/android_kvstore__src_main_cpp_weaveffi_jni_c.snap @@ -273,9 +273,11 @@ static void weaveffi_kv_Store_compact_jni_cb(void* context, weaveffi_error* err, 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, (jint)err->code, jmsg, jpayload); + (*env)->CallVoidMethod(env, ctx->callback, mid, jcode, jmsg, jpayload); } else { jclass boxCls = (*env)->FindClass(env, "java/lang/Long"); jmethodID valueOf = (*env)->GetStaticMethodID(env, boxCls, "valueOf", "(J)Ljava/lang/Long;"); diff --git a/crates/weaveffi-cli/tests/snapshots/c_async_demo__weaveffi_h.snap b/crates/weaveffi-cli/tests/snapshots/c_async_demo__weaveffi_h.snap index 4d925a07..9cea224e 100644 --- a/crates/weaveffi-cli/tests/snapshots/c_async_demo__weaveffi_h.snap +++ b/crates/weaveffi-cli/tests/snapshots/c_async_demo__weaveffi_h.snap @@ -56,6 +56,11 @@ typedef struct weaveffi_error { } weaveffi_error; WEAVEFFI_API void weaveffi_error_clear(weaveffi_error* err); + +/* Async completion callbacks receive a heap-boxed error the consumer + owns; weaveffi_error_free releases the message, the payload, and the + box itself. Passing NULL is a safe no-op. */ +WEAVEFFI_API void weaveffi_error_free(weaveffi_error* err); WEAVEFFI_API void weaveffi_free_string(const char* ptr); WEAVEFFI_API void weaveffi_free_bytes(uint8_t* ptr, size_t len); diff --git a/crates/weaveffi-cli/tests/snapshots/c_calculator__weaveffi_h.snap b/crates/weaveffi-cli/tests/snapshots/c_calculator__weaveffi_h.snap index 76098c6d..9fa12951 100644 --- a/crates/weaveffi-cli/tests/snapshots/c_calculator__weaveffi_h.snap +++ b/crates/weaveffi-cli/tests/snapshots/c_calculator__weaveffi_h.snap @@ -56,6 +56,11 @@ typedef struct weaveffi_error { } weaveffi_error; WEAVEFFI_API void weaveffi_error_clear(weaveffi_error* err); + +/* Async completion callbacks receive a heap-boxed error the consumer + owns; weaveffi_error_free releases the message, the payload, and the + box itself. Passing NULL is a safe no-op. */ +WEAVEFFI_API void weaveffi_error_free(weaveffi_error* err); WEAVEFFI_API void weaveffi_free_string(const char* ptr); WEAVEFFI_API void weaveffi_free_bytes(uint8_t* ptr, size_t len); diff --git a/crates/weaveffi-cli/tests/snapshots/c_contacts__weaveffi_h.snap b/crates/weaveffi-cli/tests/snapshots/c_contacts__weaveffi_h.snap index c0f90180..ecd6b856 100644 --- a/crates/weaveffi-cli/tests/snapshots/c_contacts__weaveffi_h.snap +++ b/crates/weaveffi-cli/tests/snapshots/c_contacts__weaveffi_h.snap @@ -56,6 +56,11 @@ typedef struct weaveffi_error { } weaveffi_error; WEAVEFFI_API void weaveffi_error_clear(weaveffi_error* err); + +/* Async completion callbacks receive a heap-boxed error the consumer + owns; weaveffi_error_free releases the message, the payload, and the + box itself. Passing NULL is a safe no-op. */ +WEAVEFFI_API void weaveffi_error_free(weaveffi_error* err); WEAVEFFI_API void weaveffi_free_string(const char* ptr); WEAVEFFI_API void weaveffi_free_bytes(uint8_t* ptr, size_t len); diff --git a/crates/weaveffi-cli/tests/snapshots/c_docs_everywhere__weaveffi_h.snap b/crates/weaveffi-cli/tests/snapshots/c_docs_everywhere__weaveffi_h.snap index 65ba5d3b..3296d149 100644 --- a/crates/weaveffi-cli/tests/snapshots/c_docs_everywhere__weaveffi_h.snap +++ b/crates/weaveffi-cli/tests/snapshots/c_docs_everywhere__weaveffi_h.snap @@ -56,6 +56,11 @@ typedef struct weaveffi_error { } weaveffi_error; WEAVEFFI_API void weaveffi_error_clear(weaveffi_error* err); + +/* Async completion callbacks receive a heap-boxed error the consumer + owns; weaveffi_error_free releases the message, the payload, and the + box itself. Passing NULL is a safe no-op. */ +WEAVEFFI_API void weaveffi_error_free(weaveffi_error* err); WEAVEFFI_API void weaveffi_free_string(const char* ptr); WEAVEFFI_API void weaveffi_free_bytes(uint8_t* ptr, size_t len); diff --git a/crates/weaveffi-cli/tests/snapshots/c_events__weaveffi_h.snap b/crates/weaveffi-cli/tests/snapshots/c_events__weaveffi_h.snap index e27c2661..a3c299b9 100644 --- a/crates/weaveffi-cli/tests/snapshots/c_events__weaveffi_h.snap +++ b/crates/weaveffi-cli/tests/snapshots/c_events__weaveffi_h.snap @@ -56,6 +56,11 @@ typedef struct weaveffi_error { } weaveffi_error; WEAVEFFI_API void weaveffi_error_clear(weaveffi_error* err); + +/* Async completion callbacks receive a heap-boxed error the consumer + owns; weaveffi_error_free releases the message, the payload, and the + box itself. Passing NULL is a safe no-op. */ +WEAVEFFI_API void weaveffi_error_free(weaveffi_error* err); WEAVEFFI_API void weaveffi_free_string(const char* ptr); WEAVEFFI_API void weaveffi_free_bytes(uint8_t* ptr, size_t len); diff --git a/crates/weaveffi-cli/tests/snapshots/c_inventory__weaveffi_h.snap b/crates/weaveffi-cli/tests/snapshots/c_inventory__weaveffi_h.snap index c6de21b1..f76a89d5 100644 --- a/crates/weaveffi-cli/tests/snapshots/c_inventory__weaveffi_h.snap +++ b/crates/weaveffi-cli/tests/snapshots/c_inventory__weaveffi_h.snap @@ -56,6 +56,11 @@ typedef struct weaveffi_error { } weaveffi_error; WEAVEFFI_API void weaveffi_error_clear(weaveffi_error* err); + +/* Async completion callbacks receive a heap-boxed error the consumer + owns; weaveffi_error_free releases the message, the payload, and the + box itself. Passing NULL is a safe no-op. */ +WEAVEFFI_API void weaveffi_error_free(weaveffi_error* err); WEAVEFFI_API void weaveffi_free_string(const char* ptr); WEAVEFFI_API void weaveffi_free_bytes(uint8_t* ptr, size_t len); diff --git a/crates/weaveffi-cli/tests/snapshots/c_kitchen_sink__weaveffi_h.snap b/crates/weaveffi-cli/tests/snapshots/c_kitchen_sink__weaveffi_h.snap index eea5ecdf..6d9914ac 100644 --- a/crates/weaveffi-cli/tests/snapshots/c_kitchen_sink__weaveffi_h.snap +++ b/crates/weaveffi-cli/tests/snapshots/c_kitchen_sink__weaveffi_h.snap @@ -56,6 +56,11 @@ typedef struct weaveffi_error { } weaveffi_error; WEAVEFFI_API void weaveffi_error_clear(weaveffi_error* err); + +/* Async completion callbacks receive a heap-boxed error the consumer + owns; weaveffi_error_free releases the message, the payload, and the + box itself. Passing NULL is a safe no-op. */ +WEAVEFFI_API void weaveffi_error_free(weaveffi_error* err); WEAVEFFI_API void weaveffi_free_string(const char* ptr); WEAVEFFI_API void weaveffi_free_bytes(uint8_t* ptr, size_t len); diff --git a/crates/weaveffi-cli/tests/snapshots/c_kvstore__weaveffi_h.snap b/crates/weaveffi-cli/tests/snapshots/c_kvstore__weaveffi_h.snap index 4689701f..dd8a66de 100644 --- a/crates/weaveffi-cli/tests/snapshots/c_kvstore__weaveffi_h.snap +++ b/crates/weaveffi-cli/tests/snapshots/c_kvstore__weaveffi_h.snap @@ -56,6 +56,11 @@ typedef struct weaveffi_error { } weaveffi_error; WEAVEFFI_API void weaveffi_error_clear(weaveffi_error* err); + +/* Async completion callbacks receive a heap-boxed error the consumer + owns; weaveffi_error_free releases the message, the payload, and the + box itself. Passing NULL is a safe no-op. */ +WEAVEFFI_API void weaveffi_error_free(weaveffi_error* err); WEAVEFFI_API void weaveffi_free_string(const char* ptr); WEAVEFFI_API void weaveffi_free_bytes(uint8_t* ptr, size_t len); diff --git a/crates/weaveffi-cli/tests/snapshots/c_nested_modules__weaveffi_h.snap b/crates/weaveffi-cli/tests/snapshots/c_nested_modules__weaveffi_h.snap index 09bb6445..f334f7ae 100644 --- a/crates/weaveffi-cli/tests/snapshots/c_nested_modules__weaveffi_h.snap +++ b/crates/weaveffi-cli/tests/snapshots/c_nested_modules__weaveffi_h.snap @@ -56,6 +56,11 @@ typedef struct weaveffi_error { } weaveffi_error; WEAVEFFI_API void weaveffi_error_clear(weaveffi_error* err); + +/* Async completion callbacks receive a heap-boxed error the consumer + owns; weaveffi_error_free releases the message, the payload, and the + box itself. Passing NULL is a safe no-op. */ +WEAVEFFI_API void weaveffi_error_free(weaveffi_error* err); WEAVEFFI_API void weaveffi_free_string(const char* ptr); WEAVEFFI_API void weaveffi_free_bytes(uint8_t* ptr, size_t len); diff --git a/crates/weaveffi-cli/tests/snapshots/c_shapes__weaveffi_h.snap b/crates/weaveffi-cli/tests/snapshots/c_shapes__weaveffi_h.snap index 9c0cea87..51187592 100644 --- a/crates/weaveffi-cli/tests/snapshots/c_shapes__weaveffi_h.snap +++ b/crates/weaveffi-cli/tests/snapshots/c_shapes__weaveffi_h.snap @@ -56,6 +56,11 @@ typedef struct weaveffi_error { } weaveffi_error; WEAVEFFI_API void weaveffi_error_clear(weaveffi_error* err); + +/* Async completion callbacks receive a heap-boxed error the consumer + owns; weaveffi_error_free releases the message, the payload, and the + box itself. Passing NULL is a safe no-op. */ +WEAVEFFI_API void weaveffi_error_free(weaveffi_error* err); WEAVEFFI_API void weaveffi_free_string(const char* ptr); WEAVEFFI_API void weaveffi_free_bytes(uint8_t* ptr, size_t len); diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_async_demo__weaveffi_hpp.snap b/crates/weaveffi-cli/tests/snapshots/cpp_async_demo__weaveffi_hpp.snap index 61aa6579..b8284f45 100644 --- a/crates/weaveffi-cli/tests/snapshots/cpp_async_demo__weaveffi_hpp.snap +++ b/crates/weaveffi-cli/tests/snapshots/cpp_async_demo__weaveffi_hpp.snap @@ -61,6 +61,11 @@ typedef struct weaveffi_error { } weaveffi_error; WEAVEFFI_API void weaveffi_error_clear(weaveffi_error* err); + +/* Async completion callbacks receive a heap-boxed error the consumer + owns; weaveffi_error_free releases the message, the payload, and the + box itself. Passing NULL is a safe no-op. */ +WEAVEFFI_API void weaveffi_error_free(weaveffi_error* err); WEAVEFFI_API void weaveffi_free_string(const char* ptr); WEAVEFFI_API void weaveffi_free_bytes(uint8_t* ptr, size_t len); @@ -364,10 +369,12 @@ inline std::future run_task(const std::string& name) { if (err && err->code != 0) { std::string msg(err->message ? err->message : "unknown error"); p->set_exception(detail::make_tasks_error(err->code, msg, err->payload_ptr, err->payload_len)); + weaveffi_error_free(err); } else { detail::BufferReader result_r(result_ptr, result_len); TaskResult value = detail::read_TaskResult(result_r); result_r.expect_end(); + weaveffi_free_bytes(const_cast(result_ptr), result_len); p->set_value(std::move(value)); } delete p; @@ -388,6 +395,7 @@ inline std::future> run_batch(const std::vectorcode != 0) { std::string msg(err->message ? err->message : "unknown error"); p->set_exception(detail::make_error(err->code, msg)); + weaveffi_error_free(err); } else { detail::BufferReader result_r(result_ptr, result_len); std::vector value{}; @@ -400,6 +408,7 @@ inline std::future> run_batch(const std::vector(result_ptr), result_len); p->set_value(std::move(value)); } delete p; @@ -422,6 +431,7 @@ inline std::future run_n_tasks(int32_t n) { if (err && err->code != 0) { std::string msg(err->message ? err->message : "unknown error"); p->set_exception(detail::make_error(err->code, msg)); + weaveffi_error_free(err); } else { p->set_value(result); } diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_calculator__weaveffi_hpp.snap b/crates/weaveffi-cli/tests/snapshots/cpp_calculator__weaveffi_hpp.snap index c0246c15..7ebc0ca7 100644 --- a/crates/weaveffi-cli/tests/snapshots/cpp_calculator__weaveffi_hpp.snap +++ b/crates/weaveffi-cli/tests/snapshots/cpp_calculator__weaveffi_hpp.snap @@ -58,6 +58,11 @@ typedef struct weaveffi_error { } weaveffi_error; WEAVEFFI_API void weaveffi_error_clear(weaveffi_error* err); + +/* Async completion callbacks receive a heap-boxed error the consumer + owns; weaveffi_error_free releases the message, the payload, and the + box itself. Passing NULL is a safe no-op. */ +WEAVEFFI_API void weaveffi_error_free(weaveffi_error* err); WEAVEFFI_API void weaveffi_free_string(const char* ptr); WEAVEFFI_API void weaveffi_free_bytes(uint8_t* ptr, size_t len); diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_contacts__weaveffi_hpp.snap b/crates/weaveffi-cli/tests/snapshots/cpp_contacts__weaveffi_hpp.snap index 5c557057..eb4799ee 100644 --- a/crates/weaveffi-cli/tests/snapshots/cpp_contacts__weaveffi_hpp.snap +++ b/crates/weaveffi-cli/tests/snapshots/cpp_contacts__weaveffi_hpp.snap @@ -60,6 +60,11 @@ typedef struct weaveffi_error { } weaveffi_error; WEAVEFFI_API void weaveffi_error_clear(weaveffi_error* err); + +/* Async completion callbacks receive a heap-boxed error the consumer + owns; weaveffi_error_free releases the message, the payload, and the + box itself. Passing NULL is a safe no-op. */ +WEAVEFFI_API void weaveffi_error_free(weaveffi_error* err); WEAVEFFI_API void weaveffi_free_string(const char* ptr); WEAVEFFI_API void weaveffi_free_bytes(uint8_t* ptr, size_t len); diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_docs_everywhere__weaveffi_hpp.snap b/crates/weaveffi-cli/tests/snapshots/cpp_docs_everywhere__weaveffi_hpp.snap index 83ad3fa1..fdd89779 100644 --- a/crates/weaveffi-cli/tests/snapshots/cpp_docs_everywhere__weaveffi_hpp.snap +++ b/crates/weaveffi-cli/tests/snapshots/cpp_docs_everywhere__weaveffi_hpp.snap @@ -63,6 +63,11 @@ typedef struct weaveffi_error { } weaveffi_error; WEAVEFFI_API void weaveffi_error_clear(weaveffi_error* err); + +/* Async completion callbacks receive a heap-boxed error the consumer + owns; weaveffi_error_free releases the message, the payload, and the + box itself. Passing NULL is a safe no-op. */ +WEAVEFFI_API void weaveffi_error_free(weaveffi_error* err); WEAVEFFI_API void weaveffi_free_string(const char* ptr); WEAVEFFI_API void weaveffi_free_bytes(uint8_t* ptr, size_t len); @@ -614,6 +619,7 @@ inline std::future publish_async(int64_t id) { if (err && err->code != 0) { std::string msg(err->message ? err->message : "unknown error"); p->set_exception(detail::make_error(err->code, msg)); + weaveffi_error_free(err); } else { p->set_value(result); } diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_events__weaveffi_hpp.snap b/crates/weaveffi-cli/tests/snapshots/cpp_events__weaveffi_hpp.snap index 34f55630..b1584d64 100644 --- a/crates/weaveffi-cli/tests/snapshots/cpp_events__weaveffi_hpp.snap +++ b/crates/weaveffi-cli/tests/snapshots/cpp_events__weaveffi_hpp.snap @@ -62,6 +62,11 @@ typedef struct weaveffi_error { } weaveffi_error; WEAVEFFI_API void weaveffi_error_clear(weaveffi_error* err); + +/* Async completion callbacks receive a heap-boxed error the consumer + owns; weaveffi_error_free releases the message, the payload, and the + box itself. Passing NULL is a safe no-op. */ +WEAVEFFI_API void weaveffi_error_free(weaveffi_error* err); WEAVEFFI_API void weaveffi_free_string(const char* ptr); WEAVEFFI_API void weaveffi_free_bytes(uint8_t* ptr, size_t len); diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_inventory__weaveffi_hpp.snap b/crates/weaveffi-cli/tests/snapshots/cpp_inventory__weaveffi_hpp.snap index 1198c153..3223ff8d 100644 --- a/crates/weaveffi-cli/tests/snapshots/cpp_inventory__weaveffi_hpp.snap +++ b/crates/weaveffi-cli/tests/snapshots/cpp_inventory__weaveffi_hpp.snap @@ -60,6 +60,11 @@ typedef struct weaveffi_error { } weaveffi_error; WEAVEFFI_API void weaveffi_error_clear(weaveffi_error* err); + +/* Async completion callbacks receive a heap-boxed error the consumer + owns; weaveffi_error_free releases the message, the payload, and the + box itself. Passing NULL is a safe no-op. */ +WEAVEFFI_API void weaveffi_error_free(weaveffi_error* err); WEAVEFFI_API void weaveffi_free_string(const char* ptr); WEAVEFFI_API void weaveffi_free_bytes(uint8_t* ptr, size_t len); diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_kitchen_sink__weaveffi_hpp.snap b/crates/weaveffi-cli/tests/snapshots/cpp_kitchen_sink__weaveffi_hpp.snap index 18a6025f..e7bc99e9 100644 --- a/crates/weaveffi-cli/tests/snapshots/cpp_kitchen_sink__weaveffi_hpp.snap +++ b/crates/weaveffi-cli/tests/snapshots/cpp_kitchen_sink__weaveffi_hpp.snap @@ -65,6 +65,11 @@ typedef struct weaveffi_error { } weaveffi_error; WEAVEFFI_API void weaveffi_error_clear(weaveffi_error* err); + +/* Async completion callbacks receive a heap-boxed error the consumer + owns; weaveffi_error_free releases the message, the payload, and the + box itself. Passing NULL is a safe no-op. */ +WEAVEFFI_API void weaveffi_error_free(weaveffi_error* err); WEAVEFFI_API void weaveffi_free_string(const char* ptr); WEAVEFFI_API void weaveffi_free_bytes(uint8_t* ptr, size_t len); @@ -914,8 +919,11 @@ inline std::future do_async(const std::string& input) { if (err && err->code != 0) { std::string msg(err->message ? err->message : "unknown error"); p->set_exception(detail::make_error(err->code, msg)); + weaveffi_error_free(err); } else { - p->set_value(std::string(result)); + std::string value(result ? result : ""); + weaveffi_free_string(result); + p->set_value(std::move(value)); } delete p; }, static_cast(promise_ptr)); @@ -931,8 +939,11 @@ inline std::future do_cancellable(const std::string& input, weaveff if (err && err->code != 0) { std::string msg(err->message ? err->message : "unknown error"); p->set_exception(detail::make_error(err->code, msg)); + weaveffi_error_free(err); } else { - p->set_value(std::string(result)); + std::string value(result ? result : ""); + weaveffi_free_string(result); + p->set_value(std::move(value)); } delete p; }, static_cast(promise_ptr)); diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_kvstore__weaveffi_hpp.snap b/crates/weaveffi-cli/tests/snapshots/cpp_kvstore__weaveffi_hpp.snap index 550bdb39..a1a7caa7 100644 --- a/crates/weaveffi-cli/tests/snapshots/cpp_kvstore__weaveffi_hpp.snap +++ b/crates/weaveffi-cli/tests/snapshots/cpp_kvstore__weaveffi_hpp.snap @@ -65,6 +65,11 @@ typedef struct weaveffi_error { } weaveffi_error; WEAVEFFI_API void weaveffi_error_clear(weaveffi_error* err); + +/* Async completion callbacks receive a heap-boxed error the consumer + owns; weaveffi_error_free releases the message, the payload, and the + box itself. Passing NULL is a safe no-op. */ +WEAVEFFI_API void weaveffi_error_free(weaveffi_error* err); WEAVEFFI_API void weaveffi_free_string(const char* ptr); WEAVEFFI_API void weaveffi_free_bytes(uint8_t* ptr, size_t len); @@ -725,6 +730,7 @@ public: if (err && err->code != 0) { std::string msg(err->message ? err->message : "unknown error"); p->set_exception(detail::make_kv_error(err->code, msg, err->payload_ptr, err->payload_len)); + weaveffi_error_free(err); } else { p->set_value(result); } diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_nested_modules__weaveffi_hpp.snap b/crates/weaveffi-cli/tests/snapshots/cpp_nested_modules__weaveffi_hpp.snap index f557eaae..4ca70d39 100644 --- a/crates/weaveffi-cli/tests/snapshots/cpp_nested_modules__weaveffi_hpp.snap +++ b/crates/weaveffi-cli/tests/snapshots/cpp_nested_modules__weaveffi_hpp.snap @@ -60,6 +60,11 @@ typedef struct weaveffi_error { } weaveffi_error; WEAVEFFI_API void weaveffi_error_clear(weaveffi_error* err); + +/* Async completion callbacks receive a heap-boxed error the consumer + owns; weaveffi_error_free releases the message, the payload, and the + box itself. Passing NULL is a safe no-op. */ +WEAVEFFI_API void weaveffi_error_free(weaveffi_error* err); WEAVEFFI_API void weaveffi_free_string(const char* ptr); WEAVEFFI_API void weaveffi_free_bytes(uint8_t* ptr, size_t len); diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_shapes__weaveffi_hpp.snap b/crates/weaveffi-cli/tests/snapshots/cpp_shapes__weaveffi_hpp.snap index 622bef77..73a8b2d4 100644 --- a/crates/weaveffi-cli/tests/snapshots/cpp_shapes__weaveffi_hpp.snap +++ b/crates/weaveffi-cli/tests/snapshots/cpp_shapes__weaveffi_hpp.snap @@ -61,6 +61,11 @@ typedef struct weaveffi_error { } weaveffi_error; WEAVEFFI_API void weaveffi_error_clear(weaveffi_error* err); + +/* Async completion callbacks receive a heap-boxed error the consumer + owns; weaveffi_error_free releases the message, the payload, and the + box itself. Passing NULL is a safe no-op. */ +WEAVEFFI_API void weaveffi_error_free(weaveffi_error* err); WEAVEFFI_API void weaveffi_free_string(const char* ptr); WEAVEFFI_API void weaveffi_free_bytes(uint8_t* ptr, size_t len); diff --git a/crates/weaveffi-cli/tests/snapshots/dart_async_demo__lib_weaveffi_dart.snap b/crates/weaveffi-cli/tests/snapshots/dart_async_demo__lib_weaveffi_dart.snap index 42c76059..d3353042 100644 --- a/crates/weaveffi-cli/tests/snapshots/dart_async_demo__lib_weaveffi_dart.snap +++ b/crates/weaveffi-cli/tests/snapshots/dart_async_demo__lib_weaveffi_dart.snap @@ -42,6 +42,11 @@ 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< @@ -382,11 +387,12 @@ Future runTask(String name) { final code = err.ref.code; final msg = err.ref.message.toDartString(); final payload = _copyNativeBytes(err.ref.payloadPtr, err.ref.payloadLen); - _weaveffiErrorClear(err); + _weaveffiErrorFree(err); completer.completeError(_mapTaskException(code, msg, payload)); return; } final resultData = _copyNativeBytes(result, resultLen); + _weaveffiFreeBytes(result, resultLen); final resultReader = _BufferReader(resultData); final value = _unpackTaskResult(resultReader); resultReader.expectEnd(); @@ -432,11 +438,12 @@ Future> runBatch(List names) { if (err.address != 0 && err.ref.code != 0) { final code = err.ref.code; final msg = err.ref.message.toDartString(); - _weaveffiErrorClear(err); + _weaveffiErrorFree(err); completer.completeError(WeaveFFIException(code, msg)); return; } final resultData = _copyNativeBytes(result, resultLen); + _weaveffiFreeBytes(result, resultLen); final resultReader = _BufferReader(resultData); final value = List.generate(resultReader.readLength(), (_) => _unpackTaskResult(resultReader)); resultReader.expectEnd(); @@ -490,7 +497,7 @@ Future runNTasks(int n) { if (err.address != 0 && err.ref.code != 0) { final code = err.ref.code; final msg = err.ref.message.toDartString(); - _weaveffiErrorClear(err); + _weaveffiErrorFree(err); completer.completeError(WeaveFFIException(code, msg)); return; } diff --git a/crates/weaveffi-cli/tests/snapshots/dart_calculator__lib_weaveffi_dart.snap b/crates/weaveffi-cli/tests/snapshots/dart_calculator__lib_weaveffi_dart.snap index fd28ad1a..5076c9bd 100644 --- a/crates/weaveffi-cli/tests/snapshots/dart_calculator__lib_weaveffi_dart.snap +++ b/crates/weaveffi-cli/tests/snapshots/dart_calculator__lib_weaveffi_dart.snap @@ -41,6 +41,11 @@ 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< diff --git a/crates/weaveffi-cli/tests/snapshots/dart_contacts__lib_weaveffi_dart.snap b/crates/weaveffi-cli/tests/snapshots/dart_contacts__lib_weaveffi_dart.snap index 1cb1c42d..b749a69f 100644 --- a/crates/weaveffi-cli/tests/snapshots/dart_contacts__lib_weaveffi_dart.snap +++ b/crates/weaveffi-cli/tests/snapshots/dart_contacts__lib_weaveffi_dart.snap @@ -41,6 +41,11 @@ 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< diff --git a/crates/weaveffi-cli/tests/snapshots/dart_docs_everywhere__lib_weaveffi_dart.snap b/crates/weaveffi-cli/tests/snapshots/dart_docs_everywhere__lib_weaveffi_dart.snap index 369e7eaa..382bab49 100644 --- a/crates/weaveffi-cli/tests/snapshots/dart_docs_everywhere__lib_weaveffi_dart.snap +++ b/crates/weaveffi-cli/tests/snapshots/dart_docs_everywhere__lib_weaveffi_dart.snap @@ -42,6 +42,11 @@ 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< @@ -634,7 +639,7 @@ Future publishAsync(int id) { if (err.address != 0 && err.ref.code != 0) { final code = err.ref.code; final msg = err.ref.message.toDartString(); - _weaveffiErrorClear(err); + _weaveffiErrorFree(err); completer.completeError(WeaveFFIException(code, msg)); return; } diff --git a/crates/weaveffi-cli/tests/snapshots/dart_events__lib_weaveffi_dart.snap b/crates/weaveffi-cli/tests/snapshots/dart_events__lib_weaveffi_dart.snap index 5114e951..77dc825d 100644 --- a/crates/weaveffi-cli/tests/snapshots/dart_events__lib_weaveffi_dart.snap +++ b/crates/weaveffi-cli/tests/snapshots/dart_events__lib_weaveffi_dart.snap @@ -41,6 +41,11 @@ 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< diff --git a/crates/weaveffi-cli/tests/snapshots/dart_inventory__lib_weaveffi_dart.snap b/crates/weaveffi-cli/tests/snapshots/dart_inventory__lib_weaveffi_dart.snap index 205adcc2..36da8e2a 100644 --- a/crates/weaveffi-cli/tests/snapshots/dart_inventory__lib_weaveffi_dart.snap +++ b/crates/weaveffi-cli/tests/snapshots/dart_inventory__lib_weaveffi_dart.snap @@ -41,6 +41,11 @@ 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< diff --git a/crates/weaveffi-cli/tests/snapshots/dart_kitchen_sink__lib_weaveffi_dart.snap b/crates/weaveffi-cli/tests/snapshots/dart_kitchen_sink__lib_weaveffi_dart.snap index b830a395..3f9fb87d 100644 --- a/crates/weaveffi-cli/tests/snapshots/dart_kitchen_sink__lib_weaveffi_dart.snap +++ b/crates/weaveffi-cli/tests/snapshots/dart_kitchen_sink__lib_weaveffi_dart.snap @@ -42,6 +42,11 @@ 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< @@ -991,11 +996,13 @@ Future doAsync(String input) { if (err.address != 0 && err.ref.code != 0) { final code = err.ref.code; final msg = err.ref.message.toDartString(); - _weaveffiErrorClear(err); + _weaveffiErrorFree(err); completer.completeError(WeaveFFIException(code, msg)); return; } - completer.complete(result.toDartString()); + final value = result.toDartString(); + _weaveffiFreeString(result); + completer.complete(value); } catch (e) { completer.completeError(e); } finally { @@ -1031,11 +1038,13 @@ Future doCancellable(String input) { if (err.address != 0 && err.ref.code != 0) { final code = err.ref.code; final msg = err.ref.message.toDartString(); - _weaveffiErrorClear(err); + _weaveffiErrorFree(err); completer.completeError(WeaveFFIException(code, msg)); return; } - completer.complete(result.toDartString()); + final value = result.toDartString(); + _weaveffiFreeString(result); + completer.complete(value); } catch (e) { completer.completeError(e); } finally { diff --git a/crates/weaveffi-cli/tests/snapshots/dart_kvstore__lib_weaveffi_dart.snap b/crates/weaveffi-cli/tests/snapshots/dart_kvstore__lib_weaveffi_dart.snap index bd7445c2..60864abd 100644 --- a/crates/weaveffi-cli/tests/snapshots/dart_kvstore__lib_weaveffi_dart.snap +++ b/crates/weaveffi-cli/tests/snapshots/dart_kvstore__lib_weaveffi_dart.snap @@ -42,6 +42,11 @@ 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< @@ -697,7 +702,7 @@ class Store { final code = err.ref.code; final msg = err.ref.message.toDartString(); final payload = _copyNativeBytes(err.ref.payloadPtr, err.ref.payloadLen); - _weaveffiErrorClear(err); + _weaveffiErrorFree(err); completer.completeError(_mapKvException(code, msg, payload)); return; } diff --git a/crates/weaveffi-cli/tests/snapshots/dart_nested_modules__lib_weaveffi_dart.snap b/crates/weaveffi-cli/tests/snapshots/dart_nested_modules__lib_weaveffi_dart.snap index 2cbd05e1..23a4c6bd 100644 --- a/crates/weaveffi-cli/tests/snapshots/dart_nested_modules__lib_weaveffi_dart.snap +++ b/crates/weaveffi-cli/tests/snapshots/dart_nested_modules__lib_weaveffi_dart.snap @@ -41,6 +41,11 @@ 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< diff --git a/crates/weaveffi-cli/tests/snapshots/dart_shapes__lib_weaveffi_dart.snap b/crates/weaveffi-cli/tests/snapshots/dart_shapes__lib_weaveffi_dart.snap index 7575627d..816e7ae7 100644 --- a/crates/weaveffi-cli/tests/snapshots/dart_shapes__lib_weaveffi_dart.snap +++ b/crates/weaveffi-cli/tests/snapshots/dart_shapes__lib_weaveffi_dart.snap @@ -41,6 +41,11 @@ 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< diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_async_demo__WeaveFFI_cs.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_async_demo__WeaveFFI_cs.snap index 866b1cb5..50adeb1a 100644 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_async_demo__WeaveFFI_cs.snap +++ b/crates/weaveffi-cli/tests/snapshots/dotnet_async_demo__WeaveFFI_cs.snap @@ -422,6 +422,9 @@ namespace WeaveFFI [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] internal static extern void weaveffi_error_clear(ref WeaveFFIError err); + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] + internal static extern void weaveffi_error_free(IntPtr err); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate void AsyncCb_weaveffi_tasks_run_task(IntPtr context, IntPtr err, IntPtr result, UIntPtr resultLen); @@ -465,12 +468,14 @@ namespace WeaveFFI { var msg = Marshal.PtrToStringUTF8(wErr.Message) ?? ""; var payload = WeaveFFIError.CopyPayload(wErr); + NativeMethods.weaveffi_error_free(err); tcs.SetException(TaskException.FromCode(wErr.Code, msg, payload)); return; } } var resultBuf = new byte[(int)resultLen]; if (result != IntPtr.Zero && (int)resultLen > 0) Marshal.Copy(result, resultBuf, 0, (int)resultLen); + if (result != IntPtr.Zero) NativeMethods.weaveffi_free_bytes(result, resultLen); var valueReader = new WeaveFFIBufferReader(resultBuf); var value = TaskResult.ReadFrom(valueReader); valueReader.ExpectEnd(); @@ -519,12 +524,14 @@ namespace WeaveFFI if (wErr.Code != 0) { var msg = Marshal.PtrToStringUTF8(wErr.Message) ?? ""; + NativeMethods.weaveffi_error_free(err); tcs.SetException(new WeaveFFIException(wErr.Code, msg)); return; } } var resultBuf = new byte[(int)resultLen]; if (result != IntPtr.Zero && (int)resultLen > 0) Marshal.Copy(result, resultBuf, 0, (int)resultLen); + if (result != IntPtr.Zero) NativeMethods.weaveffi_free_bytes(result, resultLen); var valueReader = new WeaveFFIBufferReader(resultBuf); var valueCount = valueReader.ReadLen(); var value = new TaskResult[valueCount]; @@ -594,6 +601,7 @@ namespace WeaveFFI if (wErr.Code != 0) { var msg = Marshal.PtrToStringUTF8(wErr.Message) ?? ""; + NativeMethods.weaveffi_error_free(err); tcs.SetException(new WeaveFFIException(wErr.Code, msg)); return; } diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_calculator__WeaveFFI_cs.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_calculator__WeaveFFI_cs.snap index 1b3dda86..1ac303ba 100644 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_calculator__WeaveFFI_cs.snap +++ b/crates/weaveffi-cli/tests/snapshots/dotnet_calculator__WeaveFFI_cs.snap @@ -390,6 +390,9 @@ namespace WeaveFFI [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] internal static extern void weaveffi_error_clear(ref WeaveFFIError err); + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] + internal static extern void weaveffi_error_free(IntPtr err); + [DllImport(LibName, EntryPoint = "weaveffi_calculator_add", CallingConvention = CallingConvention.Cdecl)] internal static extern int weaveffi_calculator_add(int a, int b, ref WeaveFFIError err); diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_contacts__WeaveFFI_cs.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_contacts__WeaveFFI_cs.snap index d4dd5950..e9f1f435 100644 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_contacts__WeaveFFI_cs.snap +++ b/crates/weaveffi-cli/tests/snapshots/dotnet_contacts__WeaveFFI_cs.snap @@ -581,6 +581,9 @@ namespace WeaveFFI [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] internal static extern void weaveffi_error_clear(ref WeaveFFIError err); + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] + internal static extern void weaveffi_error_free(IntPtr err); + [DllImport(LibName, EntryPoint = "weaveffi_contacts_ContactBook_destroy", CallingConvention = CallingConvention.Cdecl)] internal static extern void weaveffi_contacts_ContactBook_destroy(IntPtr self); diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_docs_everywhere__WeaveFFI_cs.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_docs_everywhere__WeaveFFI_cs.snap index 4e2ed152..dbf576e3 100644 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_docs_everywhere__WeaveFFI_cs.snap +++ b/crates/weaveffi-cli/tests/snapshots/dotnet_docs_everywhere__WeaveFFI_cs.snap @@ -563,6 +563,9 @@ namespace WeaveFFI [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_destroy", CallingConvention = CallingConvention.Cdecl)] internal static extern void weaveffi_docs_Notebook_destroy(IntPtr self); @@ -714,6 +717,7 @@ namespace WeaveFFI if (wErr.Code != 0) { var msg = Marshal.PtrToStringUTF8(wErr.Message) ?? ""; + NativeMethods.weaveffi_error_free(err); tcs.SetException(new WeaveFFIException(wErr.Code, msg)); return; } diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_events__WeaveFFI_cs.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_events__WeaveFFI_cs.snap index 5d1ff23a..2cf1cdc2 100644 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_events__WeaveFFI_cs.snap +++ b/crates/weaveffi-cli/tests/snapshots/dotnet_events__WeaveFFI_cs.snap @@ -382,6 +382,9 @@ namespace WeaveFFI [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] internal static extern void weaveffi_error_clear(ref WeaveFFIError err); + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] + internal static extern void weaveffi_error_free(IntPtr err); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate void Cb_weaveffi_events_OnMessage_fn(IntPtr message, IntPtr context); diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_inventory__WeaveFFI_cs.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_inventory__WeaveFFI_cs.snap index 37b35148..d9e994b5 100644 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_inventory__WeaveFFI_cs.snap +++ b/crates/weaveffi-cli/tests/snapshots/dotnet_inventory__WeaveFFI_cs.snap @@ -701,6 +701,9 @@ namespace WeaveFFI [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] internal static extern void weaveffi_error_clear(ref WeaveFFIError err); + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] + internal static extern void weaveffi_error_free(IntPtr err); + [DllImport(LibName, EntryPoint = "weaveffi_products_Catalog_destroy", CallingConvention = CallingConvention.Cdecl)] internal static extern void weaveffi_products_Catalog_destroy(IntPtr self); diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_kitchen_sink__WeaveFFI_cs.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_kitchen_sink__WeaveFFI_cs.snap index c6156e50..8f792b31 100644 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_kitchen_sink__WeaveFFI_cs.snap +++ b/crates/weaveffi-cli/tests/snapshots/dotnet_kitchen_sink__WeaveFFI_cs.snap @@ -661,6 +661,9 @@ namespace WeaveFFI [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 err); @@ -1087,11 +1090,14 @@ namespace WeaveFFI 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(Marshal.PtrToStringUTF8(result) ?? ""); + var str = Marshal.PtrToStringUTF8(result) ?? ""; + if (result != IntPtr.Zero) NativeMethods.weaveffi_free_string(result); + tcs.SetResult(str); } finally { @@ -1137,11 +1143,14 @@ namespace WeaveFFI 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(Marshal.PtrToStringUTF8(result) ?? ""); + var str = Marshal.PtrToStringUTF8(result) ?? ""; + if (result != IntPtr.Zero) NativeMethods.weaveffi_free_string(result); + tcs.SetResult(str); } finally { diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_kvstore__WeaveFFI_cs.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_kvstore__WeaveFFI_cs.snap index f492ed1e..7d2f1429 100644 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_kvstore__WeaveFFI_cs.snap +++ b/crates/weaveffi-cli/tests/snapshots/dotnet_kvstore__WeaveFFI_cs.snap @@ -726,6 +726,7 @@ namespace WeaveFFI { 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; } @@ -848,6 +849,9 @@ namespace WeaveFFI [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] internal static extern void weaveffi_error_clear(ref WeaveFFIError err); + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] + internal static extern void weaveffi_error_free(IntPtr err); + [DllImport(LibName, EntryPoint = "weaveffi_kv_Store_destroy", CallingConvention = CallingConvention.Cdecl)] internal static extern void weaveffi_kv_Store_destroy(IntPtr self); diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_nested_modules__WeaveFFI_cs.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_nested_modules__WeaveFFI_cs.snap index f7c7e812..ac956587 100644 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_nested_modules__WeaveFFI_cs.snap +++ b/crates/weaveffi-cli/tests/snapshots/dotnet_nested_modules__WeaveFFI_cs.snap @@ -481,6 +481,9 @@ namespace WeaveFFI [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 err); diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_shapes__WeaveFFI_cs.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_shapes__WeaveFFI_cs.snap index c6ff3f02..6563d3f7 100644 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_shapes__WeaveFFI_cs.snap +++ b/crates/weaveffi-cli/tests/snapshots/dotnet_shapes__WeaveFFI_cs.snap @@ -566,6 +566,9 @@ namespace WeaveFFI [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 err); diff --git a/crates/weaveffi-cli/tests/snapshots/go_async_demo__weaveffi_go.snap b/crates/weaveffi-cli/tests/snapshots/go_async_demo__weaveffi_go.snap index 6e738b94..0b0ec545 100644 --- a/crates/weaveffi-cli/tests/snapshots/go_async_demo__weaveffi_go.snap +++ b/crates/weaveffi-cli/tests/snapshots/go_async_demo__weaveffi_go.snap @@ -65,6 +65,22 @@ func wvTakeError(cErr *C.weaveffi_error) (int32, string, []byte) { 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} } @@ -388,10 +404,10 @@ func goWv_weaveffi_tasks_run_task_callback(context unsafe.Pointer, err *C.weavef } ch := v.(chan wvOutcomeTasksRunTask) if err != nil && err.code != 0 { - ch <- wvOutcomeTasksRunTask{err: wvMapTasks(wvTakeError(err))} + ch <- wvOutcomeTasksRunTask{err: wvMapTasks(wvTakeBoxedError(err))} return } - rRes := &wvReader{buf: wvBorrowBuffer(result_ptr, result_len)} + rRes := &wvReader{buf: wvCopyBuffer(result_ptr, result_len)} var val TaskResult val = wvUnpackTaskResult(rRes) rRes.expectEnd() @@ -425,10 +441,10 @@ func goWv_weaveffi_tasks_run_batch_callback(context unsafe.Pointer, err *C.weave } ch := v.(chan wvOutcomeTasksRunBatch) if err != nil && err.code != 0 { - ch <- wvOutcomeTasksRunBatch{err: wvBrandError(wvTakeError(err))} + ch <- wvOutcomeTasksRunBatch{err: wvBrandError(wvTakeBoxedError(err))} return } - rRes := &wvReader{buf: wvBorrowBuffer(result_ptr, result_len)} + rRes := &wvReader{buf: wvCopyBuffer(result_ptr, result_len)} var val []TaskResult nRes0 := rRes.readLen() val = make([]TaskResult, nRes0) @@ -480,7 +496,7 @@ func goWv_weaveffi_tasks_run_n_tasks_callback(context unsafe.Pointer, err *C.wea } ch := v.(chan wvOutcomeTasksRunNTasks) if err != nil && err.code != 0 { - ch <- wvOutcomeTasksRunNTasks{err: wvBrandError(wvTakeError(err))} + ch <- wvOutcomeTasksRunNTasks{err: wvBrandError(wvTakeBoxedError(err))} return } ch <- wvOutcomeTasksRunNTasks{val: int32(result)} diff --git a/crates/weaveffi-cli/tests/snapshots/go_calculator__weaveffi_go.snap b/crates/weaveffi-cli/tests/snapshots/go_calculator__weaveffi_go.snap index ae1867dd..074a1b1b 100644 --- a/crates/weaveffi-cli/tests/snapshots/go_calculator__weaveffi_go.snap +++ b/crates/weaveffi-cli/tests/snapshots/go_calculator__weaveffi_go.snap @@ -50,6 +50,22 @@ func wvTakeError(cErr *C.weaveffi_error) (int32, string, []byte) { 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} } diff --git a/crates/weaveffi-cli/tests/snapshots/go_contacts__weaveffi_go.snap b/crates/weaveffi-cli/tests/snapshots/go_contacts__weaveffi_go.snap index 729620ec..c209027b 100644 --- a/crates/weaveffi-cli/tests/snapshots/go_contacts__weaveffi_go.snap +++ b/crates/weaveffi-cli/tests/snapshots/go_contacts__weaveffi_go.snap @@ -61,6 +61,22 @@ func wvTakeError(cErr *C.weaveffi_error) (int32, string, []byte) { 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} } diff --git a/crates/weaveffi-cli/tests/snapshots/go_docs_everywhere__weaveffi_go.snap b/crates/weaveffi-cli/tests/snapshots/go_docs_everywhere__weaveffi_go.snap index 47ae3c59..0e8c0acd 100644 --- a/crates/weaveffi-cli/tests/snapshots/go_docs_everywhere__weaveffi_go.snap +++ b/crates/weaveffi-cli/tests/snapshots/go_docs_everywhere__weaveffi_go.snap @@ -64,6 +64,22 @@ func wvTakeError(cErr *C.weaveffi_error) (int32, string, []byte) { 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} } @@ -586,7 +602,7 @@ func goWv_weaveffi_docs_publish_async_callback(context unsafe.Pointer, err *C.we } ch := v.(chan wvOutcomeDocsPublishAsync) if err != nil && err.code != 0 { - ch <- wvOutcomeDocsPublishAsync{err: wvBrandError(wvTakeError(err))} + ch <- wvOutcomeDocsPublishAsync{err: wvBrandError(wvTakeBoxedError(err))} return } ch <- wvOutcomeDocsPublishAsync{val: cToBool(result)} diff --git a/crates/weaveffi-cli/tests/snapshots/go_events__weaveffi_go.snap b/crates/weaveffi-cli/tests/snapshots/go_events__weaveffi_go.snap index f94d076e..ac887889 100644 --- a/crates/weaveffi-cli/tests/snapshots/go_events__weaveffi_go.snap +++ b/crates/weaveffi-cli/tests/snapshots/go_events__weaveffi_go.snap @@ -53,6 +53,22 @@ func wvTakeError(cErr *C.weaveffi_error) (int32, string, []byte) { 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} } diff --git a/crates/weaveffi-cli/tests/snapshots/go_inventory__weaveffi_go.snap b/crates/weaveffi-cli/tests/snapshots/go_inventory__weaveffi_go.snap index f7752e11..efd246d0 100644 --- a/crates/weaveffi-cli/tests/snapshots/go_inventory__weaveffi_go.snap +++ b/crates/weaveffi-cli/tests/snapshots/go_inventory__weaveffi_go.snap @@ -61,6 +61,22 @@ func wvTakeError(cErr *C.weaveffi_error) (int32, string, []byte) { 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} } diff --git a/crates/weaveffi-cli/tests/snapshots/go_kitchen_sink__weaveffi_go.snap b/crates/weaveffi-cli/tests/snapshots/go_kitchen_sink__weaveffi_go.snap index 7ccb77f0..273b8078 100644 --- a/crates/weaveffi-cli/tests/snapshots/go_kitchen_sink__weaveffi_go.snap +++ b/crates/weaveffi-cli/tests/snapshots/go_kitchen_sink__weaveffi_go.snap @@ -66,6 +66,22 @@ func wvTakeError(cErr *C.weaveffi_error) (int32, string, []byte) { 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} } @@ -811,12 +827,13 @@ func goWv_weaveffi_kitchen_do_async_callback(context unsafe.Pointer, err *C.weav } ch := v.(chan wvOutcomeKitchenDoAsync) if err != nil && err.code != 0 { - ch <- wvOutcomeKitchenDoAsync{err: wvBrandError(wvTakeError(err))} + ch <- wvOutcomeKitchenDoAsync{err: wvBrandError(wvTakeBoxedError(err))} return } val := "" if result != nil { val = C.GoString(result) + C.weaveffi_free_string(result) } ch <- wvOutcomeKitchenDoAsync{val: val} } @@ -849,12 +866,13 @@ func goWv_weaveffi_kitchen_do_cancellable_callback(context unsafe.Pointer, err * } ch := v.(chan wvOutcomeKitchenDoCancellable) if err != nil && err.code != 0 { - ch <- wvOutcomeKitchenDoCancellable{err: wvBrandError(wvTakeError(err))} + ch <- wvOutcomeKitchenDoCancellable{err: wvBrandError(wvTakeBoxedError(err))} return } val := "" if result != nil { val = C.GoString(result) + C.weaveffi_free_string(result) } ch <- wvOutcomeKitchenDoCancellable{val: val} } diff --git a/crates/weaveffi-cli/tests/snapshots/go_kvstore__weaveffi_go.snap b/crates/weaveffi-cli/tests/snapshots/go_kvstore__weaveffi_go.snap index 955f1100..8c134bc6 100644 --- a/crates/weaveffi-cli/tests/snapshots/go_kvstore__weaveffi_go.snap +++ b/crates/weaveffi-cli/tests/snapshots/go_kvstore__weaveffi_go.snap @@ -65,6 +65,22 @@ func wvTakeError(cErr *C.weaveffi_error) (int32, string, []byte) { 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} } @@ -611,7 +627,7 @@ func goWv_weaveffi_kv_Store_compact_callback(context unsafe.Pointer, err *C.weav } ch := v.(chan wvOutcomeKvStoreCompact) if err != nil && err.code != 0 { - ch <- wvOutcomeKvStoreCompact{err: wvMapKv(wvTakeError(err))} + ch <- wvOutcomeKvStoreCompact{err: wvMapKv(wvTakeBoxedError(err))} return } ch <- wvOutcomeKvStoreCompact{val: int64(result)} diff --git a/crates/weaveffi-cli/tests/snapshots/go_nested_modules__weaveffi_go.snap b/crates/weaveffi-cli/tests/snapshots/go_nested_modules__weaveffi_go.snap index f3eb10a9..1681a6ab 100644 --- a/crates/weaveffi-cli/tests/snapshots/go_nested_modules__weaveffi_go.snap +++ b/crates/weaveffi-cli/tests/snapshots/go_nested_modules__weaveffi_go.snap @@ -53,6 +53,22 @@ func wvTakeError(cErr *C.weaveffi_error) (int32, string, []byte) { 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} } diff --git a/crates/weaveffi-cli/tests/snapshots/go_shapes__weaveffi_go.snap b/crates/weaveffi-cli/tests/snapshots/go_shapes__weaveffi_go.snap index 9e9bcaa3..abf4fa77 100644 --- a/crates/weaveffi-cli/tests/snapshots/go_shapes__weaveffi_go.snap +++ b/crates/weaveffi-cli/tests/snapshots/go_shapes__weaveffi_go.snap @@ -53,6 +53,22 @@ func wvTakeError(cErr *C.weaveffi_error) (int32, string, []byte) { 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} } diff --git a/crates/weaveffi-cli/tests/snapshots/node_async_demo__weaveffi_addon_c.snap b/crates/weaveffi-cli/tests/snapshots/node_async_demo__weaveffi_addon_c.snap index 93fc767f..b018ccb8 100644 --- a/crates/weaveffi-cli/tests/snapshots/node_async_demo__weaveffi_addon_c.snap +++ b/crates/weaveffi-cli/tests/snapshots/node_async_demo__weaveffi_addon_c.snap @@ -47,7 +47,9 @@ static void weaveffi_tasks_run_task_napi_cb(void* context, weaveffi_error* err, } 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); } @@ -112,7 +114,9 @@ static void weaveffi_tasks_run_batch_napi_cb(void* context, weaveffi_error* err, } 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); } @@ -192,6 +196,7 @@ static void weaveffi_tasks_run_n_tasks_napi_cb(void* context, weaveffi_error* er } else { ctx->result = result; } + weaveffi_error_free(err); napi_call_threadsafe_function(ctx->tsfn, ctx, napi_tsfn_blocking); } diff --git a/crates/weaveffi-cli/tests/snapshots/node_docs_everywhere__weaveffi_addon_c.snap b/crates/weaveffi-cli/tests/snapshots/node_docs_everywhere__weaveffi_addon_c.snap index 8492427d..0aa8984d 100644 --- a/crates/weaveffi-cli/tests/snapshots/node_docs_everywhere__weaveffi_addon_c.snap +++ b/crates/weaveffi-cli/tests/snapshots/node_docs_everywhere__weaveffi_addon_c.snap @@ -273,6 +273,7 @@ static void weaveffi_docs_publish_async_napi_cb(void* context, weaveffi_error* e } else { ctx->result = result; } + weaveffi_error_free(err); napi_call_threadsafe_function(ctx->tsfn, ctx, napi_tsfn_blocking); } diff --git a/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__weaveffi_addon_c.snap b/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__weaveffi_addon_c.snap index d3771903..0806b12c 100644 --- a/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__weaveffi_addon_c.snap +++ b/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__weaveffi_addon_c.snap @@ -583,7 +583,9 @@ static void weaveffi_kitchen_do_async_napi_cb(void* context, weaveffi_error* err } 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); } @@ -648,7 +650,9 @@ static void weaveffi_kitchen_do_cancellable_napi_cb(void* context, weaveffi_erro } 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); } diff --git a/crates/weaveffi-cli/tests/snapshots/node_kvstore__weaveffi_addon_c.snap b/crates/weaveffi-cli/tests/snapshots/node_kvstore__weaveffi_addon_c.snap index de8a9690..76c7d872 100644 --- a/crates/weaveffi-cli/tests/snapshots/node_kvstore__weaveffi_addon_c.snap +++ b/crates/weaveffi-cli/tests/snapshots/node_kvstore__weaveffi_addon_c.snap @@ -289,6 +289,7 @@ static void weaveffi_kv_Store_compact_napi_cb(void* context, weaveffi_error* err } else { ctx->result = result; } + weaveffi_error_free(err); napi_call_threadsafe_function(ctx->tsfn, ctx, napi_tsfn_blocking); } diff --git a/crates/weaveffi-cli/tests/snapshots/python_async_demo__weaveffi_weaveffi_py.snap b/crates/weaveffi-cli/tests/snapshots/python_async_demo__weaveffi_weaveffi_py.snap index f42df782..38680aee 100644 --- a/crates/weaveffi-cli/tests/snapshots/python_async_demo__weaveffi_weaveffi_py.snap +++ b/crates/weaveffi-cli/tests/snapshots/python_async_demo__weaveffi_weaveffi_py.snap @@ -51,6 +51,10 @@ def _load_library() -> ctypes.CDLL: _lib = _load_library() _lib.weaveffi_error_clear.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] _lib.weaveffi_error_clear.restype = None +# Async completion callbacks receive a heap-boxed error the consumer owns; +# weaveffi_error_free releases the message, the payload, and the box. +_lib.weaveffi_error_free.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] +_lib.weaveffi_error_free.restype = None # The free helpers take raw addresses (`c_void_p`) so wrappers can release # owned producer allocations they hold as plain integers or typed pointers. _lib.weaveffi_free_string.argtypes = [ctypes.c_void_p] @@ -367,18 +371,18 @@ async def run_task(name: str) -> "TaskResult": _fut = _loop.create_future() def _cb_impl(context, err, result_ptr, result_len): - # Fires exactly once, on a producer thread: convert (copying - # borrowed buffers) here, then hop back to the event loop. + # 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_clear(ctypes.byref(err.contents)) + _lib.weaveffi_error_free(err) _state["err"] = _task_error_from(_code, _msg, _payload) else: try: - _state["val"] = _unpack_TaskResult(ctypes.string_at(result_ptr, result_len) if result_ptr else b"") + _state["val"] = _unpack_TaskResult(_take_buffer(ctypes.cast(result_ptr, ctypes.c_void_p).value, result_len)) except Exception as _exc: _state["err"] = _exc @@ -409,17 +413,17 @@ async def run_batch(names: List[str]) -> List["TaskResult"]: _fut = _loop.create_future() def _cb_impl(context, err, result_ptr, result_len): - # Fires exactly once, on a producer thread: convert (copying - # borrowed buffers) here, then hop back to the event loop. + # 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_clear(ctypes.byref(err.contents)) + _lib.weaveffi_error_free(err) _state["err"] = WeaveFFIError(_code, _msg) else: try: - _state["val"] = _decode_buffer(ctypes.string_at(result_ptr, result_len) if result_ptr else b"", lambda _r: [_read_TaskResult(_r) for _i0 in range(_r.read_len())]) + _state["val"] = _decode_buffer(_take_buffer(ctypes.cast(result_ptr, ctypes.c_void_p).value, result_len), lambda _r: [_read_TaskResult(_r) for _i0 in range(_r.read_len())]) except Exception as _exc: _state["err"] = _exc @@ -465,13 +469,13 @@ async def run_n_tasks(n: int) -> int: _fut = _loop.create_future() def _cb_impl(context, err, result): - # Fires exactly once, on a producer thread: convert (copying - # borrowed buffers) here, then hop back to the event loop. + # 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_clear(ctypes.byref(err.contents)) + _lib.weaveffi_error_free(err) _state["err"] = WeaveFFIError(_code, _msg) else: try: diff --git a/crates/weaveffi-cli/tests/snapshots/python_calculator__weaveffi_weaveffi_py.snap b/crates/weaveffi-cli/tests/snapshots/python_calculator__weaveffi_weaveffi_py.snap index ec12dfee..bfde59ea 100644 --- a/crates/weaveffi-cli/tests/snapshots/python_calculator__weaveffi_weaveffi_py.snap +++ b/crates/weaveffi-cli/tests/snapshots/python_calculator__weaveffi_weaveffi_py.snap @@ -51,6 +51,10 @@ def _load_library() -> ctypes.CDLL: _lib = _load_library() _lib.weaveffi_error_clear.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] _lib.weaveffi_error_clear.restype = None +# Async completion callbacks receive a heap-boxed error the consumer owns; +# weaveffi_error_free releases the message, the payload, and the box. +_lib.weaveffi_error_free.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] +_lib.weaveffi_error_free.restype = None # The free helpers take raw addresses (`c_void_p`) so wrappers can release # owned producer allocations they hold as plain integers or typed pointers. _lib.weaveffi_free_string.argtypes = [ctypes.c_void_p] diff --git a/crates/weaveffi-cli/tests/snapshots/python_contacts__weaveffi_weaveffi_py.snap b/crates/weaveffi-cli/tests/snapshots/python_contacts__weaveffi_weaveffi_py.snap index f100a7e5..b6191e71 100644 --- a/crates/weaveffi-cli/tests/snapshots/python_contacts__weaveffi_weaveffi_py.snap +++ b/crates/weaveffi-cli/tests/snapshots/python_contacts__weaveffi_weaveffi_py.snap @@ -51,6 +51,10 @@ def _load_library() -> ctypes.CDLL: _lib = _load_library() _lib.weaveffi_error_clear.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] _lib.weaveffi_error_clear.restype = None +# Async completion callbacks receive a heap-boxed error the consumer owns; +# weaveffi_error_free releases the message, the payload, and the box. +_lib.weaveffi_error_free.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] +_lib.weaveffi_error_free.restype = None # The free helpers take raw addresses (`c_void_p`) so wrappers can release # owned producer allocations they hold as plain integers or typed pointers. _lib.weaveffi_free_string.argtypes = [ctypes.c_void_p] diff --git a/crates/weaveffi-cli/tests/snapshots/python_docs_everywhere__weaveffi_weaveffi_py.snap b/crates/weaveffi-cli/tests/snapshots/python_docs_everywhere__weaveffi_weaveffi_py.snap index e7daf088..9bca75c0 100644 --- a/crates/weaveffi-cli/tests/snapshots/python_docs_everywhere__weaveffi_weaveffi_py.snap +++ b/crates/weaveffi-cli/tests/snapshots/python_docs_everywhere__weaveffi_weaveffi_py.snap @@ -51,6 +51,10 @@ def _load_library() -> ctypes.CDLL: _lib = _load_library() _lib.weaveffi_error_clear.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] _lib.weaveffi_error_clear.restype = None +# Async completion callbacks receive a heap-boxed error the consumer owns; +# weaveffi_error_free releases the message, the payload, and the box. +_lib.weaveffi_error_free.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] +_lib.weaveffi_error_free.restype = None # The free helpers take raw addresses (`c_void_p`) so wrappers can release # owned producer allocations they hold as plain integers or typed pointers. _lib.weaveffi_free_string.argtypes = [ctypes.c_void_p] @@ -591,13 +595,13 @@ async def publish_async(id: int) -> bool: _fut = _loop.create_future() def _cb_impl(context, err, result): - # Fires exactly once, on a producer thread: convert (copying - # borrowed buffers) here, then hop back to the event loop. + # 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_clear(ctypes.byref(err.contents)) + _lib.weaveffi_error_free(err) _state["err"] = WeaveFFIError(_code, _msg) else: try: diff --git a/crates/weaveffi-cli/tests/snapshots/python_events__weaveffi_weaveffi_py.snap b/crates/weaveffi-cli/tests/snapshots/python_events__weaveffi_weaveffi_py.snap index dbdcd34f..0fd935eb 100644 --- a/crates/weaveffi-cli/tests/snapshots/python_events__weaveffi_weaveffi_py.snap +++ b/crates/weaveffi-cli/tests/snapshots/python_events__weaveffi_weaveffi_py.snap @@ -51,6 +51,10 @@ def _load_library() -> ctypes.CDLL: _lib = _load_library() _lib.weaveffi_error_clear.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] _lib.weaveffi_error_clear.restype = None +# Async completion callbacks receive a heap-boxed error the consumer owns; +# weaveffi_error_free releases the message, the payload, and the box. +_lib.weaveffi_error_free.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] +_lib.weaveffi_error_free.restype = None # The free helpers take raw addresses (`c_void_p`) so wrappers can release # owned producer allocations they hold as plain integers or typed pointers. _lib.weaveffi_free_string.argtypes = [ctypes.c_void_p] diff --git a/crates/weaveffi-cli/tests/snapshots/python_inventory__weaveffi_weaveffi_py.snap b/crates/weaveffi-cli/tests/snapshots/python_inventory__weaveffi_weaveffi_py.snap index 78abb033..dd68682d 100644 --- a/crates/weaveffi-cli/tests/snapshots/python_inventory__weaveffi_weaveffi_py.snap +++ b/crates/weaveffi-cli/tests/snapshots/python_inventory__weaveffi_weaveffi_py.snap @@ -51,6 +51,10 @@ def _load_library() -> ctypes.CDLL: _lib = _load_library() _lib.weaveffi_error_clear.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] _lib.weaveffi_error_clear.restype = None +# Async completion callbacks receive a heap-boxed error the consumer owns; +# weaveffi_error_free releases the message, the payload, and the box. +_lib.weaveffi_error_free.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] +_lib.weaveffi_error_free.restype = None # The free helpers take raw addresses (`c_void_p`) so wrappers can release # owned producer allocations they hold as plain integers or typed pointers. _lib.weaveffi_free_string.argtypes = [ctypes.c_void_p] diff --git a/crates/weaveffi-cli/tests/snapshots/python_kitchen_sink__weaveffi_weaveffi_py.snap b/crates/weaveffi-cli/tests/snapshots/python_kitchen_sink__weaveffi_weaveffi_py.snap index ca3ece92..6fef1c15 100644 --- a/crates/weaveffi-cli/tests/snapshots/python_kitchen_sink__weaveffi_weaveffi_py.snap +++ b/crates/weaveffi-cli/tests/snapshots/python_kitchen_sink__weaveffi_weaveffi_py.snap @@ -51,6 +51,10 @@ def _load_library() -> ctypes.CDLL: _lib = _load_library() _lib.weaveffi_error_clear.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] _lib.weaveffi_error_clear.restype = None +# Async completion callbacks receive a heap-boxed error the consumer owns; +# weaveffi_error_free releases the message, the payload, and the box. +_lib.weaveffi_error_free.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] +_lib.weaveffi_error_free.restype = None # The free helpers take raw addresses (`c_void_p`) so wrappers can release # owned producer allocations they hold as plain integers or typed pointers. _lib.weaveffi_free_string.argtypes = [ctypes.c_void_p] @@ -837,17 +841,17 @@ async def do_async(input: str) -> str: _fut = _loop.create_future() def _cb_impl(context, err, result): - # Fires exactly once, on a producer thread: convert (copying - # borrowed buffers) here, then hop back to the event loop. + # 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_clear(ctypes.byref(err.contents)) + _lib.weaveffi_error_free(err) _state["err"] = WeaveFFIError(_code, _msg) else: try: - _state["val"] = _bytes_to_string(result) or "" + _state["val"] = _take_string(result) or "" except Exception as _exc: _state["err"] = _exc @@ -863,7 +867,7 @@ async def do_async(input: str) -> str: _loop.call_soon_threadsafe(_resolve) - _cb_type = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(_WeaveFFIErrorStruct), ctypes.c_char_p) + _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] @@ -880,17 +884,17 @@ async def do_cancellable(input: str) -> str: _fut = _loop.create_future() def _cb_impl(context, err, result): - # Fires exactly once, on a producer thread: convert (copying - # borrowed buffers) here, then hop back to the event loop. + # 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_clear(ctypes.byref(err.contents)) + _lib.weaveffi_error_free(err) _state["err"] = WeaveFFIError(_code, _msg) else: try: - _state["val"] = _bytes_to_string(result) or "" + _state["val"] = _take_string(result) or "" except Exception as _exc: _state["err"] = _exc @@ -906,7 +910,7 @@ async def do_cancellable(input: str) -> str: _loop.call_soon_threadsafe(_resolve) - _cb_type = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(_WeaveFFIErrorStruct), ctypes.c_char_p) + _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] diff --git a/crates/weaveffi-cli/tests/snapshots/python_kvstore__weaveffi_weaveffi_py.snap b/crates/weaveffi-cli/tests/snapshots/python_kvstore__weaveffi_weaveffi_py.snap index e5e54ee9..2565cc71 100644 --- a/crates/weaveffi-cli/tests/snapshots/python_kvstore__weaveffi_weaveffi_py.snap +++ b/crates/weaveffi-cli/tests/snapshots/python_kvstore__weaveffi_weaveffi_py.snap @@ -51,6 +51,10 @@ def _load_library() -> ctypes.CDLL: _lib = _load_library() _lib.weaveffi_error_clear.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] _lib.weaveffi_error_clear.restype = None +# Async completion callbacks receive a heap-boxed error the consumer owns; +# weaveffi_error_free releases the message, the payload, and the box. +_lib.weaveffi_error_free.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] +_lib.weaveffi_error_free.restype = None # The free helpers take raw addresses (`c_void_p`) so wrappers can release # owned producer allocations they hold as plain integers or typed pointers. _lib.weaveffi_free_string.argtypes = [ctypes.c_void_p] @@ -643,14 +647,14 @@ class Store: _fut = _loop.create_future() def _cb_impl(context, err, result): - # Fires exactly once, on a producer thread: convert (copying - # borrowed buffers) here, then hop back to the event loop. + # 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_clear(ctypes.byref(err.contents)) + _lib.weaveffi_error_free(err) _state["err"] = _kv_error_from(_code, _msg, _payload) else: try: diff --git a/crates/weaveffi-cli/tests/snapshots/python_nested_modules__weaveffi_weaveffi_py.snap b/crates/weaveffi-cli/tests/snapshots/python_nested_modules__weaveffi_weaveffi_py.snap index 7d8dc657..e0b78be5 100644 --- a/crates/weaveffi-cli/tests/snapshots/python_nested_modules__weaveffi_weaveffi_py.snap +++ b/crates/weaveffi-cli/tests/snapshots/python_nested_modules__weaveffi_weaveffi_py.snap @@ -51,6 +51,10 @@ def _load_library() -> ctypes.CDLL: _lib = _load_library() _lib.weaveffi_error_clear.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] _lib.weaveffi_error_clear.restype = None +# Async completion callbacks receive a heap-boxed error the consumer owns; +# weaveffi_error_free releases the message, the payload, and the box. +_lib.weaveffi_error_free.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] +_lib.weaveffi_error_free.restype = None # The free helpers take raw addresses (`c_void_p`) so wrappers can release # owned producer allocations they hold as plain integers or typed pointers. _lib.weaveffi_free_string.argtypes = [ctypes.c_void_p] diff --git a/crates/weaveffi-cli/tests/snapshots/python_shapes__weaveffi_weaveffi_py.snap b/crates/weaveffi-cli/tests/snapshots/python_shapes__weaveffi_weaveffi_py.snap index deffd870..ebd36397 100644 --- a/crates/weaveffi-cli/tests/snapshots/python_shapes__weaveffi_weaveffi_py.snap +++ b/crates/weaveffi-cli/tests/snapshots/python_shapes__weaveffi_weaveffi_py.snap @@ -51,6 +51,10 @@ def _load_library() -> ctypes.CDLL: _lib = _load_library() _lib.weaveffi_error_clear.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] _lib.weaveffi_error_clear.restype = None +# Async completion callbacks receive a heap-boxed error the consumer owns; +# weaveffi_error_free releases the message, the payload, and the box. +_lib.weaveffi_error_free.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] +_lib.weaveffi_error_free.restype = None # The free helpers take raw addresses (`c_void_p`) so wrappers can release # owned producer allocations they hold as plain integers or typed pointers. _lib.weaveffi_free_string.argtypes = [ctypes.c_void_p] diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_async_demo__lib_weaveffi_rb.snap b/crates/weaveffi-cli/tests/snapshots/ruby_async_demo__lib_weaveffi_rb.snap index b60cf41a..3e229d3c 100644 --- a/crates/weaveffi-cli/tests/snapshots/ruby_async_demo__lib_weaveffi_rb.snap +++ b/crates/weaveffi-cli/tests/snapshots/ruby_async_demo__lib_weaveffi_rb.snap @@ -216,6 +216,7 @@ module WeaveFFI end attach_function :weaveffi_error_clear, [:pointer], :void + attach_function :weaveffi_error_free, [:pointer], :void attach_function :weaveffi_free_string, [:pointer], :void attach_function :weaveffi_free_bytes, [:pointer, :size_t], :void @@ -326,11 +327,12 @@ module WeaveFFI code = err[:code] msg = err[:message].null? ? '' : err[:message].read_string payload = err[:payload_ptr].null? ? nil : err[:payload_ptr].read_string(err[:payload_len]) - weaveffi_error_clear(err_ptr) + weaveffi_error_free(err_ptr) queue << task_error_from(code, msg, payload) else begin _wv_r = WvBufferReader.new(result_ptr.null? ? ''.b : result_ptr.read_string(result_len)) + weaveffi_free_bytes(result_ptr, result_len) unless result_ptr.null? _wv_v = _wv_read_task_result(_wv_r) _wv_r.expect_end! queue << _wv_v @@ -354,11 +356,12 @@ module WeaveFFI if err && err[:code] != 0 code = err[:code] msg = err[:message].null? ? '' : err[:message].read_string - weaveffi_error_clear(err_ptr) + weaveffi_error_free(err_ptr) queue << Error.new(code, msg) else begin _wv_r = WvBufferReader.new(result_ptr.null? ? ''.b : result_ptr.read_string(result_len)) + weaveffi_free_bytes(result_ptr, result_len) unless result_ptr.null? _wv_v = Array.new(_wv_r.read_len) do _wv_e0 = _wv_read_task_result(_wv_r) _wv_e0 @@ -400,7 +403,7 @@ module WeaveFFI if err && err[:code] != 0 code = err[:code] msg = err[:message].null? ? '' : err[:message].read_string - weaveffi_error_clear(err_ptr) + weaveffi_error_free(err_ptr) queue << Error.new(code, msg) else queue << result diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_calculator__lib_weaveffi_rb.snap b/crates/weaveffi-cli/tests/snapshots/ruby_calculator__lib_weaveffi_rb.snap index 33ee2691..558dd27e 100644 --- a/crates/weaveffi-cli/tests/snapshots/ruby_calculator__lib_weaveffi_rb.snap +++ b/crates/weaveffi-cli/tests/snapshots/ruby_calculator__lib_weaveffi_rb.snap @@ -216,6 +216,7 @@ module WeaveFFI end attach_function :weaveffi_error_clear, [:pointer], :void + attach_function :weaveffi_error_free, [:pointer], :void attach_function :weaveffi_free_string, [:pointer], :void attach_function :weaveffi_free_bytes, [:pointer, :size_t], :void diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_contacts__lib_weaveffi_rb.snap b/crates/weaveffi-cli/tests/snapshots/ruby_contacts__lib_weaveffi_rb.snap index 572ce7b1..5343fd0f 100644 --- a/crates/weaveffi-cli/tests/snapshots/ruby_contacts__lib_weaveffi_rb.snap +++ b/crates/weaveffi-cli/tests/snapshots/ruby_contacts__lib_weaveffi_rb.snap @@ -216,6 +216,7 @@ module WeaveFFI end attach_function :weaveffi_error_clear, [:pointer], :void + attach_function :weaveffi_error_free, [:pointer], :void attach_function :weaveffi_free_string, [:pointer], :void attach_function :weaveffi_free_bytes, [:pointer, :size_t], :void diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_docs_everywhere__lib_weaveffi_rb.snap b/crates/weaveffi-cli/tests/snapshots/ruby_docs_everywhere__lib_weaveffi_rb.snap index dd32646f..49bc48f4 100644 --- a/crates/weaveffi-cli/tests/snapshots/ruby_docs_everywhere__lib_weaveffi_rb.snap +++ b/crates/weaveffi-cli/tests/snapshots/ruby_docs_everywhere__lib_weaveffi_rb.snap @@ -216,6 +216,7 @@ module WeaveFFI end attach_function :weaveffi_error_clear, [:pointer], :void + attach_function :weaveffi_error_free, [:pointer], :void attach_function :weaveffi_free_string, [:pointer], :void attach_function :weaveffi_free_bytes, [:pointer, :size_t], :void @@ -539,7 +540,7 @@ module WeaveFFI if err && err[:code] != 0 code = err[:code] msg = err[:message].null? ? '' : err[:message].read_string - weaveffi_error_clear(err_ptr) + weaveffi_error_free(err_ptr) queue << Error.new(code, msg) else queue << (result != 0) diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_events__lib_weaveffi_rb.snap b/crates/weaveffi-cli/tests/snapshots/ruby_events__lib_weaveffi_rb.snap index 9d94a197..8df2f71d 100644 --- a/crates/weaveffi-cli/tests/snapshots/ruby_events__lib_weaveffi_rb.snap +++ b/crates/weaveffi-cli/tests/snapshots/ruby_events__lib_weaveffi_rb.snap @@ -216,6 +216,7 @@ module WeaveFFI end attach_function :weaveffi_error_clear, [:pointer], :void + attach_function :weaveffi_error_free, [:pointer], :void attach_function :weaveffi_free_string, [:pointer], :void attach_function :weaveffi_free_bytes, [:pointer, :size_t], :void diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_inventory__lib_weaveffi_rb.snap b/crates/weaveffi-cli/tests/snapshots/ruby_inventory__lib_weaveffi_rb.snap index 59d9efba..6a467392 100644 --- a/crates/weaveffi-cli/tests/snapshots/ruby_inventory__lib_weaveffi_rb.snap +++ b/crates/weaveffi-cli/tests/snapshots/ruby_inventory__lib_weaveffi_rb.snap @@ -216,6 +216,7 @@ module WeaveFFI end attach_function :weaveffi_error_clear, [:pointer], :void + attach_function :weaveffi_error_free, [:pointer], :void attach_function :weaveffi_free_string, [:pointer], :void attach_function :weaveffi_free_bytes, [:pointer, :size_t], :void diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_kitchen_sink__lib_weaveffi_rb.snap b/crates/weaveffi-cli/tests/snapshots/ruby_kitchen_sink__lib_weaveffi_rb.snap index dd99050a..e99049ea 100644 --- a/crates/weaveffi-cli/tests/snapshots/ruby_kitchen_sink__lib_weaveffi_rb.snap +++ b/crates/weaveffi-cli/tests/snapshots/ruby_kitchen_sink__lib_weaveffi_rb.snap @@ -216,6 +216,7 @@ module WeaveFFI end attach_function :weaveffi_error_clear, [:pointer], :void + attach_function :weaveffi_error_free, [:pointer], :void attach_function :weaveffi_free_string, [:pointer], :void attach_function :weaveffi_free_bytes, [:pointer, :size_t], :void @@ -826,10 +827,12 @@ module WeaveFFI if err && err[:code] != 0 code = err[:code] msg = err[:message].null? ? '' : err[:message].read_string - weaveffi_error_clear(err_ptr) + weaveffi_error_free(err_ptr) queue << Error.new(code, msg) else - queue << (result.null? ? '' : result.read_string) + _wv_s = result.null? ? '' : result.read_string + weaveffi_free_string(result) unless result.null? + queue << _wv_s end end weaveffi_kitchen_do_async_async(input, callback, FFI::Pointer::NULL) @@ -848,10 +851,12 @@ module WeaveFFI if err && err[:code] != 0 code = err[:code] msg = err[:message].null? ? '' : err[:message].read_string - weaveffi_error_clear(err_ptr) + weaveffi_error_free(err_ptr) queue << Error.new(code, msg) else - queue << (result.null? ? '' : result.read_string) + _wv_s = result.null? ? '' : result.read_string + 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) diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_kvstore__lib_weaveffi_rb.snap b/crates/weaveffi-cli/tests/snapshots/ruby_kvstore__lib_weaveffi_rb.snap index c54f7eb0..3f1fa29f 100644 --- a/crates/weaveffi-cli/tests/snapshots/ruby_kvstore__lib_weaveffi_rb.snap +++ b/crates/weaveffi-cli/tests/snapshots/ruby_kvstore__lib_weaveffi_rb.snap @@ -216,6 +216,7 @@ module WeaveFFI end attach_function :weaveffi_error_clear, [:pointer], :void + attach_function :weaveffi_error_free, [:pointer], :void attach_function :weaveffi_free_string, [:pointer], :void attach_function :weaveffi_free_bytes, [:pointer, :size_t], :void @@ -590,7 +591,7 @@ module WeaveFFI code = err[:code] msg = err[:message].null? ? '' : err[:message].read_string payload = err[:payload_ptr].null? ? nil : err[:payload_ptr].read_string(err[:payload_len]) - WeaveFFI.weaveffi_error_clear(err_ptr) + WeaveFFI.weaveffi_error_free(err_ptr) queue << WeaveFFI.kv_error_from(code, msg, payload) else queue << result diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_nested_modules__lib_weaveffi_rb.snap b/crates/weaveffi-cli/tests/snapshots/ruby_nested_modules__lib_weaveffi_rb.snap index 5fd22bed..92c6b9ea 100644 --- a/crates/weaveffi-cli/tests/snapshots/ruby_nested_modules__lib_weaveffi_rb.snap +++ b/crates/weaveffi-cli/tests/snapshots/ruby_nested_modules__lib_weaveffi_rb.snap @@ -216,6 +216,7 @@ module WeaveFFI end attach_function :weaveffi_error_clear, [:pointer], :void + attach_function :weaveffi_error_free, [:pointer], :void attach_function :weaveffi_free_string, [:pointer], :void attach_function :weaveffi_free_bytes, [:pointer, :size_t], :void diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_shapes__lib_weaveffi_rb.snap b/crates/weaveffi-cli/tests/snapshots/ruby_shapes__lib_weaveffi_rb.snap index 5a21b0d0..2958d1fb 100644 --- a/crates/weaveffi-cli/tests/snapshots/ruby_shapes__lib_weaveffi_rb.snap +++ b/crates/weaveffi-cli/tests/snapshots/ruby_shapes__lib_weaveffi_rb.snap @@ -216,6 +216,7 @@ module WeaveFFI end attach_function :weaveffi_error_clear, [:pointer], :void + attach_function :weaveffi_error_free, [:pointer], :void attach_function :weaveffi_free_string, [:pointer], :void attach_function :weaveffi_free_bytes, [:pointer, :size_t], :void diff --git a/crates/weaveffi-cli/tests/snapshots/swift_async_demo__Sources_WeaveFFI_WeaveFFI_swift.snap b/crates/weaveffi-cli/tests/snapshots/swift_async_demo__Sources_WeaveFFI_WeaveFFI_swift.snap index fa5ca346..a189616a 100644 --- a/crates/weaveffi-cli/tests/snapshots/swift_async_demo__Sources_WeaveFFI_WeaveFFI_swift.snap +++ b/crates/weaveffi-cli/tests/snapshots/swift_async_demo__Sources_WeaveFFI_WeaveFFI_swift.snap @@ -244,6 +244,7 @@ public enum Tasks { let code = err.pointee.code let msg = err.pointee.message.flatMap { String(cString: $0) } ?? "" let payload: [UInt8]? = err.pointee.payload_ptr.map { [UInt8](UnsafeBufferPointer(start: $0, count: err.pointee.payload_len)) } + weaveffi_error_free(err) contRef.value.resume(throwing: mapTasks(code: code, message: msg, payload: payload)) } else { guard let resultPtr = resultPtr else { @@ -251,6 +252,7 @@ public enum Tasks { return } let resultBytes = [UInt8](UnsafeBufferPointer(start: resultPtr, count: resultLen)) + weaveffi_free_bytes(UnsafeMutablePointer(mutating: resultPtr), resultLen) var resultReader = WvReader(bytes: resultBytes) let v0 = wvReadTaskResult(&resultReader) resultReader.finish() @@ -276,12 +278,14 @@ public enum Tasks { if let err = err, err.pointee.code != 0 { let code = err.pointee.code let msg = err.pointee.message.flatMap { String(cString: $0) } ?? "" + weaveffi_error_free(err) fatalError("\(code): \(msg)") } else { guard let resultPtr = resultPtr else { fatalError("-1: null buffer") } let resultBytes = [UInt8](UnsafeBufferPointer(start: resultPtr, count: resultLen)) + weaveffi_free_bytes(UnsafeMutablePointer(mutating: resultPtr), resultLen) var resultReader = WvReader(bytes: resultBytes) let n2 = resultReader.readLen() var v1: [TaskResult] = [] @@ -311,6 +315,7 @@ public enum Tasks { 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) 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 index f871449f..01cda7f7 100644 --- 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 @@ -429,6 +429,7 @@ public enum Docs { 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) 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 index cb2d88be..450e5539 100644 --- 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 @@ -672,12 +672,15 @@ public enum Kitchen { 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") } - contRef.value.resume(returning: String(cString: result)) + let resultValue = String(cString: result) + weaveffi_free_string(result) + contRef.value.resume(returning: resultValue) } }, ctx) } @@ -693,12 +696,15 @@ public enum Kitchen { 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") } - contRef.value.resume(returning: String(cString: result)) + let resultValue = String(cString: result) + weaveffi_free_string(result) + contRef.value.resume(returning: resultValue) } }, ctx) } diff --git a/crates/weaveffi-cli/tests/snapshots/swift_kvstore__Sources_WeaveFFI_WeaveFFI_swift.snap b/crates/weaveffi-cli/tests/snapshots/swift_kvstore__Sources_WeaveFFI_WeaveFFI_swift.snap index 72cbf15f..21756547 100644 --- a/crates/weaveffi-cli/tests/snapshots/swift_kvstore__Sources_WeaveFFI_WeaveFFI_swift.snap +++ b/crates/weaveffi-cli/tests/snapshots/swift_kvstore__Sources_WeaveFFI_WeaveFFI_swift.snap @@ -443,6 +443,7 @@ public final class Store { 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) diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_async_demo__weaveffi_wasm_js.snap b/crates/weaveffi-cli/tests/snapshots/wasm_async_demo__weaveffi_wasm_js.snap index 485aea8f..fb851343 100644 --- a/crates/weaveffi-cli/tests/snapshots/wasm_async_demo__weaveffi_wasm_js.snap +++ b/crates/weaveffi-cli/tests/snapshots/wasm_async_demo__weaveffi_wasm_js.snap @@ -257,18 +257,19 @@ function _checkTaskError(wasm, errPtr) { } } -// Throw if a borrowed (producer-owned) error carries a non-zero -// code. Used by async callbacks: the producer owns and frees the -// error struct, so the slot is read but never deallocated here. +// Throw if a heap-boxed (consumer-owned) error carries a non-zero +// code. Used by async callbacks: the fields are read, then the box +// is released with weaveffi_error_free before throwing. // `mkErr` maps domain codes (and decodes payload fields) for // throwing callables; without it the generic WeaveFFIError is thrown. function _checkErrRef(wasm, errPtr, mkErr) { const dv = new DataView(wasm.memory.buffer); const code = dv.getInt32(errPtr, true); - if (code === 0) return; + if (code === 0) { wasm.weaveffi_error_free(errPtr); return; } const msg = _readCStr(wasm, dv.getUint32(errPtr + 4, true)) || ''; - if (mkErr) throw mkErr(wasm, code, msg, dv.getUint32(errPtr + 8, true), dv.getUint32(errPtr + 12, true)); - throw new WeaveFFIError(code, msg); + const err = mkErr ? mkErr(wasm, code, msg, dv.getUint32(errPtr + 8, true), dv.getUint32(errPtr + 12, true)) : new WeaveFFIError(code, msg); + wasm.weaveffi_error_free(errPtr); + throw err; } // Serialize a `tasks.TaskResult` record into the value-buffer wire format. @@ -358,6 +359,7 @@ export async function loadWeaveffiWasm(url) { const ctxId = _nextCtxId++; _asyncContexts.set(ctxId, { resolve, reject, mkErr: _taskErrorFrom, unwrap: (w, ptr, len) => { const _b = ptr === 0 || len === 0 ? new Uint8Array(0) : new Uint8Array(w.memory.buffer, ptr, len).slice(); + if (ptr !== 0) w.weaveffi_free_bytes(ptr, len); const _rd = new _BufReader(_b); const _v = _read_tasks_TaskResult(_rd); _rd.end(); @@ -373,6 +375,7 @@ export async function loadWeaveffiWasm(url) { const ctxId = _nextCtxId++; _asyncContexts.set(ctxId, { resolve, reject, unwrap: (w, ptr, len) => { const _b = ptr === 0 || len === 0 ? new Uint8Array(0) : new Uint8Array(w.memory.buffer, ptr, len).slice(); + if (ptr !== 0) w.weaveffi_free_bytes(ptr, len); const _rd = new _BufReader(_b); const _v = (() => { const _n = _rd.len(); const _arr = []; for (let _i = 0; _i < _n; _i++) _arr.push(_read_tasks_TaskResult(_rd)); return _arr; })(); _rd.end(); diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_docs_everywhere__weaveffi_wasm_js.snap b/crates/weaveffi-cli/tests/snapshots/wasm_docs_everywhere__weaveffi_wasm_js.snap index cd636069..05683be5 100644 --- a/crates/weaveffi-cli/tests/snapshots/wasm_docs_everywhere__weaveffi_wasm_js.snap +++ b/crates/weaveffi-cli/tests/snapshots/wasm_docs_everywhere__weaveffi_wasm_js.snap @@ -267,18 +267,19 @@ function _checkDocsErrorsError(wasm, errPtr) { } } -// Throw if a borrowed (producer-owned) error carries a non-zero -// code. Used by async callbacks: the producer owns and frees the -// error struct, so the slot is read but never deallocated here. +// Throw if a heap-boxed (consumer-owned) error carries a non-zero +// code. Used by async callbacks: the fields are read, then the box +// is released with weaveffi_error_free before throwing. // `mkErr` maps domain codes (and decodes payload fields) for // throwing callables; without it the generic WeaveFFIError is thrown. function _checkErrRef(wasm, errPtr, mkErr) { const dv = new DataView(wasm.memory.buffer); const code = dv.getInt32(errPtr, true); - if (code === 0) return; + if (code === 0) { wasm.weaveffi_error_free(errPtr); return; } const msg = _readCStr(wasm, dv.getUint32(errPtr + 4, true)) || ''; - if (mkErr) throw mkErr(wasm, code, msg, dv.getUint32(errPtr + 8, true), dv.getUint32(errPtr + 12, true)); - throw new WeaveFFIError(code, msg); + const err = mkErr ? mkErr(wasm, code, msg, dv.getUint32(errPtr + 8, true), dv.getUint32(errPtr + 12, true)) : new WeaveFFIError(code, msg); + wasm.weaveffi_error_free(errPtr); + throw err; } // Serialize a `docs.Document` record into the value-buffer wire format. diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_kitchen_sink__weaveffi_wasm_js.snap b/crates/weaveffi-cli/tests/snapshots/wasm_kitchen_sink__weaveffi_wasm_js.snap index 5b4f3dfa..72530846 100644 --- a/crates/weaveffi-cli/tests/snapshots/wasm_kitchen_sink__weaveffi_wasm_js.snap +++ b/crates/weaveffi-cli/tests/snapshots/wasm_kitchen_sink__weaveffi_wasm_js.snap @@ -267,18 +267,19 @@ function _checkKitchenErrorsError(wasm, errPtr) { } } -// Throw if a borrowed (producer-owned) error carries a non-zero -// code. Used by async callbacks: the producer owns and frees the -// error struct, so the slot is read but never deallocated here. +// Throw if a heap-boxed (consumer-owned) error carries a non-zero +// code. Used by async callbacks: the fields are read, then the box +// is released with weaveffi_error_free before throwing. // `mkErr` maps domain codes (and decodes payload fields) for // throwing callables; without it the generic WeaveFFIError is thrown. function _checkErrRef(wasm, errPtr, mkErr) { const dv = new DataView(wasm.memory.buffer); const code = dv.getInt32(errPtr, true); - if (code === 0) return; + if (code === 0) { wasm.weaveffi_error_free(errPtr); return; } const msg = _readCStr(wasm, dv.getUint32(errPtr + 4, true)) || ''; - if (mkErr) throw mkErr(wasm, code, msg, dv.getUint32(errPtr + 8, true), dv.getUint32(errPtr + 12, true)); - throw new WeaveFFIError(code, msg); + const err = mkErr ? mkErr(wasm, code, msg, dv.getUint32(errPtr + 8, true), dv.getUint32(errPtr + 12, true)) : new WeaveFFIError(code, msg); + wasm.weaveffi_error_free(errPtr); + throw err; } // Serialize a `shared.Token` record into the value-buffer wire format. @@ -695,7 +696,11 @@ export async function loadWeaveffiWasm(url) { doAsync(input) { return new Promise((resolve, reject) => { const ctxId = _nextCtxId++; - _asyncContexts.set(ctxId, { resolve, reject, unwrap: (w, p) => _readCStr(w, p) }); + _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); wasm.weaveffi_kitchen_do_async_async(a0_p, _cbPtr_i32_i32_i32, ctxId); wasm.weaveffi_dealloc(a0_p, a0_s); @@ -704,7 +709,11 @@ export async function loadWeaveffiWasm(url) { doCancellable(input) { return new Promise((resolve, reject) => { const ctxId = _nextCtxId++; - _asyncContexts.set(ctxId, { resolve, reject, unwrap: (w, p) => _readCStr(w, p) }); + _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); wasm.weaveffi_kitchen_do_cancellable_async(a0_p, 0, _cbPtr_i32_i32_i32, ctxId); wasm.weaveffi_dealloc(a0_p, a0_s); diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_kvstore__weaveffi_wasm_js.snap b/crates/weaveffi-cli/tests/snapshots/wasm_kvstore__weaveffi_wasm_js.snap index 2de49486..9a4aa44c 100644 --- a/crates/weaveffi-cli/tests/snapshots/wasm_kvstore__weaveffi_wasm_js.snap +++ b/crates/weaveffi-cli/tests/snapshots/wasm_kvstore__weaveffi_wasm_js.snap @@ -287,18 +287,19 @@ function _checkKvError(wasm, errPtr) { } } -// Throw if a borrowed (producer-owned) error carries a non-zero -// code. Used by async callbacks: the producer owns and frees the -// error struct, so the slot is read but never deallocated here. +// Throw if a heap-boxed (consumer-owned) error carries a non-zero +// code. Used by async callbacks: the fields are read, then the box +// is released with weaveffi_error_free before throwing. // `mkErr` maps domain codes (and decodes payload fields) for // throwing callables; without it the generic WeaveFFIError is thrown. function _checkErrRef(wasm, errPtr, mkErr) { const dv = new DataView(wasm.memory.buffer); const code = dv.getInt32(errPtr, true); - if (code === 0) return; + if (code === 0) { wasm.weaveffi_error_free(errPtr); return; } const msg = _readCStr(wasm, dv.getUint32(errPtr + 4, true)) || ''; - if (mkErr) throw mkErr(wasm, code, msg, dv.getUint32(errPtr + 8, true), dv.getUint32(errPtr + 12, true)); - throw new WeaveFFIError(code, msg); + const err = mkErr ? mkErr(wasm, code, msg, dv.getUint32(errPtr + 8, true), dv.getUint32(errPtr + 12, true)) : new WeaveFFIError(code, msg); + wasm.weaveffi_error_free(errPtr); + throw err; } // Serialize a `kv.Entry` record into the value-buffer wire format. diff --git a/crates/weaveffi-core/benches/codegen_bench.rs b/crates/weaveffi-core/benches/codegen_bench.rs index 0e21f187..1c3875bf 100644 --- a/crates/weaveffi-core/benches/codegen_bench.rs +++ b/crates/weaveffi-core/benches/codegen_bench.rs @@ -8,7 +8,7 @@ use weaveffi_ir::parse::parse_api_str; fn calculator_api() -> Api { Api { - version: "0.6.0".to_string(), + version: "0.7.0".to_string(), modules: vec![Module { name: "calculator".to_string(), functions: vec![ @@ -129,19 +129,16 @@ fn large_api() -> Api { name: "id".to_string(), ty: TypeRef::I32, doc: None, - default: None, }, StructField { name: "name".to_string(), ty: TypeRef::StringUtf8, doc: None, - default: None, }, StructField { name: "active".to_string(), ty: TypeRef::Bool, doc: None, - default: None, }, ], }) @@ -224,7 +221,7 @@ fn large_api() -> Api { .collect(); Api { - version: "0.6.0".to_string(), + version: "0.7.0".to_string(), modules, generators: None, package: None, @@ -235,8 +232,7 @@ fn bench_validate_small_api(c: &mut Criterion) { let api = calculator_api(); c.bench_function("validate_small_api", |b| { b.iter(|| { - let mut api = api.clone(); - weaveffi_core::validate::validate_api(black_box(&mut api), None).unwrap(); + weaveffi_core::validate::validate_api(black_box(api.clone()), None).unwrap(); }); }); } @@ -245,18 +241,16 @@ fn bench_validate_large_api(c: &mut Criterion) { let api = large_api(); c.bench_function("validate_large_api", |b| { b.iter(|| { - let mut api = api.clone(); - weaveffi_core::validate::validate_api(black_box(&mut api), None).unwrap(); + weaveffi_core::validate::validate_api(black_box(api.clone()), None).unwrap(); }); }); } fn bench_hash_api(c: &mut Criterion) { - let mut api = large_api(); - weaveffi_core::validate::validate_api(&mut api, None).unwrap(); + let api = weaveffi_core::validate::validate_api(large_api(), None).unwrap(); c.bench_function("hash_large_api", |b| { b.iter(|| { - weaveffi_core::cache::hash_api(black_box(&api)); + weaveffi_core::cache::hash_api(black_box(api.api())); }); }); } @@ -277,19 +271,18 @@ fn bench_validate_kitchen_sink(c: &mut Criterion) { let api = load_kitchen_sink_unvalidated(); c.bench_function("validate_kitchen_sink", |b| { b.iter(|| { - let mut api = api.clone(); - weaveffi_core::validate::validate_api(black_box(&mut api), None).unwrap(); + weaveffi_core::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 mut api = load_kitchen_sink_unvalidated(); - weaveffi_core::validate::validate_api(&mut api, None).unwrap(); + let api = + weaveffi_core::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)); + weaveffi_core::cache::hash_api(black_box(api.api())); }); }); } diff --git a/crates/weaveffi-core/src/backend/mod.rs b/crates/weaveffi-core/src/backend/mod.rs index 03b143b8..971c45ba 100644 --- a/crates/weaveffi-core/src/backend/mod.rs +++ b/crates/weaveffi-core/src/backend/mod.rs @@ -22,7 +22,6 @@ use anyhow::Result; use camino::{Utf8Path, Utf8PathBuf}; use serde::Serialize; -use weaveffi_ir::ir::Api; use crate::capabilities::TargetCapabilities; use crate::model::{ @@ -30,6 +29,7 @@ use crate::model::{ ListenerBinding, ModuleBinding, StructBinding, }; use crate::package::{PackageContext, PackagedFile}; +use crate::resolved::ResolvedApi; /// A single generated file: its full path (under the output directory) and the /// rendered contents. Backends return these from [`LanguageBackend::files`]; @@ -61,14 +61,18 @@ impl OutputFile { /// [`OutputFile`] model (rendering is pure; the driver does the I/O), an /// automatically-derived `output_files`, and one uniform `Generator` bridge. /// -/// Backends whose primary file is a straightforward per-module walk override -/// the per-entity hooks (`render_enum`, `render_struct`, `render_function`, and -/// optionally `render_callback`/`render_listener`) and call the provided +/// Backends whose primary file is a straightforward per-module walk (Python, +/// Ruby, Go, Dart, .NET) override the per-entity hooks (`render_enum`, +/// `render_struct`, `render_function`, and optionally +/// `render_callback`/`render_listener`) and call the provided /// [`emit_members`](Self::emit_members) from inside their module scoping; that /// is what removes the hand-rolled walk + call-shape dispatch each generator -/// used to carry. Multi-pass backends (Ruby, .NET, Node, Android) instead build -/// their own layout directly in [`files`](Self::files) and leave the hooks at -/// their no-op defaults. +/// used to carry. Backends whose output is not one linear pass leave the hooks +/// at their no-op defaults and build their layout directly in +/// [`files`](Self::files): C and C++ order declarations by dependency, Swift +/// splits types from the namespaced module body, and Android, Node, and Wasm +/// each render parallel files (Kotlin + JNI C, addon C + JS, JS + `.d.ts`) +/// that would each need their own walk anyway. /// /// Each hook renders into a `String` (matching how generators accumulate /// output) and is responsible for emitting its own doc comments; doc-comment @@ -224,14 +228,14 @@ 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 - /// the source `api` too, for the rare file (e.g. a `.pyi` stub) that needs - /// the raw IR. Most backends render a primary source file by composing - /// [`emit_members`](Self::emit_members) over `model.modules`, then append - /// package manifests (`package.json`, `pyproject.toml`, `go.mod`, …) as - /// additional [`OutputFile`]s. + /// 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 + /// `model.modules`, then append package manifests (`package.json`, + /// `pyproject.toml`, `go.mod`, …) as additional [`OutputFile`]s. fn files( &self, - api: &Api, + api: &ResolvedApi, model: &BindingModel, out_dir: &Utf8Path, config: &Self::Config, @@ -252,7 +256,7 @@ pub trait LanguageBackend: Send + Sync { /// tree, platform-tagged Python wheels, …). The default returns `None`. fn package( &self, - api: &Api, + api: &ResolvedApi, model: &BindingModel, ctx: &PackageContext, out_dir: &Utf8Path, @@ -274,7 +278,7 @@ pub trait LanguageBackend: Send + Sync { /// backend produced cannot be written. pub fn run( backend: &B, - api: &Api, + api: &ResolvedApi, out_dir: &Utf8Path, config: &B::Config, ) -> Result<()> { @@ -311,7 +315,7 @@ fn forward_slashes(path: Utf8PathBuf) -> String { /// identical across operating systems. pub fn output_files( backend: &B, - api: &Api, + api: &ResolvedApi, out_dir: &Utf8Path, config: &B::Config, ) -> Vec { @@ -331,7 +335,7 @@ pub fn output_files( /// generates. Returns `None` when the backend does not support packaging. pub fn package_files( backend: &B, - api: &Api, + api: &ResolvedApi, ctx: &PackageContext, out_dir: &Utf8Path, config: &B::Config, @@ -375,7 +379,7 @@ macro_rules! impl_generator_via_backend { fn generate( &self, - api: &::weaveffi_ir::ir::Api, + api: &$crate::resolved::ResolvedApi, out_dir: &::camino::Utf8Path, config: &Self::Config, ) -> $crate::backend::__anyhow::Result<()> { @@ -384,7 +388,7 @@ macro_rules! impl_generator_via_backend { fn output_files( &self, - api: &::weaveffi_ir::ir::Api, + api: &$crate::resolved::ResolvedApi, out_dir: &::camino::Utf8Path, config: &Self::Config, ) -> ::std::vec::Vec<::std::string::String> { @@ -393,7 +397,7 @@ macro_rules! impl_generator_via_backend { fn package( &self, - api: &::weaveffi_ir::ir::Api, + api: &$crate::resolved::ResolvedApi, ctx: &$crate::package::PackageContext, out_dir: &::camino::Utf8Path, config: &Self::Config, @@ -408,7 +412,7 @@ macro_rules! impl_generator_via_backend { mod tests { use super::*; use crate::codegen::Generator; - use weaveffi_ir::ir::{Function, Module, Param, TypeRef}; + use weaveffi_ir::ir::{Api, Function, Module, Param, TypeRef}; #[derive(Default, Clone, serde::Serialize)] struct FakeConfig { @@ -465,7 +469,7 @@ mod tests { fn files( &self, - _api: &Api, + _api: &ResolvedApi, model: &BindingModel, out_dir: &Utf8Path, config: &Self::Config, @@ -498,9 +502,9 @@ mod tests { } } - fn api() -> Api { - Api { - version: "0.6.0".into(), + fn api() -> ResolvedApi { + ResolvedApi::assume_resolved(Api { + version: weaveffi_ir::ir::CURRENT_SCHEMA_VERSION.into(), modules: vec![Module { name: "math".into(), functions: vec![ @@ -517,7 +521,7 @@ mod tests { }], generators: None, package: None, - } + }) } #[test] diff --git a/crates/weaveffi-core/src/cabi.rs b/crates/weaveffi-core/src/cabi.rs index b71b3bf1..2d337d5c 100644 --- a/crates/weaveffi-core/src/cabi.rs +++ b/crates/weaveffi-core/src/cabi.rs @@ -145,7 +145,11 @@ pub fn render_runtime_decls(out: &mut String, prefix: &str) { const uint8_t* payload_ptr;\n \ size_t payload_len;\n\ }} {prefix}_error;\n\n\ - {api} void {prefix}_error_clear({prefix}_error* err);\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\ {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 \ diff --git a/crates/weaveffi-core/src/cache.rs b/crates/weaveffi-core/src/cache.rs index f9624dac..d4b6922a 100644 --- a/crates/weaveffi-core/src/cache.rs +++ b/crates/weaveffi-core/src/cache.rs @@ -157,6 +157,7 @@ mod tests { use crate::codegen::{ConfiguredGenerator, Generator, Orchestrator, OrchestratorHooks}; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; + use crate::resolved::ResolvedApi; use weaveffi_ir::ir::{Function, Module, Param, TypeRef}; /// Minimal serde-able config so the cache tests can exercise the @@ -173,7 +174,7 @@ mod tests { fn minimal_api() -> Api { Api { - version: "0.6.0".to_string(), + version: "0.7.0".to_string(), modules: vec![Module { name: "math".to_string(), functions: vec![Function { @@ -231,7 +232,7 @@ mod tests { fn generate( &self, - _api: &Api, + _api: &crate::resolved::ResolvedApi, out_dir: &Utf8Path, _config: &Self::Config, ) -> anyhow::Result<()> { @@ -398,7 +399,7 @@ mod tests { let gen = configured("counting", Arc::clone(&calls), TestConfig::default()); let orch = Orchestrator::new().with_generator(&gen); - orch.run(&api, out_dir, &hooks, false).unwrap(); + orch.run(&ResolvedApi::assume_resolved(api.clone()), out_dir, &hooks, false).unwrap(); assert!(out_dir.join(CACHE_DIR).join("counting.hash").exists()); assert_eq!(calls.load(Ordering::SeqCst), 1); @@ -414,10 +415,10 @@ mod tests { let gen = configured("counting", Arc::clone(&calls), TestConfig::default()); let orch = Orchestrator::new().with_generator(&gen); - orch.run(&api, out_dir, &hooks, false).unwrap(); + orch.run(&ResolvedApi::assume_resolved(api.clone()), out_dir, &hooks, false).unwrap(); assert_eq!(calls.load(Ordering::SeqCst), 1); - orch.run(&api, out_dir, &hooks, false).unwrap(); + orch.run(&ResolvedApi::assume_resolved(api.clone()), out_dir, &hooks, false).unwrap(); assert_eq!( calls.load(Ordering::SeqCst), 1, @@ -435,7 +436,7 @@ mod tests { let gen = configured("counting", Arc::clone(&calls), TestConfig::default()); let orch = Orchestrator::new().with_generator(&gen); - orch.run(&api, out_dir, &hooks, false).unwrap(); + orch.run(&ResolvedApi::assume_resolved(api.clone()), out_dir, &hooks, false).unwrap(); assert_eq!(calls.load(Ordering::SeqCst), 1); let mut modified_api = api; @@ -464,7 +465,7 @@ mod tests { since: None, }); - orch.run(&modified_api, out_dir, &hooks, false).unwrap(); + orch.run(&ResolvedApi::assume_resolved(modified_api.clone()), out_dir, &hooks, false).unwrap(); assert_eq!( calls.load(Ordering::SeqCst), 2, @@ -482,10 +483,10 @@ mod tests { let gen = configured("counting", Arc::clone(&calls), TestConfig::default()); let orch = Orchestrator::new().with_generator(&gen); - orch.run(&api, out_dir, &hooks, true).unwrap(); + orch.run(&ResolvedApi::assume_resolved(api.clone()), out_dir, &hooks, true).unwrap(); assert_eq!(calls.load(Ordering::SeqCst), 1); - orch.run(&api, out_dir, &hooks, true).unwrap(); + orch.run(&ResolvedApi::assume_resolved(api.clone()), out_dir, &hooks, true).unwrap(); assert_eq!( calls.load(Ordering::SeqCst), 2, @@ -505,7 +506,7 @@ mod tests { let gen = configured("counting", Arc::clone(&calls), TestConfig::default()); let orch = Orchestrator::new().with_generator(&gen); - orch.run(&api, out_dir, &hooks, false).unwrap(); + orch.run(&ResolvedApi::assume_resolved(api.clone()), out_dir, &hooks, false).unwrap(); assert_eq!( calls.load(Ordering::SeqCst), 1, @@ -528,14 +529,14 @@ mod tests { .with_generator(&s_gen); let api = minimal_api(); - orch.run(&api, out_dir, &hooks, false).unwrap(); + orch.run(&ResolvedApi::assume_resolved(api.clone()), out_dir, &hooks, false).unwrap(); assert_eq!(c_calls.load(Ordering::SeqCst), 1); assert_eq!(s_calls.load(Ordering::SeqCst), 1); // Invalidate only the C generator's cache; the API itself is unchanged. std::fs::remove_file(out_dir.join(CACHE_DIR).join("c.hash")).unwrap(); - orch.run(&api, out_dir, &hooks, false).unwrap(); + orch.run(&ResolvedApi::assume_resolved(api.clone()), out_dir, &hooks, false).unwrap(); assert_eq!( c_calls.load(Ordering::SeqCst), 2, @@ -604,7 +605,7 @@ mod tests { let gen = configured("c", Arc::clone(&calls), TestConfig::default()); Orchestrator::new() .with_generator(&gen) - .run(&api, out_dir, &hooks, false) + .run(&ResolvedApi::assume_resolved(api.clone()), out_dir, &hooks, false) .unwrap(); assert_eq!(calls.load(Ordering::SeqCst), 1); @@ -618,7 +619,7 @@ mod tests { ); Orchestrator::new() .with_generator(&gen2) - .run(&api, out_dir, &hooks, false) + .run(&ResolvedApi::assume_resolved(api.clone()), out_dir, &hooks, false) .unwrap(); assert_eq!( calls.load(Ordering::SeqCst), @@ -629,7 +630,7 @@ mod tests { // A third run with the same `changed` config should hit the cache again. Orchestrator::new() .with_generator(&gen2) - .run(&api, out_dir, &hooks, false) + .run(&ResolvedApi::assume_resolved(api.clone()), out_dir, &hooks, false) .unwrap(); assert_eq!( calls.load(Ordering::SeqCst), @@ -655,7 +656,7 @@ mod tests { let stale = hash_api_for_generator(&api, "c"); write_generator_cache(out_dir, "c", &stale).unwrap(); - orch.run(&api, out_dir, &hooks, false).unwrap(); + orch.run(&ResolvedApi::assume_resolved(api.clone()), out_dir, &hooks, false).unwrap(); assert_eq!( calls.load(Ordering::SeqCst), 1, diff --git a/crates/weaveffi-core/src/capabilities.rs b/crates/weaveffi-core/src/capabilities.rs index f186c16c..d32c6eec 100644 --- a/crates/weaveffi-core/src/capabilities.rs +++ b/crates/weaveffi-core/src/capabilities.rs @@ -243,7 +243,7 @@ mod tests { fn api(modules: Vec) -> Api { Api { - version: "0.6.0".into(), + version: "0.7.0".into(), modules, generators: None, package: None, diff --git a/crates/weaveffi-core/src/codegen/common.rs b/crates/weaveffi-core/src/codegen/common.rs index 63d929d3..8b5f79e9 100644 --- a/crates/weaveffi-core/src/codegen/common.rs +++ b/crates/weaveffi-core/src/codegen/common.rs @@ -1,8 +1,7 @@ //! Shared codegen primitives that every language generator can reuse. //! //! Until 0.4.0, every generator hand-rolled its own copy of the module -//! tree walker, the doc-comment emitter, and the "is this type a C -//! pointer at the ABI boundary?" predicate. Pulling them in here gives +//! tree walker and the doc-comment emitter. Pulling them in here gives //! the generators one source of truth and shrinks each crate by a few //! dozen lines of near-identical helper code. //! @@ -12,7 +11,7 @@ //! their behaviour is non-uniform; this module deliberately covers //! only the common 80%. -use weaveffi_ir::ir::{Module, TypeRef}; +use weaveffi_ir::ir::Module; /// Doc-comment flavour used by [`emit_doc`]. /// @@ -128,34 +127,6 @@ pub fn walk_modules_with_path<'a>( }) } -/// Predicate: returns `true` when the IR type is represented as a -/// pointer at the C ABI boundary. -/// -/// String types, byte buffers, record values, rich (algebraic) enums, -/// interfaces, typed handles, lists, maps, and iterators all cross the ABI as -/// pointers. Scalars (`i32`/`bool`/etc.), `Handle`, and a C-style `Enum(_)` -/// cross by value. -/// -/// `Optional(T)` is *not* automatically a pointer here: callers that -/// care about Optional pointer-ness (the C/C++ generators) recurse -/// into the inner type before consulting this predicate. -pub fn is_c_pointer_type(ty: &TypeRef) -> bool { - matches!( - ty, - TypeRef::StringUtf8 - | TypeRef::BorrowedStr - | TypeRef::Bytes - | TypeRef::BorrowedBytes - | TypeRef::Record(_) - | TypeRef::RichEnum(_) - | TypeRef::Interface(_) - | TypeRef::TypedHandle(_) - | TypeRef::List(_) - | TypeRef::Map(_, _) - | TypeRef::Iterator(_) - ) -} - /// Convert a `snake_case` identifier to `PascalCase` by uppercasing the /// first character of each `_`-separated segment and preserving the rest. /// @@ -397,55 +368,6 @@ mod tests { assert_eq!(out, "/** hello */\n"); } - // --- is_c_pointer_type --- - - #[test] - fn is_c_pointer_for_pointer_carrying_types() { - for ty in [ - TypeRef::StringUtf8, - TypeRef::BorrowedStr, - TypeRef::Bytes, - TypeRef::BorrowedBytes, - TypeRef::Record("X".into()), - TypeRef::RichEnum("Y".into()), - TypeRef::Interface("Z".into()), - TypeRef::TypedHandle("X".into()), - TypeRef::List(Box::new(TypeRef::I32)), - TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::I32)), - TypeRef::Iterator(Box::new(TypeRef::StringUtf8)), - ] { - assert!(is_c_pointer_type(&ty), "expected pointer: {ty:?}"); - } - } - - #[test] - fn is_c_pointer_for_value_types_is_false() { - for ty in [ - TypeRef::I32, - TypeRef::U32, - TypeRef::I64, - TypeRef::F64, - TypeRef::Bool, - TypeRef::Handle, - TypeRef::Enum("E".into()), - ] { - assert!(!is_c_pointer_type(&ty), "expected non-pointer: {ty:?}"); - } - } - - #[test] - fn is_c_pointer_does_not_recurse_into_optional() { - // Callers that care about Optional pointer-ness recurse first. - // We document and enforce that contract: bare Optional is not - // a pointer. - assert!(!is_c_pointer_type(&TypeRef::Optional(Box::new( - TypeRef::I32 - )))); - assert!(!is_c_pointer_type(&TypeRef::Optional(Box::new( - TypeRef::StringUtf8 - )))); - } - // --- pascal_case --- #[test] diff --git a/crates/weaveffi-core/src/codegen/mod.rs b/crates/weaveffi-core/src/codegen/mod.rs index 23bf1f53..07159dda 100644 --- a/crates/weaveffi-core/src/codegen/mod.rs +++ b/crates/weaveffi-core/src/codegen/mod.rs @@ -11,11 +11,11 @@ use anyhow::{bail, Result}; use camino::Utf8Path; use rayon::prelude::*; use serde::Serialize; -use weaveffi_ir::ir::Api; use crate::cache; use crate::capabilities::{self, TargetCapabilities}; use crate::package::{PackageContext, PackagedFile}; +use crate::resolved::ResolvedApi; pub mod common; pub mod writer; @@ -84,13 +84,19 @@ pub trait Generator: Send + Sync { /// /// Returns an error if the target cannot render the API or cannot write its /// output files (for example a filesystem failure). - fn generate(&self, api: &Api, out_dir: &Utf8Path, config: &Self::Config) -> Result<()>; + fn generate(&self, api: &ResolvedApi, out_dir: &Utf8Path, config: &Self::Config) + -> Result<()>; /// Files that [`generate`](Generator::generate) would write, relative /// to (or anchored under) `out_dir`. Used by `--dry-run` and `diff`. /// Default implementation returns the empty list; generators override /// to surface the list without doing any I/O. - fn output_files(&self, _api: &Api, _out_dir: &Utf8Path, _config: &Self::Config) -> Vec { + fn output_files( + &self, + _api: &ResolvedApi, + _out_dir: &Utf8Path, + _config: &Self::Config, + ) -> Vec { vec![] } @@ -101,7 +107,7 @@ pub trait Generator: Send + Sync { /// the default returns `None`. fn package( &self, - _api: &Api, + _api: &ResolvedApi, _ctx: &PackageContext, _out_dir: &Utf8Path, _config: &Self::Config, @@ -131,16 +137,16 @@ pub trait DynGenerator: Send + Sync { /// /// Returns an error if the underlying [`Generator::generate`] fails to /// render or write its output. - fn generate(&self, api: &Api, out_dir: &Utf8Path) -> Result<()>; + fn generate(&self, api: &ResolvedApi, out_dir: &Utf8Path) -> Result<()>; /// The files [`generate`](Self::generate) would write. Mirrors /// [`Generator::output_files`]. - fn output_files(&self, api: &Api, out_dir: &Utf8Path) -> Vec; + fn output_files(&self, api: &ResolvedApi, out_dir: &Utf8Path) -> Vec; /// Assemble the distributable package for this target, using the bound /// config. Mirrors [`Generator::package`]; returns `None` when the target /// does not support packaging. fn package( &self, - api: &Api, + api: &ResolvedApi, ctx: &PackageContext, out_dir: &Utf8Path, ) -> Option>; @@ -191,17 +197,17 @@ impl DynGenerator for ConfiguredGenerator { self.inner.allows_unsupported(&self.config) } - fn generate(&self, api: &Api, out_dir: &Utf8Path) -> Result<()> { + fn generate(&self, api: &ResolvedApi, out_dir: &Utf8Path) -> Result<()> { self.inner.generate(api, out_dir, &self.config) } - fn output_files(&self, api: &Api, out_dir: &Utf8Path) -> Vec { + fn output_files(&self, api: &ResolvedApi, out_dir: &Utf8Path) -> Vec { self.inner.output_files(api, out_dir, &self.config) } fn package( &self, - api: &Api, + api: &ResolvedApi, ctx: &PackageContext, out_dir: &Utf8Path, ) -> Option> { @@ -260,7 +266,7 @@ impl<'a> Orchestrator<'a> { /// fails while rendering, or a cache entry cannot be written. pub fn run( &self, - api: &Api, + api: &ResolvedApi, out_dir: &Utf8Path, hooks: &OrchestratorHooks, force: bool, @@ -273,7 +279,7 @@ impl<'a> Orchestrator<'a> { // surface (throwing stubs) for the missing features instead. let mut violations: Vec = Vec::new(); for g in &self.generators { - let Err(err) = capabilities::check(api, g.name(), &g.capabilities()) else { + let Err(err) = capabilities::check(api.api(), g.name(), &g.capabilities()) else { continue; }; if g.allows_unsupported() { @@ -303,7 +309,7 @@ impl<'a> Orchestrator<'a> { let mut pending: Vec<(&'a dyn DynGenerator, String)> = Vec::new(); for &g in &self.generators { let cfg_bytes = g.config_hash_input(); - let hash = cache::hash_generator_inputs(api, g.name(), &cfg_bytes); + let hash = cache::hash_generator_inputs(api.api(), g.name(), &cfg_bytes); let cached = cache::read_generator_cache(out_dir, g.name()); if cached.as_deref() != Some(hash.as_str()) { pending.push((g, hash)); @@ -340,7 +346,7 @@ mod tests { use super::*; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; - use weaveffi_ir::ir::{Function, Module, Param, TypeRef}; + use weaveffi_ir::ir::{Api, Function, Module, Param, TypeRef}; /// Test generator with a minimal config so tests don't have to depend /// on any real per-language generator crate. @@ -371,7 +377,12 @@ mod tests { config.allow_unsupported } - fn generate(&self, _api: &Api, out_dir: &Utf8Path, _config: &Self::Config) -> Result<()> { + fn generate( + &self, + _api: &ResolvedApi, + out_dir: &Utf8Path, + _config: &Self::Config, + ) -> Result<()> { self.calls.fetch_add(1, Ordering::SeqCst); let dir = out_dir.join(self.name); std::fs::create_dir_all(dir.as_std_path())?; @@ -380,9 +391,9 @@ mod tests { } } - fn test_api() -> Api { - Api { - version: "0.6.0".to_string(), + fn test_api() -> ResolvedApi { + ResolvedApi::assume_resolved(Api { + version: weaveffi_ir::ir::CURRENT_SCHEMA_VERSION.to_string(), modules: vec![Module { name: "math".to_string(), functions: vec![Function { @@ -419,7 +430,7 @@ mod tests { }], generators: None, package: None, - } + }) } fn configured( @@ -438,8 +449,8 @@ mod tests { /// An API that uses listeners, so a target without listener support /// trips the capability gate. - fn listener_api() -> Api { - let mut api = test_api(); + fn listener_api() -> ResolvedApi { + let mut api = test_api().api().clone(); api.modules[0].listeners = vec![weaveffi_ir::ir::ListenerDef { name: "on_change".to_string(), event_callback: "OnChange".to_string(), @@ -450,7 +461,7 @@ mod tests { params: vec![], doc: None, }]; - api + ResolvedApi::assume_resolved(api) } fn partial( @@ -666,11 +677,12 @@ mod tests { // Mutate the API in a way that affects both generators' hashes by // renaming a module. Then pre-seed the Swift cache with the *new* // expected hash so only the C entry stays stale and re-runs. - let mut modified = api.clone(); + let mut modified = api.api().clone(); modified.modules[0].name = "math2".to_string(); + let modified = ResolvedApi::assume_resolved(modified); let new_swift_hash = - cache::hash_generator_inputs(&modified, "swift", &s_gen.config_hash_input()); + cache::hash_generator_inputs(modified.api(), "swift", &s_gen.config_hash_input()); cache::write_generator_cache(out_dir, "swift", &new_swift_hash).unwrap(); orch.run(&modified, out_dir, &hooks, false).unwrap(); diff --git a/crates/weaveffi-core/src/errors.rs b/crates/weaveffi-core/src/errors.rs index 1e70e7a7..cae26a96 100644 --- a/crates/weaveffi-core/src/errors.rs +++ b/crates/weaveffi-core/src/errors.rs @@ -173,7 +173,7 @@ mod tests { fn api_with(mods: Vec) -> Api { Api { - version: "0.6.0".into(), + version: "0.7.0".into(), package: None, modules: mods, generators: None, diff --git a/crates/weaveffi-core/src/lib.rs b/crates/weaveffi-core/src/lib.rs index babc64c5..5e47ce12 100644 --- a/crates/weaveffi-core/src/lib.rs +++ b/crates/weaveffi-core/src/lib.rs @@ -18,6 +18,9 @@ pub mod package; pub mod pkg; pub mod plan; pub mod platform; +pub mod resolved; pub mod utils; pub mod validate; pub mod wire; + +pub use resolved::ResolvedApi; diff --git a/crates/weaveffi-core/src/model/mod.rs b/crates/weaveffi-core/src/model/mod.rs index a09fb80b..5c2d3c8b 100644 --- a/crates/weaveffi-core/src/model/mod.rs +++ b/crates/weaveffi-core/src/model/mod.rs @@ -1,5 +1,6 @@ -//! The **binding model**: a normalized, fully-lowered view of an [`Api`] that -//! every language backend consumes. +//! The **binding model**: a normalized, fully-lowered view of a +//! [`ResolvedApi`](crate::resolved::ResolvedApi) that every language backend +//! consumes. //! //! Before this module existed, each of the eleven generators re-walked the IR, //! re-derived the C ABI calling convention, and re-invented every emitted C @@ -23,7 +24,7 @@ use heck::ToUpperCamelCase; use weaveffi_ir::ir::{ - Api, CallbackDef, EnumDef, ErrorDomain, Function, InterfaceDef, ListenerDef, Module, StructDef, + CallbackDef, EnumDef, ErrorDomain, Function, InterfaceDef, ListenerDef, Module, StructDef, TypeRef, }; @@ -31,6 +32,7 @@ use crate::abi::{ self, async_callback_params, async_input_params, context_param, error_out_param, lower_param, lower_return, sync_signature, AbiParam, CType, ConstPos, }; +use crate::resolved::ResolvedApi; /// 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. @@ -148,9 +150,8 @@ pub struct FnBinding { /// /// Records and rich enums are value types: they declare no C symbols of their /// own and cross the ABI serialized inside a value buffer, so a field is just -/// its name, type, and (for records) optional default. Field declaration -/// order **is** the wire order. -#[derive(Debug, Clone, PartialEq)] +/// its name and type. Field declaration order **is** the wire order. +#[derive(Debug, Clone, PartialEq, Eq)] pub struct FieldBinding { /// The field name as written in the IDL. pub name: String, @@ -158,16 +159,12 @@ pub struct FieldBinding { pub doc: Option, /// The idiomatic IR type of the field. pub ty: TypeRef, - /// Default value used when the field is omitted at construction, kept as - /// the raw IDL value. Only records carry defaults; variant and error - /// payload fields leave this `None`. - pub default: Option, } /// A struct (record), fully lowered: a plain value type generators emit as a /// native data class plus buffer read/write functions. No C symbols exist for /// a record; instances cross the ABI serialized in value buffers. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct StructBinding { /// The struct name as written in the IDL. pub name: String, @@ -186,7 +183,7 @@ pub struct StructBinding { /// variant's fields in declaration order. Either way, the C header still /// emits the discriminant constants ([`EnumVariantBinding::c_const`]) so C /// consumers can switch on the value or tag. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct EnumBinding { /// The enum name as written in the IDL. pub name: String, @@ -210,7 +207,7 @@ impl EnumBinding { /// A single enum variant with its precomputed C constant name and any /// associated data. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct EnumVariantBinding { /// The variant name as written in the IDL. pub name: String, @@ -284,7 +281,7 @@ pub struct InterfaceBinding { } /// One error code of a module's error domain, with its C constant name. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct ErrorCodeBinding { /// The code name exactly as written in the IDL (e.g. `KEY_NOT_FOUND`). pub name: String, @@ -310,7 +307,7 @@ pub struct ErrorCodeBinding { /// Backends emit one error type per *declaring* module /// ([`declared_here`](Self::declared_here) is `true`) and reference the /// ancestor's type from inheriting submodules. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct ErrorBinding { /// The domain name as written in the IDL (e.g. `KvError`). pub name: String, @@ -331,7 +328,7 @@ pub struct ErrorBinding { } /// One module, flattened with its underscore-joined symbol path. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct ModuleBinding { /// The module name (its final path segment). pub name: String, @@ -397,7 +394,7 @@ impl ModuleBinding { } /// The whole API, normalized and lowered for code generation. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct BindingModel { /// The C symbol prefix every emitted name is built from. pub prefix: String, @@ -408,11 +405,11 @@ pub struct BindingModel { } impl BindingModel { - /// Build the model from a validated [`Api`], using `prefix` for every C + /// 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: &Api, prefix: &str) -> Self { + pub fn build(api: &ResolvedApi, prefix: &str) -> Self { let mut modules = Vec::new(); for m in &api.modules { lower_module(m, &[], prefix, None, &mut modules); @@ -534,7 +531,6 @@ fn lower_field(f: &weaveffi_ir::ir::StructField) -> FieldBinding { name: f.name.clone(), doc: f.doc.clone(), ty: f.ty.clone(), - default: f.default.clone(), } } @@ -796,7 +792,7 @@ pub fn iterator_item_ctype(elem: &TypeRef, module: &str) -> CType { mod tests { use super::*; use weaveffi_ir::ir::{ - CallbackDef, EnumDef, EnumVariant, Function, ListenerDef, Module, Param, StructDef, + Api, CallbackDef, EnumDef, EnumVariant, Function, ListenerDef, Module, Param, StructDef, StructField, }; @@ -837,13 +833,13 @@ mod tests { } } - fn api(modules: Vec) -> Api { - Api { - version: "0.6.0".into(), + fn api(modules: Vec) -> ResolvedApi { + ResolvedApi::assume_resolved(Api { + version: weaveffi_ir::ir::CURRENT_SCHEMA_VERSION.into(), modules, generators: None, package: None, - } + }) } #[test] @@ -969,13 +965,11 @@ mod tests { name: "name".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }, StructField { name: "age".into(), ty: TypeRef::I32, doc: None, - default: None, }, ], }], diff --git a/crates/weaveffi-core/src/pkg.rs b/crates/weaveffi-core/src/pkg.rs index 16e6e0f4..8df17163 100644 --- a/crates/weaveffi-core/src/pkg.rs +++ b/crates/weaveffi-core/src/pkg.rs @@ -178,7 +178,7 @@ mod tests { fn api_with(pkg: Option) -> Api { Api { - version: "0.6.0".into(), + version: "0.7.0".into(), package: pkg, modules: vec![], generators: None, diff --git a/crates/weaveffi-core/src/plan.rs b/crates/weaveffi-core/src/plan.rs index 4c33df0f..bf8c39be 100644 --- a/crates/weaveffi-core/src/plan.rs +++ b/crates/weaveffi-core/src/plan.rs @@ -19,8 +19,8 @@ //! including the requirement that wrappers stay **lazy** (one producer //! `next` per consumer step, never a hidden drain into a list). //! * **Async** ([`AsyncProtocol`]): the completion-callback contract, -//! including the rule that result buffers are borrowed for the callback's -//! duration and must be copied before it returns. +//! including the rule that results and errors are owned by the consumer +//! and released through the runtime free symbols. //! //! A backend that renders these plans in its own syntax cannot drift from the //! others on semantics; only the spelling differs. @@ -384,12 +384,14 @@ impl IteratorBinding { /// wrapper resolves its native future idiom (a Python `asyncio` future, a /// JS `Promise`, a Swift continuation, a C# `TaskCompletionSource`, a Go /// channel) exactly once and then releases the registration. -/// 2. **Borrowed results.** Result buffers passed to the callback (strings, -/// bytes, and buffered values) are owned by the producer and valid **only -/// for the callback's duration**; the wrapper must deep-copy or decode -/// them before the callback returns and must not free them. -/// Interface-object results are the exception: the callback receives -/// ownership and adopts the pointer. +/// 2. **Owned results.** Everything passed to the callback is owned by the +/// consumer. String results are released with `{prefix}_free_string`, +/// byte and buffered-value results with `{prefix}_free_bytes`, and +/// interface-object results transfer ownership of the object (the +/// wrapper adopts the pointer). This is what lets runtimes that defer +/// callback bodies past the native return (Dart's +/// `NativeCallable.listener`, for example) decode safely; a wrapper that +/// processes results inline still copies or decodes first and then frees. /// 3. **Foreign-thread delivery.** The callback runs on a producer thread, /// so the wrapper must hop back to its native scheduler before touching /// consumer state (`call_soon_threadsafe`, a threadsafe function, a @@ -397,11 +399,9 @@ impl IteratorBinding { /// target's runtime forbids it. /// /// The callback's `err` slot follows the owning function's [`ErrorStrategy`]. -/// The error struct itself is producer-owned and borrowed for the callback's -/// duration: the wrapper copies the code, message, and payload inside the -/// callback and the producer releases them afterward. A wrapper may also call -/// `error_clear` itself; the clear is idempotent (it nulls the owned -/// pointers), so the producer's own release stays safe. +/// A non-null error is heap-boxed and owned by the consumer: the wrapper +/// copies the code, message, and payload, then releases the box with +/// `{prefix}_error_free` exactly once. #[derive(Debug, Clone, PartialEq, Eq)] pub struct AsyncProtocol<'a> { /// The lowered async surface: launcher and callback typedef. @@ -410,7 +410,8 @@ pub struct AsyncProtocol<'a> { /// `callback`/`context`. pub cancellable: bool, /// The release owed for an *owned interface* result adopted by the - /// callback; [`ReturnFree::None`] for borrowed (copy-only) results. + /// callback; [`ReturnFree::None`] for results copied or decoded and + /// then released through the runtime free symbols. pub result_adopt: ReturnFree, /// How the callback's `err` slot is interpreted. pub error: ErrorStrategy, @@ -422,7 +423,7 @@ impl AsyncBinding { /// /// A direct or optional interface result (where an optional's null slot /// simply means none) is adopted by the callback; every other result - /// shape is borrowed and copied or decoded. + /// shape is copied or decoded, then freed through the runtime symbols. pub fn protocol<'a>(&'a self, f: &FnBinding, module: &str, prefix: &str) -> AsyncProtocol<'a> { fn adoptable(ty: &TypeRef) -> Option<&TypeRef> { match ty { diff --git a/crates/weaveffi-core/src/resolved.rs b/crates/weaveffi-core/src/resolved.rs new file mode 100644 index 00000000..051a7d1e --- /dev/null +++ b/crates/weaveffi-core/src/resolved.rs @@ -0,0 +1,86 @@ +//! The resolved IR: a proof-carrying wrapper around a validated [`Api`]. +//! +//! WeaveFFI's pipeline has two distinct representations of an API that used +//! to share one mutable type: +//! +//! * the **IDL document**: the [`Api`] tree exactly as parsed from +//! YAML/JSON/TOML (or extracted from annotated Rust), in which every +//! user-type reference is still an unresolved [`TypeRef::Named`]; and +//! * the **resolved IR**: the same tree after [`validate_api`] has checked +//! every rule and rewritten each `Named` reference into its resolved kind +//! ([`TypeRef::Record`], [`TypeRef::RichEnum`], [`TypeRef::Enum`], or +//! [`TypeRef::Interface`]), qualifying cross-module references. +//! +//! [`ResolvedApi`] is the second representation, made unforgeable: the only +//! checked way to obtain one is [`validate_api`], and the wrapper exposes no +//! mutable access, so a generator holding a `&ResolvedApi` can rely on the +//! post-resolution invariants (no `Named` reference remains anywhere in the +//! tree) instead of documenting them as a convention. +//! +//! Everything downstream of validation consumes this type: the +//! [`BindingModel`](crate::model::BindingModel), the +//! [`Generator`](crate::codegen::Generator) and +//! [`LanguageBackend`](crate::backend::LanguageBackend) traits, and the +//! [`Orchestrator`](crate::codegen::Orchestrator). Code that manipulates the +//! *document* (parsing, `weaveffi format`, `weaveffi extract`) keeps working +//! with the plain [`Api`]. +//! +//! [`validate_api`]: crate::validate::validate_api +//! [`TypeRef::Named`]: weaveffi_ir::ir::TypeRef::Named +//! [`TypeRef::Record`]: weaveffi_ir::ir::TypeRef::Record +//! [`TypeRef::RichEnum`]: weaveffi_ir::ir::TypeRef::RichEnum +//! [`TypeRef::Enum`]: weaveffi_ir::ir::TypeRef::Enum +//! [`TypeRef::Interface`]: weaveffi_ir::ir::TypeRef::Interface + +use weaveffi_ir::ir::Api; + +/// A validated API whose type references have all been resolved. +/// +/// This is the input every generator consumes. It cannot be constructed +/// from an unchecked document: obtain one through +/// [`validate_api`](crate::validate::validate_api), or through +/// [`assume_resolved`](Self::assume_resolved) when resolution has provably +/// happened by other means (the producer macro's expansion path, or tests +/// that build already-resolved trees by hand). +/// +/// `ResolvedApi` dereferences to [`Api`] for read access; there is no +/// mutable access, so the resolution invariants cannot be broken after +/// construction. +#[derive(Debug, Clone, PartialEq)] +pub struct ResolvedApi { + api: Api, +} + +impl ResolvedApi { + /// Wrap an [`Api`] the caller asserts is already validated and resolved. + /// + /// This is the escape hatch for the two construction sites that cannot + /// route through [`validate_api`](crate::validate::validate_api): the + /// `#[weaveffi::module]` proc-macro (which resolves the single module it + /// expands) and tests that build pre-resolved trees directly. The caller + /// asserts that no [`TypeRef::Named`](weaveffi_ir::ir::TypeRef::Named) + /// reference remains anywhere in `api`; downstream lowering treats a + /// surviving one as a bug and panics on it. + pub fn assume_resolved(api: Api) -> Self { + Self { api } + } + + /// Borrow the underlying resolved [`Api`] tree. + pub fn api(&self) -> &Api { + &self.api + } +} + +impl std::ops::Deref for ResolvedApi { + type Target = Api; + + fn deref(&self) -> &Api { + &self.api + } +} + +impl AsRef for ResolvedApi { + fn as_ref(&self) -> &Api { + &self.api + } +} diff --git a/crates/weaveffi-core/src/utils.rs b/crates/weaveffi-core/src/utils.rs index 240c0237..1a5c27a1 100644 --- a/crates/weaveffi-core/src/utils.rs +++ b/crates/weaveffi-core/src/utils.rs @@ -1,22 +1,11 @@ -//! Shared text helpers for the generators: C symbol naming, the standard -//! generated-file prelude/trailer, and cross-module type-name resolution. +//! Shared text helpers for the generators: the standard generated-file +//! prelude/trailer and cross-module type-name resolution. //! //! These are the small string-building routines every backend reaches for //! when stamping the "generated by WeaveFFI" banner onto a file or turning a //! qualified `module.Type` reference into its flattened C symbol. Centralizing -//! them keeps the banner, the symbol-prefixing rule, and the dotted-path -//! flattening identical across all of the language generators. - -/// Build the C symbol name for a function: `__`. -/// -/// `prefix` is the configured ABI symbol prefix (default `"weaveffi"`). -/// Every backend must route user-symbol construction through this (or the -/// equivalent [`crate::model::BindingModel`] fields) so a non-default -/// `c_prefix` is honored consistently across all eleven languages, not just -/// the C and C++ headers. -pub fn c_symbol_name(prefix: &str, module: &str, func: &str) -> String { - format!("{prefix}_{module}_{func}") -} +//! them keeps the banner and the dotted-path flattening identical across all +//! of the language generators. /// Comment syntax used to emit the standard prelude/trailer in generated files. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -173,15 +162,6 @@ pub fn c_abi_struct_name(name: &str, current_module: &str, prefix: &str) -> Stri mod tests { use super::*; - #[test] - fn c_symbol_name_uses_prefix() { - assert_eq!( - c_symbol_name("weaveffi", "calc", "add"), - "weaveffi_calc_add" - ); - assert_eq!(c_symbol_name("myffi", "calc", "add"), "myffi_calc_add"); - } - #[test] fn local_type_name_unqualified() { assert_eq!(local_type_name("Contact"), "Contact"); diff --git a/crates/weaveffi-core/src/validate.rs b/crates/weaveffi-core/src/validate.rs index 4fe03805..376c7035 100644 --- a/crates/weaveffi-core/src/validate.rs +++ b/crates/weaveffi-core/src/validate.rs @@ -11,6 +11,8 @@ use miette::Diagnostic; use std::collections::BTreeSet; use weaveffi_ir::ir::{Api, SUPPORTED_VERSIONS}; +use crate::resolved::ResolvedApi; + mod diagnostic; mod resolve; mod rules; @@ -525,14 +527,17 @@ impl Diagnostic for ValidationDiagnostics { } } -/// Validate an [`Api`], reporting **every** rule violation found. The optional -/// `source` is `(filename, contents)` of the IDL file and is used to attach -/// spans to the returned diagnostics. Pass `None` when the API is constructed -/// in memory (tests, programmatic builds) and there is no on-disk source. +/// Validate an [`Api`] document, reporting **every** rule violation found. +/// The optional `source` is `(filename, contents)` of the IDL file and is +/// used to attach spans to the returned diagnostics. Pass `None` when the +/// API is constructed in memory (tests, programmatic builds) and there is no +/// on-disk source. /// -/// On success, type references in `api` are resolved in place (see -/// [`resolve_type_refs`]): enum and interface references are distinguished -/// from struct references, and cross-module references are qualified. +/// This is the one checked way to turn a parsed document into the +/// [`ResolvedApi`] every generator consumes. On success, type references are +/// resolved (see [`resolve_type_refs`]): enum and interface references are +/// distinguished from struct references, and cross-module references are +/// qualified with their owning module's path. /// /// # Errors /// @@ -541,12 +546,12 @@ impl Diagnostic for ValidationDiagnostics { /// an unknown or misplaced type, an empty struct or enum, a `throws` without /// an error domain, or any other rule violation in the catalog above. pub fn validate_api( - api: &mut Api, + mut api: Api, source: Option<(&str, &str)>, -) -> Result<(), ValidationDiagnostics> { - let errors = validate_api_inner(api); +) -> Result { + let errors = validate_api_inner(&mut api); if errors.is_empty() { - return Ok(()); + return Ok(ResolvedApi::assume_resolved(api)); } Err(ValidationDiagnostics { diagnostics: errors diff --git a/crates/weaveffi-core/src/validate/tests.rs b/crates/weaveffi-core/src/validate/tests.rs index ca7234ee..520df909 100644 --- a/crates/weaveffi-core/src/validate/tests.rs +++ b/crates/weaveffi-core/src/validate/tests.rs @@ -42,7 +42,7 @@ fn simple_module(name: &str) -> Module { fn simple_api() -> Api { Api { - version: "0.6.0".to_string(), + version: "0.7.0".to_string(), modules: vec![simple_module("mymod")], generators: None, package: None, @@ -51,28 +51,28 @@ fn simple_api() -> Api { #[test] fn valid_api_passes() { - let mut api = simple_api(); - assert!(validate_api(&mut api, None).is_ok()); + let api = simple_api(); + assert!(validate_api(api, None).is_ok()); } #[test] fn duplicate_module_names_rejected() { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![simple_module("dup"), simple_module("dup")], generators: None, package: None, }; assert!(matches!( - &validate_api(&mut api, None).unwrap_err().first().error, + &validate_api(api, None).unwrap_err().first().error, ValidationError::DuplicateModuleName(n) if n == "dup" )); } #[test] fn duplicate_function_names_rejected() { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "mymod".to_string(), functions: vec![simple_function("same"), simple_function("same")], @@ -88,7 +88,7 @@ fn duplicate_function_names_rejected() { package: None, }; assert!(matches!( - &validate_api(&mut api, None).unwrap_err().first().error, + &validate_api(api, None).unwrap_err().first().error, ValidationError::DuplicateFunctionName { .. } )); } @@ -96,8 +96,8 @@ fn duplicate_function_names_rejected() { #[test] fn reserved_keywords_rejected() { for kw in ["type", "async"] { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: kw.to_string(), functions: vec![simple_function("ok_fn")], @@ -113,7 +113,7 @@ fn reserved_keywords_rejected() { package: None, }; assert!( - validate_api(&mut api, None).is_err(), + validate_api(api, None).is_err(), "Expected reserved keyword '{kw}' to be rejected" ); } @@ -122,8 +122,8 @@ fn reserved_keywords_rejected() { #[test] fn invalid_identifiers_rejected() { for bad in ["123", "has spaces", ""] { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: bad.to_string(), functions: vec![simple_function("ok_fn")], @@ -139,7 +139,7 @@ fn invalid_identifiers_rejected() { package: None, }; assert!( - validate_api(&mut api, None).is_err(), + validate_api(api, None).is_err(), "Expected invalid identifier '{bad}' to be rejected" ); } @@ -147,8 +147,8 @@ fn invalid_identifiers_rejected() { #[test] fn async_function_passes_validation() { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "mymod".to_string(), functions: vec![Function { @@ -173,13 +173,13 @@ fn async_function_passes_validation() { generators: None, package: None, }; - assert!(validate_api(&mut api, None).is_ok()); + assert!(validate_api(api, None).is_ok()); } #[test] fn async_function_with_return_passes() { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "mymod".to_string(), functions: vec![Function { @@ -209,13 +209,13 @@ fn async_function_with_return_passes() { generators: None, package: None, }; - assert!(validate_api(&mut api, None).is_ok()); + assert!(validate_api(api, None).is_ok()); } #[test] fn async_void_function_emits_warning() { let api = Api { - version: "0.6.0".to_string(), + version: "0.7.0".to_string(), modules: vec![Module { name: "mymod".to_string(), functions: vec![Function { @@ -251,7 +251,7 @@ fn async_void_function_emits_warning() { #[test] fn async_function_with_return_no_void_warning() { let api = Api { - version: "0.6.0".to_string(), + version: "0.7.0".to_string(), modules: vec![Module { name: "mymod".to_string(), functions: vec![Function { @@ -284,8 +284,8 @@ fn async_function_with_return_no_void_warning() { #[test] fn empty_module_name_rejected() { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "".to_string(), functions: vec![simple_function("ok_fn")], @@ -301,15 +301,15 @@ fn empty_module_name_rejected() { package: None, }; assert!(matches!( - &validate_api(&mut api, None).unwrap_err().first().error, + &validate_api(api, None).unwrap_err().first().error, ValidationError::NoModuleName )); } #[test] fn doc_example_error_domain_validates() { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "contacts".to_string(), functions: vec![ @@ -390,13 +390,13 @@ fn doc_example_error_domain_validates() { generators: None, package: None, }; - assert!(validate_api(&mut api, None).is_ok()); + assert!(validate_api(api, None).is_ok()); } #[test] fn error_code_zero_rejected() { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "mymod".to_string(), functions: vec![simple_function("ok_fn")], @@ -421,7 +421,7 @@ fn error_code_zero_rejected() { package: None, }; assert!(matches!( - &validate_api(&mut api, None).unwrap_err().first().error, + &validate_api(api, None).unwrap_err().first().error, ValidationError::InvalidErrorCode { module, name } if module == "mymod" && name == "success" )); @@ -429,8 +429,8 @@ fn error_code_zero_rejected() { #[test] fn error_domain_name_collision_rejected() { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "mymod".to_string(), functions: vec![simple_function("do_stuff")], @@ -455,7 +455,7 @@ fn error_domain_name_collision_rejected() { package: None, }; assert!(matches!( - &validate_api(&mut api, None).unwrap_err().first().error, + &validate_api(api, None).unwrap_err().first().error, ValidationError::NameCollisionWithErrorDomain { module, name } if module == "mymod" && name == "do_stuff" )); @@ -463,8 +463,8 @@ fn error_domain_name_collision_rejected() { #[test] fn duplicate_error_names_rejected() { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "mymod".to_string(), functions: vec![simple_function("ok_fn")], @@ -498,7 +498,7 @@ fn duplicate_error_names_rejected() { package: None, }; assert!(matches!( - &validate_api(&mut api, None).unwrap_err().first().error, + &validate_api(api, None).unwrap_err().first().error, ValidationError::DuplicateErrorName { module, name } if module == "mymod" && name == "fail" )); @@ -506,8 +506,8 @@ fn duplicate_error_names_rejected() { #[test] fn duplicate_error_codes_rejected() { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "mymod".to_string(), functions: vec![simple_function("ok_fn")], @@ -541,7 +541,7 @@ fn duplicate_error_codes_rejected() { package: None, }; assert!(matches!( - &validate_api(&mut api, None).unwrap_err().first().error, + &validate_api(api, None).unwrap_err().first().error, ValidationError::DuplicateErrorCode { .. } )); } @@ -571,8 +571,8 @@ fn duplicate_error_code_names_across_domains_rejected() { errors, modules: vec![], }; - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![ module("products", domain("ProductsError", "NotFound")), module("orders", domain("OrdersError", "NotFound")), @@ -581,7 +581,7 @@ fn duplicate_error_code_names_across_domains_rejected() { package: None, }; assert!(matches!( - &validate_api(&mut api, None).unwrap_err().first().error, + &validate_api(api, None).unwrap_err().first().error, ValidationError::DuplicateErrorCodeName { name, first, second } if name == "NotFound" && first == "products.ProductsError" @@ -589,8 +589,8 @@ fn duplicate_error_code_names_across_domains_rejected() { )); // Distinct code names across domains stay valid. - let mut ok = Api { - version: "0.6.0".to_string(), + let ok = Api { + version: "0.7.0".to_string(), modules: vec![ module("products", domain("ProductsError", "ProductNotFound")), module("orders", domain("OrdersError", "OrderNotFound")), @@ -598,7 +598,7 @@ fn duplicate_error_code_names_across_domains_rejected() { generators: None, package: None, }; - assert!(validate_api(&mut ok, None).is_ok()); + assert!(validate_api(ok, None).is_ok()); } fn simple_struct(name: &str) -> StructDef { @@ -609,15 +609,14 @@ fn simple_struct(name: &str) -> StructDef { name: "x".to_string(), ty: TypeRef::I32, doc: None, - default: None, }], } } #[test] fn duplicate_struct_names_rejected() { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "mymod".to_string(), functions: vec![simple_function("ok_fn")], @@ -633,7 +632,7 @@ fn duplicate_struct_names_rejected() { package: None, }; assert!(matches!( - &validate_api(&mut api, None).unwrap_err().first().error, + &validate_api(api, None).unwrap_err().first().error, ValidationError::DuplicateStructName { module, name } if module == "mymod" && name == "Point" )); @@ -641,8 +640,8 @@ fn duplicate_struct_names_rejected() { #[test] fn empty_struct_rejected() { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "mymod".to_string(), functions: vec![simple_function("ok_fn")], @@ -662,7 +661,7 @@ fn empty_struct_rejected() { package: None, }; assert!(matches!( - &validate_api(&mut api, None).unwrap_err().first().error, + &validate_api(api, None).unwrap_err().first().error, ValidationError::EmptyStruct { module, name } if module == "mymod" && name == "Empty" )); @@ -670,8 +669,8 @@ fn empty_struct_rejected() { #[test] fn duplicate_struct_field_names_rejected() { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "mymod".to_string(), functions: vec![simple_function("ok_fn")], @@ -684,13 +683,11 @@ fn duplicate_struct_field_names_rejected() { name: "x".to_string(), ty: TypeRef::I32, doc: None, - default: None, }, StructField { name: "x".to_string(), ty: TypeRef::F64, doc: None, - default: None, }, ], }], @@ -704,7 +701,7 @@ fn duplicate_struct_field_names_rejected() { package: None, }; assert!(matches!( - &validate_api(&mut api, None).unwrap_err().first().error, + &validate_api(api, None).unwrap_err().first().error, ValidationError::DuplicateStructField { struct_name, field } if struct_name == "Point" && field == "x" )); @@ -733,8 +730,8 @@ fn simple_enum(name: &str) -> EnumDef { #[test] fn duplicate_enum_names_rejected() { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "mymod".to_string(), functions: vec![simple_function("ok_fn")], @@ -750,7 +747,7 @@ fn duplicate_enum_names_rejected() { package: None, }; assert!(matches!( - &validate_api(&mut api, None).unwrap_err().first().error, + &validate_api(api, None).unwrap_err().first().error, ValidationError::DuplicateEnumName { module, name } if module == "mymod" && name == "Color" )); @@ -758,8 +755,8 @@ fn duplicate_enum_names_rejected() { #[test] fn empty_enum_rejected() { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "mymod".to_string(), functions: vec![simple_function("ok_fn")], @@ -779,7 +776,7 @@ fn empty_enum_rejected() { package: None, }; assert!(matches!( - &validate_api(&mut api, None).unwrap_err().first().error, + &validate_api(api, None).unwrap_err().first().error, ValidationError::EmptyEnum { module, name } if module == "mymod" && name == "Empty" )); @@ -787,8 +784,8 @@ fn empty_enum_rejected() { #[test] fn duplicate_enum_variant_rejected() { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "mymod".to_string(), functions: vec![simple_function("ok_fn")], @@ -821,7 +818,7 @@ fn duplicate_enum_variant_rejected() { package: None, }; assert!(matches!( - &validate_api(&mut api, None).unwrap_err().first().error, + &validate_api(api, None).unwrap_err().first().error, ValidationError::DuplicateEnumVariant { enum_name, variant } if enum_name == "Color" && variant == "Red" )); @@ -829,8 +826,8 @@ fn duplicate_enum_variant_rejected() { #[test] fn duplicate_enum_value_rejected() { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "mymod".to_string(), functions: vec![simple_function("ok_fn")], @@ -863,7 +860,7 @@ fn duplicate_enum_value_rejected() { package: None, }; assert!(matches!( - &validate_api(&mut api, None).unwrap_err().first().error, + &validate_api(api, None).unwrap_err().first().error, ValidationError::DuplicateEnumValue { enum_name, value } if enum_name == "Color" && *value == 0 )); @@ -871,8 +868,8 @@ fn duplicate_enum_value_rejected() { #[test] fn unknown_type_ref_rejected() { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "mymod".to_string(), functions: vec![Function { @@ -903,15 +900,15 @@ fn unknown_type_ref_rejected() { package: None, }; assert!(matches!( - &validate_api(&mut api, None).unwrap_err().first().error, + &validate_api(api, None).unwrap_err().first().error, ValidationError::UnknownTypeRef { name } if name == "Foo" )); } #[test] fn valid_struct_ref_passes() { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "mymod".to_string(), functions: vec![Function { @@ -941,13 +938,13 @@ fn valid_struct_ref_passes() { generators: None, package: None, }; - assert!(validate_api(&mut api, None).is_ok()); + assert!(validate_api(api, None).is_ok()); } #[test] fn unknown_type_ref_in_optional_rejected() { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "mymod".to_string(), functions: vec![Function { @@ -978,15 +975,15 @@ fn unknown_type_ref_in_optional_rejected() { package: None, }; assert!(matches!( - &validate_api(&mut api, None).unwrap_err().first().error, + &validate_api(api, None).unwrap_err().first().error, ValidationError::UnknownTypeRef { name } if name == "Bar" )); } #[test] fn unknown_type_ref_in_list_rejected() { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "mymod".to_string(), functions: vec![Function { @@ -1012,15 +1009,15 @@ fn unknown_type_ref_in_list_rejected() { package: None, }; assert!(matches!( - &validate_api(&mut api, None).unwrap_err().first().error, + &validate_api(api, None).unwrap_err().first().error, ValidationError::UnknownTypeRef { name } if name == "Baz" )); } #[test] fn struct_field_referencing_unknown_type() { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "mymod".to_string(), functions: vec![simple_function("ok_fn")], @@ -1032,7 +1029,6 @@ fn struct_field_referencing_unknown_type() { name: "inner".to_string(), ty: TypeRef::Named("Nonexistent".to_string()), doc: None, - default: None, }], }], enums: vec![], @@ -1045,15 +1041,15 @@ fn struct_field_referencing_unknown_type() { package: None, }; assert!(matches!( - &validate_api(&mut api, None).unwrap_err().first().error, + &validate_api(api, None).unwrap_err().first().error, ValidationError::UnknownTypeRef { name } if name == "Nonexistent" )); } #[test] fn function_param_with_optional_struct() { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "mymod".to_string(), functions: vec![Function { @@ -1080,7 +1076,6 @@ fn function_param_with_optional_struct() { name: "name".to_string(), ty: TypeRef::StringUtf8, doc: None, - default: None, }], }], enums: vec![], @@ -1092,13 +1087,13 @@ fn function_param_with_optional_struct() { generators: None, package: None, }; - assert!(validate_api(&mut api, None).is_ok()); + assert!(validate_api(api, None).is_ok()); } #[test] fn function_param_with_list_of_enums() { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "mymod".to_string(), functions: vec![Function { @@ -1128,13 +1123,13 @@ fn function_param_with_list_of_enums() { generators: None, package: None, }; - assert!(validate_api(&mut api, None).is_ok()); + assert!(validate_api(api, None).is_ok()); } #[test] fn nested_optional_list_validates() { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "mymod".to_string(), functions: vec![Function { @@ -1158,7 +1153,6 @@ fn nested_optional_list_validates() { name: "name".to_string(), ty: TypeRef::StringUtf8, doc: None, - default: None, }], }], enums: vec![], @@ -1170,13 +1164,13 @@ fn nested_optional_list_validates() { generators: None, package: None, }; - assert!(validate_api(&mut api, None).is_ok()); + assert!(validate_api(api, None).is_ok()); } #[test] fn list_of_list_param_rejected() { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "mymod".to_string(), functions: vec![Function { @@ -1207,13 +1201,13 @@ fn list_of_list_param_rejected() { package: None, }; // Nested lists serialize recursively in the value-buffer format. - validate_api(&mut api, None).expect("nested lists are representable as buffers"); + validate_api(api, None).expect("nested lists are representable as buffers"); } #[test] fn list_of_optional_scalar_return_rejected() { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "mymod".to_string(), functions: vec![Function { @@ -1242,13 +1236,13 @@ fn list_of_optional_scalar_return_rejected() { }; // The buffer format carries per-element presence flags, so a list of // optional scalars round-trips. - validate_api(&mut api, None).expect("optional scalars in lists are representable as buffers"); + validate_api(api, None).expect("optional scalars in lists are representable as buffers"); } #[test] fn map_of_struct_value_field_rejected() { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "mymod".to_string(), functions: vec![simple_function("ok_fn")], @@ -1263,7 +1257,6 @@ fn map_of_struct_value_field_rejected() { Box::new(TypeRef::Named("Widget".to_string())), ), doc: None, - default: None, }], }], enums: vec![], @@ -1277,13 +1270,13 @@ fn map_of_struct_value_field_rejected() { }; // Records nest recursively in the value-buffer format, including as map // values inside another record's field. - validate_api(&mut api, None).expect("maps of records are representable as buffers"); + validate_api(api, None).expect("maps of records are representable as buffers"); } #[test] fn enum_variant_value_zero_allowed() { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "mymod".to_string(), functions: vec![simple_function("ok_fn")], @@ -1315,13 +1308,13 @@ fn enum_variant_value_zero_allowed() { generators: None, package: None, }; - assert!(validate_api(&mut api, None).is_ok()); + assert!(validate_api(api, None).is_ok()); } #[test] fn valid_enum_ref_passes() { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "mymod".to_string(), functions: vec![Function { @@ -1346,13 +1339,13 @@ fn valid_enum_ref_passes() { generators: None, package: None, }; - assert!(validate_api(&mut api, None).is_ok()); + assert!(validate_api(api, None).is_ok()); } #[test] fn resolve_enum_ref_in_function_param() { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "mymod".to_string(), functions: vec![Function { @@ -1382,7 +1375,7 @@ fn resolve_enum_ref_in_function_param() { generators: None, package: None, }; - validate_api(&mut api, None).unwrap(); + let api = validate_api(api, None).unwrap(); assert_eq!( api.modules[0].functions[0].params[0].ty, TypeRef::Enum("Color".to_string()) @@ -1391,8 +1384,8 @@ fn resolve_enum_ref_in_function_param() { #[test] fn resolve_enum_ref_in_optional() { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "mymod".to_string(), functions: vec![Function { @@ -1422,7 +1415,7 @@ fn resolve_enum_ref_in_optional() { generators: None, package: None, }; - validate_api(&mut api, None).unwrap(); + let api = validate_api(api, None).unwrap(); assert_eq!( api.modules[0].functions[0].params[0].ty, TypeRef::Optional(Box::new(TypeRef::Enum("Color".to_string()))) @@ -1431,8 +1424,8 @@ fn resolve_enum_ref_in_optional() { #[test] fn struct_ref_not_changed() { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "mymod".to_string(), functions: vec![Function { @@ -1462,7 +1455,7 @@ fn struct_ref_not_changed() { generators: None, package: None, }; - validate_api(&mut api, None).unwrap(); + let api = validate_api(api, None).unwrap(); assert_eq!( api.modules[0].functions[0].params[0].ty, TypeRef::Record("Contact".to_string()) @@ -1471,8 +1464,8 @@ fn struct_ref_not_changed() { #[test] fn map_with_string_key_passes() { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "mymod".to_string(), functions: vec![Function { @@ -1500,13 +1493,13 @@ fn map_with_string_key_passes() { generators: None, package: None, }; - assert!(validate_api(&mut api, None).is_ok()); + assert!(validate_api(api, None).is_ok()); } #[test] fn map_with_struct_key_rejected() { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "mymod".to_string(), functions: vec![Function { @@ -1535,15 +1528,15 @@ fn map_with_struct_key_rejected() { package: None, }; assert!(matches!( - &validate_api(&mut api, None).unwrap_err().first().error, + &validate_api(api, None).unwrap_err().first().error, ValidationError::InvalidMapKey { key_type } if key_type == "struct Point" )); } #[test] fn map_with_enum_key_passes() { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "mymod".to_string(), functions: vec![Function { @@ -1571,7 +1564,7 @@ fn map_with_enum_key_passes() { generators: None, package: None, }; - assert!(validate_api(&mut api, None).is_ok()); + assert!(validate_api(api, None).is_ok()); } #[test] @@ -1585,7 +1578,7 @@ fn warning_large_enum_variant_count() { }) .collect(); let api = Api { - version: "0.6.0".to_string(), + version: "0.7.0".to_string(), modules: vec![Module { name: "mymod".to_string(), functions: vec![simple_function("ok_fn")], @@ -1623,7 +1616,7 @@ fn warning_enum_at_100_no_warning() { }) .collect(); let api = Api { - version: "0.6.0".to_string(), + version: "0.7.0".to_string(), modules: vec![Module { name: "mymod".to_string(), functions: vec![simple_function("ok_fn")], @@ -1654,7 +1647,7 @@ fn warning_deep_nesting_in_param() { Box::new(TypeRef::List(Box::new(TypeRef::I32))), ))))); let api = Api { - version: "0.6.0".to_string(), + version: "0.7.0".to_string(), modules: vec![Module { name: "mymod".to_string(), functions: vec![Function { @@ -1698,7 +1691,7 @@ fn warning_nesting_at_3_no_warning() { Box::new(TypeRef::I32), ))))); let api = Api { - version: "0.6.0".to_string(), + version: "0.7.0".to_string(), modules: vec![Module { name: "mymod".to_string(), functions: vec![Function { @@ -1740,7 +1733,7 @@ fn warning_deep_nesting_in_struct_field() { Box::new(TypeRef::List(Box::new(TypeRef::I32))), ))))); let api = Api { - version: "0.6.0".to_string(), + version: "0.7.0".to_string(), modules: vec![Module { name: "mymod".to_string(), functions: vec![simple_function("ok_fn")], @@ -1752,7 +1745,6 @@ fn warning_deep_nesting_in_struct_field() { name: "data".to_string(), ty: deep, doc: None, - default: None, }], }], enums: vec![], @@ -1775,7 +1767,7 @@ fn warning_deep_nesting_in_struct_field() { #[test] fn warning_empty_module_doc() { let api = Api { - version: "0.6.0".to_string(), + version: "0.7.0".to_string(), modules: vec![Module { name: "undocumented".to_string(), functions: vec![ @@ -1823,7 +1815,7 @@ fn warning_empty_module_doc() { #[test] fn warning_partial_docs_no_warning() { let api = Api { - version: "0.6.0".to_string(), + version: "0.7.0".to_string(), modules: vec![Module { name: "partial".to_string(), functions: vec![ @@ -1870,7 +1862,7 @@ fn warning_partial_docs_no_warning() { #[test] fn warning_no_functions_no_empty_doc_warning() { let api = Api { - version: "0.6.0".to_string(), + version: "0.7.0".to_string(), modules: vec![Module { name: "empty".to_string(), functions: vec![], @@ -1894,7 +1886,7 @@ fn warning_no_functions_no_empty_doc_warning() { #[test] fn warning_clean_api_no_warnings() { let api = Api { - version: "0.6.0".to_string(), + version: "0.7.0".to_string(), modules: vec![Module { name: "clean".to_string(), functions: vec![Function { @@ -1930,8 +1922,8 @@ fn warning_clean_api_no_warnings() { #[test] fn resolve_enum_ref_in_struct_field() { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "mymod".to_string(), functions: vec![simple_function("ok_fn")], @@ -1943,7 +1935,6 @@ fn resolve_enum_ref_in_struct_field() { name: "color".to_string(), ty: TypeRef::Named("Color".to_string()), doc: None, - default: None, }], }], enums: vec![simple_enum("Color")], @@ -1955,7 +1946,7 @@ fn resolve_enum_ref_in_struct_field() { generators: None, package: None, }; - validate_api(&mut api, None).unwrap(); + let api = validate_api(api, None).unwrap(); assert_eq!( api.modules[0].structs[0].fields[0].ty, TypeRef::Enum("Color".to_string()) @@ -1964,8 +1955,8 @@ fn resolve_enum_ref_in_struct_field() { #[test] fn typed_handle_valid_struct_passes() { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "mymod".to_string(), functions: vec![Function { @@ -1995,13 +1986,13 @@ fn typed_handle_valid_struct_passes() { generators: None, package: None, }; - assert!(validate_api(&mut api, None).is_ok()); + assert!(validate_api(api, None).is_ok()); } #[test] fn typed_handle_unknown_struct_rejected() { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "mymod".to_string(), functions: vec![Function { @@ -2032,15 +2023,15 @@ fn typed_handle_unknown_struct_rejected() { package: None, }; assert!(matches!( - &validate_api(&mut api, None).unwrap_err().first().error, + &validate_api(api, None).unwrap_err().first().error, ValidationError::UnknownTypeRef { name } if name == "Nonexistent" )); } #[test] fn borrowed_str_param_accepted() { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "io".to_string(), functions: vec![Function { @@ -2070,13 +2061,13 @@ fn borrowed_str_param_accepted() { generators: None, package: None, }; - assert!(validate_api(&mut api, None).is_ok()); + assert!(validate_api(api, None).is_ok()); } #[test] fn borrowed_bytes_param_accepted() { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "io".to_string(), functions: vec![Function { @@ -2106,13 +2097,13 @@ fn borrowed_bytes_param_accepted() { generators: None, package: None, }; - assert!(validate_api(&mut api, None).is_ok()); + assert!(validate_api(api, None).is_ok()); } #[test] fn borrowed_str_in_return_rejected() { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "io".to_string(), functions: vec![Function { @@ -2138,7 +2129,7 @@ fn borrowed_str_in_return_rejected() { package: None, }; assert!(matches!( - &validate_api(&mut api, None).unwrap_err().first().error, + &validate_api(api, None).unwrap_err().first().error, ValidationError::BorrowedTypeInInvalidPosition { ty, location } if ty == "&str" && location.contains("return type") )); @@ -2146,8 +2137,8 @@ fn borrowed_str_in_return_rejected() { #[test] fn borrowed_bytes_in_return_rejected() { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "io".to_string(), functions: vec![Function { @@ -2173,7 +2164,7 @@ fn borrowed_bytes_in_return_rejected() { package: None, }; assert!(matches!( - &validate_api(&mut api, None).unwrap_err().first().error, + &validate_api(api, None).unwrap_err().first().error, ValidationError::BorrowedTypeInInvalidPosition { ty, location } if ty == "&[u8]" && location.contains("return type") )); @@ -2181,8 +2172,8 @@ fn borrowed_bytes_in_return_rejected() { #[test] fn borrowed_str_in_struct_field_rejected() { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "data".to_string(), functions: vec![], @@ -2193,7 +2184,6 @@ fn borrowed_str_in_struct_field_rejected() { name: "text".to_string(), ty: TypeRef::BorrowedStr, doc: None, - default: None, }], doc: None, }], @@ -2207,7 +2197,7 @@ fn borrowed_str_in_struct_field_rejected() { package: None, }; assert!(matches!( - &validate_api(&mut api, None).unwrap_err().first().error, + &validate_api(api, None).unwrap_err().first().error, ValidationError::BorrowedTypeInInvalidPosition { ty, location } if ty == "&str" && location.contains("struct") )); @@ -2215,8 +2205,8 @@ fn borrowed_str_in_struct_field_rejected() { #[test] fn borrowed_bytes_in_struct_field_rejected() { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "data".to_string(), functions: vec![], @@ -2227,7 +2217,6 @@ fn borrowed_bytes_in_struct_field_rejected() { name: "content".to_string(), ty: TypeRef::BorrowedBytes, doc: None, - default: None, }], doc: None, }], @@ -2241,7 +2230,7 @@ fn borrowed_bytes_in_struct_field_rejected() { package: None, }; assert!(matches!( - &validate_api(&mut api, None).unwrap_err().first().error, + &validate_api(api, None).unwrap_err().first().error, ValidationError::BorrowedTypeInInvalidPosition { ty, location } if ty == "&[u8]" && location.contains("struct") )); @@ -2249,8 +2238,8 @@ fn borrowed_bytes_in_struct_field_rejected() { #[test] fn borrowed_str_nested_in_optional_return_rejected() { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "io".to_string(), functions: vec![Function { @@ -2276,7 +2265,7 @@ fn borrowed_str_nested_in_optional_return_rejected() { package: None, }; assert!(matches!( - &validate_api(&mut api, None).unwrap_err().first().error, + &validate_api(api, None).unwrap_err().first().error, ValidationError::BorrowedTypeInInvalidPosition { ty, .. } if ty == "&str" )); @@ -2284,8 +2273,8 @@ fn borrowed_str_nested_in_optional_return_rejected() { #[test] fn cross_module_struct_ref_passes() { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![ Module { name: "orders".to_string(), @@ -2328,7 +2317,7 @@ fn cross_module_struct_ref_passes() { generators: None, package: None, }; - validate_api(&mut api, None).unwrap(); + let api = validate_api(api, None).unwrap(); assert_eq!( api.modules[0].functions[0].params[0].ty, TypeRef::Record("catalog.Product".to_string()) @@ -2337,8 +2326,8 @@ fn cross_module_struct_ref_passes() { #[test] fn cross_module_enum_ref_passes() { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![ Module { name: "orders".to_string(), @@ -2376,7 +2365,7 @@ fn cross_module_enum_ref_passes() { generators: None, package: None, }; - validate_api(&mut api, None).unwrap(); + let api = validate_api(api, None).unwrap(); assert_eq!( api.modules[0].functions[0].returns, Some(TypeRef::Enum("shared.Status".to_string())) @@ -2385,8 +2374,8 @@ fn cross_module_enum_ref_passes() { #[test] fn cross_module_unknown_still_rejected() { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![ Module { name: "orders".to_string(), @@ -2430,7 +2419,7 @@ fn cross_module_unknown_still_rejected() { package: None, }; assert!(matches!( - &validate_api(&mut api, None).unwrap_err().first().error, + &validate_api(api, None).unwrap_err().first().error, ValidationError::UnknownTypeRef { name } if name == "Nonexistent" )); } @@ -2438,7 +2427,7 @@ fn cross_module_unknown_still_rejected() { #[test] fn find_type_in_api_finds_struct() { let api = Api { - version: "0.6.0".to_string(), + version: "0.7.0".to_string(), modules: vec![Module { name: "catalog".to_string(), functions: vec![], @@ -2460,7 +2449,7 @@ fn find_type_in_api_finds_struct() { #[test] fn find_type_in_api_finds_enum() { let api = Api { - version: "0.6.0".to_string(), + version: "0.7.0".to_string(), modules: vec![Module { name: "shared".to_string(), functions: vec![], @@ -2482,7 +2471,7 @@ fn find_type_in_api_finds_enum() { #[test] fn find_type_in_api_returns_none_for_unknown() { let api = Api { - version: "0.6.0".to_string(), + version: "0.7.0".to_string(), modules: vec![simple_module("mymod")], generators: None, package: None, @@ -2492,8 +2481,8 @@ fn find_type_in_api_returns_none_for_unknown() { #[test] fn validate_nested_module_passes() { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "parent".to_string(), functions: vec![simple_function("top_fn")], @@ -2518,7 +2507,7 @@ fn validate_nested_module_passes() { generators: None, package: None, }; - assert!(validate_api(&mut api, None).is_ok()); + assert!(validate_api(api, None).is_ok()); } fn function_returning(name: &str, ret: TypeRef) -> Function { @@ -2538,7 +2527,7 @@ fn function_returning(name: &str, ret: TypeRef) -> Function { #[test] fn find_type_in_api_finds_nested_type() { let api = Api { - version: "0.6.0".to_string(), + version: "0.7.0".to_string(), modules: vec![Module { name: "graphics".to_string(), functions: vec![], @@ -2575,8 +2564,8 @@ fn resolve_qualifies_reference_to_nested_module_type() { // A top-level module references a struct defined in one of its own // nested submodules by bare name; resolution must qualify it to the // full dotted path so codegen can mangle the right C symbol. - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "app".to_string(), functions: vec![function_returning( @@ -2604,7 +2593,7 @@ fn resolve_qualifies_reference_to_nested_module_type() { generators: None, package: None, }; - validate_api(&mut api, None).unwrap(); + let api = validate_api(api, None).unwrap(); assert_eq!( api.modules[0].functions[0].returns, Some(TypeRef::Record("app.ui.Widget".to_string())) @@ -2616,8 +2605,8 @@ fn resolve_qualifies_nested_module_reference_to_parent_type() { // A nested module references a type owned by an ancestor module. Before // the nested-aware resolver, refs inside submodules were never // qualified at all; now they resolve to the owner's dotted path. - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "lib".to_string(), functions: vec![], @@ -2645,7 +2634,7 @@ fn resolve_qualifies_nested_module_reference_to_parent_type() { generators: None, package: None, }; - validate_api(&mut api, None).unwrap(); + let api = validate_api(api, None).unwrap(); assert_eq!( api.modules[0].modules[0].functions[0].returns, Some(TypeRef::Record("lib.Token".to_string())) @@ -2660,8 +2649,8 @@ fn resolve_qualifies_nested_module_typed_handle_to_parent_type() { // and every consumer (C ABI lowering + language wrappers) mis-attributed // it to the *referrer's* prefix (e.g. `weaveffi_kv_stats_Store` instead // of `weaveffi_kv_Store`), producing an undeclared type. - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "kv".to_string(), functions: vec![], @@ -2689,7 +2678,7 @@ fn resolve_qualifies_nested_module_typed_handle_to_parent_type() { generators: None, package: None, }; - validate_api(&mut api, None).unwrap(); + let api = validate_api(api, None).unwrap(); assert_eq!( api.modules[0].modules[0].functions[0].returns, Some(TypeRef::TypedHandle("kv.Store".to_string())) @@ -2700,8 +2689,8 @@ fn resolve_qualifies_nested_module_typed_handle_to_parent_type() { fn resolve_keeps_same_module_typed_handle_unqualified() { // A `handle` whose target is defined in the *same* module stays bare // so the lowering keeps using the current module's prefix. - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "sessions".to_string(), functions: vec![function_returning( @@ -2719,7 +2708,7 @@ fn resolve_keeps_same_module_typed_handle_unqualified() { generators: None, package: None, }; - validate_api(&mut api, None).unwrap(); + let api = validate_api(api, None).unwrap(); assert_eq!( api.modules[0].functions[0].returns, Some(TypeRef::TypedHandle("Session".to_string())) @@ -2731,8 +2720,8 @@ fn resolve_converts_nested_enum_reference_to_enum_variant() { // An unqualified reference to an enum that lives in another module must // be rewritten as `TypeRef::Enum` (not `Struct`) with the dotted path, // using the global index's `is_enum` flag. - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![ Module { name: "consumer".to_string(), @@ -2763,7 +2752,7 @@ fn resolve_converts_nested_enum_reference_to_enum_variant() { generators: None, package: None, }; - validate_api(&mut api, None).unwrap(); + let api = validate_api(api, None).unwrap(); assert_eq!( api.modules[0].functions[0].returns, Some(TypeRef::Enum("shared.Phase".to_string())) @@ -2772,8 +2761,8 @@ fn resolve_converts_nested_enum_reference_to_enum_variant() { #[test] fn duplicate_callback_names_rejected() { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "events".to_string(), functions: vec![], @@ -2800,7 +2789,7 @@ fn duplicate_callback_names_rejected() { package: None, }; assert!(matches!( - &validate_api(&mut api, None).unwrap_err().first().error, + &validate_api(api, None).unwrap_err().first().error, ValidationError::DuplicateCallbackName { module, name } if module == "events" && name == "on_data" )); @@ -2808,8 +2797,8 @@ fn duplicate_callback_names_rejected() { #[test] fn listener_referencing_undefined_callback_rejected() { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "events".to_string(), functions: vec![], @@ -2829,7 +2818,7 @@ fn listener_referencing_undefined_callback_rejected() { package: None, }; assert!(matches!( - &validate_api(&mut api, None).unwrap_err().first().error, + &validate_api(api, None).unwrap_err().first().error, ValidationError::ListenerCallbackNotFound { module, listener, callback } if module == "events" && listener == "watcher" && callback == "nonexistent" )); @@ -2837,8 +2826,8 @@ fn listener_referencing_undefined_callback_rejected() { #[test] fn listener_referencing_defined_callback_passes() { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "events".to_string(), functions: vec![], @@ -2866,13 +2855,13 @@ fn listener_referencing_defined_callback_passes() { generators: None, package: None, }; - assert!(validate_api(&mut api, None).is_ok()); + assert!(validate_api(api, None).is_ok()); } #[test] fn duplicate_listener_names_rejected() { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "events".to_string(), functions: vec![], @@ -2903,7 +2892,7 @@ fn duplicate_listener_names_rejected() { package: None, }; assert!(matches!( - &validate_api(&mut api, None).unwrap_err().first().error, + &validate_api(api, None).unwrap_err().first().error, ValidationError::DuplicateListenerName { module, name } if module == "events" && name == "watcher" )); @@ -2911,8 +2900,8 @@ fn duplicate_listener_names_rejected() { #[test] fn iterator_valid_as_return_type() { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "data".to_string(), functions: vec![Function { @@ -2937,13 +2926,13 @@ fn iterator_valid_as_return_type() { generators: None, package: None, }; - assert!(validate_api(&mut api, None).is_ok()); + assert!(validate_api(api, None).is_ok()); } #[test] fn iterator_rejected_as_param() { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "data".to_string(), functions: vec![Function { @@ -2974,15 +2963,15 @@ fn iterator_rejected_as_param() { package: None, }; assert!(matches!( - &validate_api(&mut api, None).unwrap_err().first().error, + &validate_api(api, None).unwrap_err().first().error, ValidationError::IteratorInInvalidPosition { .. } )); } #[test] fn iterator_rejected_in_struct_field() { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "data".to_string(), functions: vec![], @@ -2994,7 +2983,6 @@ fn iterator_rejected_in_struct_field() { name: "items".to_string(), ty: TypeRef::Iterator(Box::new(TypeRef::I32)), doc: None, - default: None, }], }], enums: vec![], @@ -3007,15 +2995,15 @@ fn iterator_rejected_in_struct_field() { package: None, }; assert!(matches!( - &validate_api(&mut api, None).unwrap_err().first().error, + &validate_api(api, None).unwrap_err().first().error, ValidationError::IteratorInInvalidPosition { .. } )); } #[test] fn mutable_buffered_param_is_error() { - let mut api = Api { - version: "0.6.0".to_string(), + let api = Api { + version: "0.7.0".to_string(), modules: vec![Module { name: "m".into(), functions: vec![Function { @@ -3045,7 +3033,7 @@ fn mutable_buffered_param_is_error() { generators: None, package: None, }; - let err = validate_api(&mut api, None).unwrap_err(); + let err = validate_api(api, None).unwrap_err(); assert!( matches!( err.first().error, @@ -3058,7 +3046,7 @@ fn mutable_buffered_param_is_error() { #[test] fn warning_mutable_on_value_type() { let api = Api { - version: "0.6.0".to_string(), + version: "0.7.0".to_string(), modules: vec![Module { name: "math".to_string(), functions: vec![Function { @@ -3101,7 +3089,7 @@ fn warning_mutable_on_value_type() { #[test] fn no_warning_mutable_on_pointer_type() { let api = Api { - version: "0.6.0".to_string(), + version: "0.7.0".to_string(), modules: vec![Module { name: "io".to_string(), functions: vec![Function { @@ -3157,7 +3145,7 @@ fn no_warning_mutable_on_pointer_type() { #[test] fn no_warning_mutable_false_on_value_type() { let api = Api { - version: "0.6.0".to_string(), + version: "0.7.0".to_string(), modules: vec![Module { name: "math".to_string(), functions: vec![Function { @@ -3199,7 +3187,7 @@ fn no_warning_mutable_false_on_value_type() { #[test] fn warning_mutable_on_enum_type() { let api = Api { - version: "0.6.0".to_string(), + version: "0.7.0".to_string(), modules: vec![Module { name: "paint".to_string(), functions: vec![Function { @@ -3239,7 +3227,7 @@ fn warning_mutable_on_enum_type() { #[test] fn warning_deprecated_function() { let api = Api { - version: "0.6.0".to_string(), + version: "0.7.0".to_string(), modules: vec![Module { name: "math".to_string(), functions: vec![Function { @@ -3275,7 +3263,7 @@ fn warning_deprecated_function() { #[test] fn no_warning_for_non_deprecated_function() { let api = Api { - version: "0.6.0".to_string(), + version: "0.7.0".to_string(), modules: vec![Module { name: "math".to_string(), functions: vec![Function { diff --git a/crates/weaveffi-core/tests/codegen_hooks.rs b/crates/weaveffi-core/tests/codegen_hooks.rs index 301bff6c..ddf0c133 100644 --- a/crates/weaveffi-core/tests/codegen_hooks.rs +++ b/crates/weaveffi-core/tests/codegen_hooks.rs @@ -11,6 +11,7 @@ use std::sync::Arc; use anyhow::Result; use camino::Utf8Path; use weaveffi_core::codegen::{ConfiguredGenerator, Generator, Orchestrator, OrchestratorHooks}; +use weaveffi_core::resolved::ResolvedApi; use weaveffi_ir::ir::{Api, Function, Module, Param, TypeRef}; const HOOK_HELPER: &str = env!("CARGO_BIN_EXE_hook_helper"); @@ -46,7 +47,12 @@ impl Generator for CountingGenerator { weaveffi_core::capabilities::TargetCapabilities::full() } - fn generate(&self, _api: &Api, out_dir: &Utf8Path, _config: &Self::Config) -> Result<()> { + fn generate( + &self, + _api: &ResolvedApi, + out_dir: &Utf8Path, + _config: &Self::Config, + ) -> Result<()> { self.calls.fetch_add(1, Ordering::SeqCst); let dir = out_dir.join(self.name); std::fs::create_dir_all(dir.as_std_path())?; @@ -55,8 +61,8 @@ impl Generator for CountingGenerator { } } -fn test_api() -> Api { - Api { +fn test_api() -> ResolvedApi { + ResolvedApi::assume_resolved(Api { version: "0.5.0".to_string(), modules: vec![Module { name: "math".to_string(), @@ -94,7 +100,7 @@ fn test_api() -> Api { }], generators: None, package: None, - } + }) } fn configured( 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 98e90fd9..24dcc7af 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.6.0", + "version": "0.7.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 a5fa4d1d..305a078c 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.6.0" +version = "0.7.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 6c47b1dd..5082db36 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.6.0" +version: "0.7.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 710e12ee..a4a83d89 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.6.0" +version: "0.7.0" modules: - name: m functions: diff --git a/crates/weaveffi-fuzz/tests/seeds.rs b/crates/weaveffi-fuzz/tests/seeds.rs index db39566a..2735346c 100644 --- a/crates/weaveffi-fuzz/tests/seeds.rs +++ b/crates/weaveffi-fuzz/tests/seeds.rs @@ -55,8 +55,8 @@ fn parse_type_ref_seed_is_well_formed() { #[test] fn validate_seed_passes_validation() { let s = seed("fuzz_validate", "minimal.yml"); - let mut api = parse_api_str(&s, "yaml").expect("seed must parse as YAML"); - validate_api(&mut api, None).expect("seed must pass validation"); + let api = parse_api_str(&s, "yaml").expect("seed must parse as YAML"); + validate_api(api, None).expect("seed must pass validation"); } /// Mirrors the body of `fuzz_target!` in `parse_yaml.rs`: arbitrary bytes must diff --git a/crates/weaveffi-gen-android/src/jni.rs b/crates/weaveffi-gen-android/src/jni.rs index 1c53f4c9..a1be0c51 100644 --- a/crates/weaveffi-gen-android/src/jni.rs +++ b/crates/weaveffi-gen-android/src/jni.rs @@ -196,15 +196,16 @@ pub(crate) fn render_jni_interface( jni_c.push_str(&w.finish()); } -/// Box one borrowed async result into the JVM local `boxed` for delivery to -/// the pinned `WeaveContinuation`. Buffered results arrive as a borrowed +/// Box one owned async result into the JVM local `boxed` for delivery to +/// the pinned `WeaveContinuation`. Buffered results arrive as an owned /// `(result_ptr, result_len)` pair, copied into a `jbyteArray` the Kotlin -/// wrapper decodes; the producer frees the buffer after the callback returns. +/// wrapper decodes and then released with `weaveffi_free_bytes`. fn write_jni_box_result(out: &mut String, ret: Option<&TypeRef>) { let mut w = CodeWriter::four_space().with_depth(2); if ret.is_some_and(abi::is_buffered) { w.line("jbyteArray boxed = (*env)->NewByteArray(env, (jsize)result_len);"); w.line("if (boxed && result_ptr) { (*env)->SetByteArrayRegion(env, boxed, 0, (jsize)result_len, (const jbyte*)result_ptr); }"); + w.line("weaveffi_free_bytes((uint8_t*)result_ptr, result_len);"); w.line("jclass cls = (*env)->GetObjectClass(env, ctx->callback);"); w.line( "jmethodID mid = (*env)->GetMethodID(env, cls, \"onSuccess\", \"(Ljava/lang/Object;)V\");", @@ -260,13 +261,15 @@ fn write_jni_box_result(out: &mut String, ret: Option<&TypeRef>) { w.line("jobject boxed = (*env)->CallStaticObjectMethod(env, boxCls, valueOf, result ? JNI_TRUE : JNI_FALSE);"); } Some(TypeRef::StringUtf8 | TypeRef::BorrowedStr) => { - // The producer owns `result` for the callback's duration only: - // copy, never free. + // Owned by the consumer: copy, then free. w.line("jobject boxed = result ? (jobject)(*env)->NewStringUTF(env, result) : (jobject)(*env)->NewStringUTF(env, \"\");"); + w.line("weaveffi_free_string(result);"); } Some(TypeRef::Bytes | TypeRef::BorrowedBytes) => { + // Owned by the consumer: copy, then free. w.line("jbyteArray boxed = (*env)->NewByteArray(env, (jsize)result_len);"); w.line("if (boxed && result) { (*env)->SetByteArrayRegion(env, boxed, 0, (jsize)result_len, (const jbyte*)result); }"); + w.line("weaveffi_free_bytes((uint8_t*)result, result_len);"); } // Only `Interface?` reaches here (every other optional is buffered): // a nullable owned pointer boxed as `Long`, null crossing as `null`. @@ -354,8 +357,9 @@ pub(crate) fn render_jni_async_function( // 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 payload - // buffer is borrowed, so it is copied before the callback returns. + // 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 = (*env)->NewStringUTF(env, msg);"); w.line("jbyteArray jpayload = NULL;"); @@ -363,9 +367,11 @@ pub(crate) fn render_jni_async_function( 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, (jint)err->code, jmsg, jpayload);"); + w.line("(*env)->CallVoidMethod(env, ctx->callback, mid, jcode, jmsg, jpayload);"); }); w.line("} else {"); w.scope(|w| { diff --git a/crates/weaveffi-gen-android/src/lib.rs b/crates/weaveffi-gen-android/src/lib.rs index b200255d..713a730a 100644 --- a/crates/weaveffi-gen-android/src/lib.rs +++ b/crates/weaveffi-gen-android/src/lib.rs @@ -23,12 +23,12 @@ mod types; use camino::Utf8Path; use serde::{Deserialize, Serialize}; use std::fmt::Write as _; +use weaveffi_core::resolved::ResolvedApi; use weaveffi_core::backend::{LanguageBackend, OutputFile}; use weaveffi_core::capabilities::TargetCapabilities; use weaveffi_core::model::{BindingModel, CallShape, CallbackBinding}; use weaveffi_core::pkg; use weaveffi_core::utils::{render_prelude, render_trailer, CommentStyle}; -use weaveffi_ir::ir::Api; use crate::calls::{render_kotlin_free_fn, render_listener_api}; use crate::codec::model_uses_buffers; @@ -122,7 +122,7 @@ impl LanguageBackend for AndroidGenerator { fn files( &self, - api: &Api, + api: &ResolvedApi, model: &BindingModel, out_dir: &Utf8Path, config: &Self::Config, diff --git a/crates/weaveffi-gen-android/src/tests.rs b/crates/weaveffi-gen-android/src/tests.rs index 92723bcf..1cb18eb6 100644 --- a/crates/weaveffi-gen-android/src/tests.rs +++ b/crates/weaveffi-gen-android/src/tests.rs @@ -1,6 +1,7 @@ //! Unit tests for the Android backend: rendering assertions over the Kotlin //! wrapper and the JNI C bridge, plus file-set and config-variant checks. +use weaveffi_core::resolved::ResolvedApi; use camino::Utf8Path; use weaveffi_core::codegen::common::pascal_case; use weaveffi_core::codegen::Generator; @@ -13,18 +14,18 @@ use weaveffi_ir::ir::{ use crate::types::{jni_param_type, kotlin_jni_type, kotlin_type}; use crate::{AndroidConfig, AndroidGenerator}; -fn make_api(modules: Vec) -> Api { - Api { - version: "0.6.0".to_string(), +fn make_api(modules: Vec) -> ResolvedApi { + ResolvedApi::assume_resolved(Api { + version: "0.7.0".to_string(), modules, generators: None, package: None, - } + }) } /// Test-local shim mirroring the driver: build the model once and hand it /// to the renderer (production code never calls `BindingModel::build`). -fn render_kotlin(api: &Api, package: &str, strip: bool, input_basename: &str) -> String { +fn render_kotlin(api: &ResolvedApi, package: &str, strip: bool, input_basename: &str) -> String { super::render_kotlin( &BindingModel::build(api, "weaveffi"), package, @@ -36,7 +37,7 @@ fn render_kotlin(api: &Api, package: &str, strip: bool, input_basename: &str) -> /// Test-local shim for the JNI renderer; `c_prefix` seeds the model the /// same way the driver's global prefix does. fn render_jni_c( - api: &Api, + api: &ResolvedApi, package: &str, strip: bool, input_basename: &str, @@ -50,7 +51,7 @@ fn render_jni_c( ) } -fn make_struct_api() -> Api { +fn make_struct_api() -> ResolvedApi { make_api(vec![Module { name: "contacts".to_string(), functions: vec![], @@ -62,13 +63,11 @@ fn make_struct_api() -> Api { name: "name".to_string(), ty: TypeRef::StringUtf8, doc: None, - default: None, }, StructField { name: "age".to_string(), ty: TypeRef::I32, doc: None, - default: None, }, ], }], @@ -95,14 +94,13 @@ fn field(name: &str, ty: TypeRef) -> StructField { name: name.to_string(), ty, doc: None, - default: None, } } /// The `shapes` conformance sample in its already-resolved IR form: a rich /// (algebraic) enum `Shape`, a plain enum `Channel`, and free functions that /// take/return the rich enum (lowered to an opaque `Struct` pointer). -fn make_shapes_api() -> Api { +fn make_shapes_api() -> ResolvedApi { make_api(vec![Module { name: "shapes".to_string(), enums: vec![ @@ -557,7 +555,7 @@ fn list_of_string_return_is_buffered() { /// A single-module API with one free function, for return-marshalling /// tests. -fn make_fn_api(name: &str, params: Vec, returns: Option, throws: bool) -> Api { +fn make_fn_api(name: &str, params: Vec, returns: Option, throws: bool) -> ResolvedApi { make_api(vec![Module { name: "m".to_string(), functions: vec![Function { @@ -894,7 +892,7 @@ fn listener_exception_policy_routes_to_handler_then_describes() { } #[test] -fn async_bytes_result_is_copied_not_freed() { +fn async_bytes_result_is_copied_then_freed() { let api = make_api(vec![Module { name: "m".to_string(), functions: vec![Function { @@ -923,7 +921,7 @@ fn async_bytes_result_is_copied_not_freed() { ); assert!( jni.contains("jbyteArray boxed = (*env)->NewByteArray(env, (jsize)result_len);"), - "the borrowed buffer must be deep-copied into a ByteArray: {jni}" + "the owned buffer must be deep-copied into a ByteArray: {jni}" ); let cb_start = jni.find("_jni_cb(void* context").expect("callback missing"); let cb_end = jni[cb_start..] @@ -932,8 +930,12 @@ fn async_bytes_result_is_copied_not_freed() { .expect("callback body must close"); let cb_body = &jni[cb_start..cb_end]; assert!( - !cb_body.contains("weaveffi_free_bytes"), - "the callback borrows the result buffer and must not free it: {cb_body}" + cb_body.contains("weaveffi_free_bytes((uint8_t*)result, result_len);"), + "the callback owns the result buffer and must free it after copying: {cb_body}" + ); + assert!( + cb_body.contains("weaveffi_error_free(err);"), + "the callback owns the boxed error and must free it: {cb_body}" ); assert!( cb_body.contains("weaveffi_jni_handle_uncaught(env);"), @@ -1035,7 +1037,6 @@ fn kotlin_struct_with_bytes_field() { name: "data".to_string(), ty: TypeRef::Bytes, doc: None, - default: None, }], }], enums: vec![], @@ -1073,7 +1074,6 @@ fn kotlin_struct_with_nested_struct_field() { name: "start".to_string(), ty: TypeRef::Record("Point".into()), doc: None, - default: None, }], }], enums: vec![], @@ -1920,19 +1920,16 @@ fn generate_android_with_structs_and_enums() { name: "name".to_string(), ty: TypeRef::StringUtf8, doc: None, - default: None, }, StructField { name: "email".to_string(), ty: TypeRef::StringUtf8, doc: None, - default: None, }, StructField { name: "age".to_string(), ty: TypeRef::I32, doc: None, - default: None, }, ], }], @@ -2329,7 +2326,7 @@ fn android_custom_package() { /// One module declaring an error domain, with one throwing and one /// non-throwing function, shared by the typed-error tests. -fn make_error_api() -> Api { +fn make_error_api() -> ResolvedApi { make_api(vec![Module { name: "contacts".to_string(), functions: vec![ @@ -2428,11 +2425,12 @@ fn kotlin_inline_error_types() { #[test] fn kotlin_error_payload_fields_decode() { - let mut api = make_error_api(); + let mut api = make_error_api().api().clone(); api.modules[0].errors.as_mut().unwrap().codes[0].fields = vec![ field("contact_id", TypeRef::I64), field("hint", TypeRef::Optional(Box::new(TypeRef::StringUtf8))), ]; + let api = ResolvedApi::assume_resolved(api); let kt = render_kotlin(&api, "com.weaveffi", true, "weaveffi.yml"); assert!( kt.contains( @@ -2655,7 +2653,6 @@ fn android_deeply_nested_optional() { name: "name".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }], }], enums: vec![], @@ -2746,7 +2743,6 @@ fn android_enum_keyed_map() { name: "name".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }], }], enums: vec![EnumDef { @@ -2819,7 +2815,6 @@ fn android_typed_handle_type() { name: "name".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }], }], enums: vec![], @@ -2863,7 +2858,6 @@ fn android_no_double_free_on_error() { name: "name".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }], }], enums: vec![], @@ -3011,7 +3005,6 @@ fn android_null_check_on_optional_return() { name: "name".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }], }], enums: vec![], @@ -3184,7 +3177,7 @@ fn android_async_pins_callback_for_lifetime() { ); } -fn doc_api() -> Api { +fn doc_api() -> ResolvedApi { make_api(vec![Module { name: "docs".into(), functions: vec![Function { @@ -3210,7 +3203,6 @@ fn doc_api() -> Api { name: "id".into(), ty: TypeRef::I64, doc: Some("Stable id".into()), - default: None, }], }], enums: vec![EnumDef { @@ -3275,7 +3267,7 @@ fn android_emits_doc_on_param() { /// the `new` constructor, a named factory, sync methods (throwing and /// not), an async throwing method, a static, and an interface-typed /// parameter and return. -fn make_interface_api() -> Api { +fn make_interface_api() -> ResolvedApi { use weaveffi_ir::ir::InterfaceDef; make_api(vec![Module { name: "kv".to_string(), @@ -3561,9 +3553,9 @@ fn samples_generate_android_and_c_outputs() { return; } let contents = std::fs::read_to_string(idl.as_std_path()).unwrap(); - let mut api = weaveffi_ir::parse::parse_api_str(&contents, "yaml") + let api = weaveffi_ir::parse::parse_api_str(&contents, "yaml") .unwrap_or_else(|e| panic!("parse {sample}: {e}")); - weaveffi_core::validate::validate_api(&mut api, None) + let api = weaveffi_core::validate::validate_api(api, None) .unwrap_or_else(|e| panic!("validate {sample}: {e:?}")); let out = genroot.join(sample); let android_cfg = AndroidConfig { @@ -3663,7 +3655,6 @@ fn kotlin_reserved_word_identifiers_are_escaped() { name: "val".to_string(), ty: TypeRef::StringUtf8, doc: None, - default: None, }], }], enums: vec![], diff --git a/crates/weaveffi-gen-c/src/header.rs b/crates/weaveffi-gen-c/src/header.rs index 06116522..d1971587 100644 --- a/crates/weaveffi-gen-c/src/header.rs +++ b/crates/weaveffi-gen-c/src/header.rs @@ -12,16 +12,21 @@ use weaveffi_core::model::BindingModel; use weaveffi_core::utils::{ render_abi_prefix_aliases, render_prelude, render_trailer, CommentStyle, }; -use weaveffi_ir::ir::Api; +use weaveffi_core::resolved::ResolvedApi; use crate::idents::escape_module_param_names; /// Render the complete `{prefix}.h` for `api` using `prefix` for every symbol. /// -/// Thin `Api`-based wrapper over [`render_c_header_from_model`] for tests and -/// callers that only hold an [`Api`]; the production path renders directly from -/// the driver-built [`BindingModel`] without re-deriving it. -pub fn render_c_header(api: &Api, prefix: &str, input_basename: &str, filename: &str) -> String { +/// 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) } diff --git a/crates/weaveffi-gen-c/src/lib.rs b/crates/weaveffi-gen-c/src/lib.rs index 19c77514..ef229a96 100644 --- a/crates/weaveffi-gen-c/src/lib.rs +++ b/crates/weaveffi-gen-c/src/lib.rs @@ -1,7 +1,8 @@ //! C header generator for WeaveFFI. //! -//! Emits a single `{prefix}.h` describing the stable C ABI surface of an -//! [`Api`], plus a companion `{prefix}.c` placeholder for future convenience +//! Emits a single `{prefix}.h` describing the stable C ABI surface of a +//! [`ResolvedApi`](weaveffi_core::resolved::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. //! @@ -21,11 +22,11 @@ mod tests; use camino::Utf8Path; use serde::{Deserialize, Serialize}; +use weaveffi_core::resolved::ResolvedApi; use weaveffi_core::backend::{LanguageBackend, OutputFile}; use weaveffi_core::capabilities::TargetCapabilities; use weaveffi_core::model::BindingModel; use weaveffi_core::package::{PackageContext, PackagedFile}; -use weaveffi_ir::ir::Api; pub use header::{render_c_header, render_c_header_from_model}; @@ -81,7 +82,7 @@ impl LanguageBackend for CGenerator { fn files( &self, - _api: &Api, + _api: &ResolvedApi, model: &BindingModel, out_dir: &Utf8Path, config: &Self::Config, @@ -105,7 +106,7 @@ impl LanguageBackend for CGenerator { fn package( &self, - _api: &Api, + _api: &ResolvedApi, model: &BindingModel, ctx: &PackageContext, out_dir: &Utf8Path, diff --git a/crates/weaveffi-gen-c/src/tests.rs b/crates/weaveffi-gen-c/src/tests.rs index 68013ad4..437e9df5 100644 --- a/crates/weaveffi-gen-c/src/tests.rs +++ b/crates/weaveffi-gen-c/src/tests.rs @@ -1,6 +1,8 @@ //! Tests for the C backend: rendered-header assertions over fixture APIs, //! keyword-escaping regressions, and packaging checks. +use weaveffi_ir::ir::Api; +use weaveffi_core::resolved::ResolvedApi; use camino::Utf8Path; use weaveffi_core::codegen::Generator; use weaveffi_ir::ir::{ @@ -15,12 +17,12 @@ fn package_bundles_header_libs_and_cmake() { use weaveffi_core::package::{FileContent, PackageContext}; use weaveffi_core::platform::{BinarySet, Platform}; - let api = Api { - version: "0.6.0".into(), + let api = ResolvedApi::assume_resolved(Api { + version: "0.7.0".into(), modules: vec![module("calc")], generators: None, package: None, - }; + }); let model = BindingModel::build(&api, "weaveffi"); let mut bins = BinarySet::new("calculator"); bins.insert(Platform::MacosArm64, "/s/darwin-arm64/libcalculator.dylib"); @@ -97,16 +99,16 @@ fn module(name: &str) -> Module { } } -fn api(modules: Vec) -> Api { - Api { - version: "0.6.0".into(), +fn api(modules: Vec) -> ResolvedApi { + ResolvedApi::assume_resolved(Api { + version: "0.7.0".into(), modules, generators: None, package: None, - } + }) } -fn header(api: &Api, prefix: &str) -> String { +fn header(api: &ResolvedApi, prefix: &str) -> String { render_c_header(api, prefix, "weaveffi.yml", "weaveffi.h") } @@ -131,7 +133,6 @@ fn typed_handle_target_gets_forward_typedef() { name: "id".into(), ty: TypeRef::I64, doc: None, - default: None, }], }], ..module("shared") @@ -249,7 +250,6 @@ fn record_is_a_value_type_with_no_c_object() { name: "name".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }], }], functions: vec![func( @@ -281,7 +281,6 @@ fn record_return_uses_out_buffer_params() { name: "name".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }], }], functions: vec![func( diff --git a/crates/weaveffi-gen-c/tests/render_samples.rs b/crates/weaveffi-gen-c/tests/render_samples.rs index aa7c8ce6..186d11a5 100644 --- a/crates/weaveffi-gen-c/tests/render_samples.rs +++ b/crates/weaveffi-gen-c/tests/render_samples.rs @@ -15,8 +15,8 @@ fn render_sample_headers() { 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 mut api = weaveffi_ir::parse::parse_api_str(&src, "yaml").unwrap(); - weaveffi_core::validate::validate_api(&mut api, None).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(); diff --git a/crates/weaveffi-gen-cpp/src/calls.rs b/crates/weaveffi-gen-cpp/src/calls.rs index 6b569e4a..28eb686a 100644 --- a/crates/weaveffi-gen-cpp/src/calls.rs +++ b/crates/weaveffi-gen-cpp/src/calls.rs @@ -640,9 +640,10 @@ fn render_iterator_next_method( /// 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. Borrowed -/// result buffers are copied or decoded inside the callback, before the -/// producer reclaims them. +/// 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. fn render_async_callable( out: &mut String, f: &FnBinding, @@ -710,6 +711,7 @@ fn render_async_callable( w.scope(|w| { w.line("std::string msg(err->message ? err->message : \"unknown error\");"); w.line(format!("p->set_exception({make_error});")); + w.line(format!("{prefix}_error_free(err);")); }); w.line("} else {"); w.scope(|w| { @@ -735,25 +737,33 @@ fn render_async_callable( /// /// Per the async completion contract (`weaveffi_core::plan::AsyncProtocol`), /// result buffers handed to the callback (strings, bytes, and buffered -/// values) are *borrowed*: they stay owned by the producer and are valid only -/// for the callback's duration, so the wrapper deep-copies or decodes them -/// and never frees them. An owned interface result is the exception: the -/// callback receives ownership and adopts the pointer into the RAII wrapper. +/// values) are *owned by the consumer*: the wrapper copies or decodes them, +/// then releases the producer allocation through the runtime free symbols. +/// An owned interface result is instead adopted into the RAII wrapper. fn emit_async_set_value(w: &mut CodeWriter, ty: &TypeRef, module: &str, prefix: &str) { match ret_pass(Some(ty), module, prefix) { RetPass::Buffer => { - // Borrowed `(result_ptr, result_len)` buffer: decode, never free. + // Owned `(result_ptr, result_len)` buffer: decode, then free. w.line("detail::BufferReader result_r(result_ptr, result_len);"); emit_read_decl(w, ty, "value", "result_r", module, prefix); w.line("result_r.expect_end();"); + w.line(format!( + "{prefix}_free_bytes(const_cast(result_ptr), result_len);" + )); w.line("p->set_value(std::move(value));"); } - // Borrowed for the callback's duration: copy, do not free. + // Owned by the consumer: copy, then free. RetPass::String => { - w.line("p->set_value(std::string(result));"); + w.line("std::string value(result ? result : \"\");"); + w.line(format!("{prefix}_free_string(result);")); + w.line("p->set_value(std::move(value));"); } RetPass::Bytes => { - w.line("p->set_value(std::vector(result, result + result_len));"); + w.line("std::vector value(result, result + result_len);"); + w.line(format!( + "{prefix}_free_bytes(const_cast(result), result_len);" + )); + w.line("p->set_value(std::move(value));"); } // Owned interface result: the callback receives ownership; adopt it. // A nullable result maps null to `std::nullopt`. diff --git a/crates/weaveffi-gen-cpp/src/lib.rs b/crates/weaveffi-gen-cpp/src/lib.rs index e6ea20ae..98ff6bf7 100644 --- a/crates/weaveffi-gen-cpp/src/lib.rs +++ b/crates/weaveffi-gen-cpp/src/lib.rs @@ -7,7 +7,7 @@ //! [`LanguageBackend`]; the shared driver bridges it into the generator //! pipeline. //! -//! The generated surface follows the 0.6.0 value-buffer layout: +//! The generated surface follows the 0.7.0 value-buffer layout: //! //! * Records are plain C++ value structs with typed members; rich (algebraic) //! enums are `std::variant`-backed sum types with one payload struct per @@ -52,6 +52,7 @@ mod types; use camino::Utf8Path; use serde::{Deserialize, Serialize}; +use weaveffi_core::resolved::ResolvedApi; use weaveffi_core::abi::is_buffered; use weaveffi_core::backend::{LanguageBackend, OutputFile}; use weaveffi_core::cabi; @@ -61,7 +62,7 @@ use weaveffi_core::package::{PackageContext, PackagedFile}; use weaveffi_core::utils::{ render_abi_prefix_aliases, render_prelude, render_trailer, CommentStyle, }; -use weaveffi_ir::ir::{Api, TypeRef}; +use weaveffi_ir::ir::TypeRef; use crate::calls::render_cpp_module_ns; use crate::entities::{ @@ -142,7 +143,7 @@ impl LanguageBackend for CppGenerator { fn files( &self, - api: &Api, + api: &ResolvedApi, model: &BindingModel, out_dir: &Utf8Path, config: &Self::Config, @@ -170,7 +171,7 @@ impl LanguageBackend for CppGenerator { fn package( &self, - api: &Api, + api: &ResolvedApi, model: &BindingModel, ctx: &PackageContext, out_dir: &Utf8Path, diff --git a/crates/weaveffi-gen-cpp/src/tests.rs b/crates/weaveffi-gen-cpp/src/tests.rs index d42a12e0..92ebd246 100644 --- a/crates/weaveffi-gen-cpp/src/tests.rs +++ b/crates/weaveffi-gen-cpp/src/tests.rs @@ -1,6 +1,7 @@ //! Tests for the C++ backend: rendered-header assertions over fixture APIs, //! plus config, packaging, and determinism checks. +use weaveffi_core::resolved::ResolvedApi; use camino::Utf8Path; use weaveffi_core::codegen::Generator; use weaveffi_core::lang::{self, CPP_KEYWORDS}; @@ -28,7 +29,6 @@ fn field(name: &str, ty: TypeRef) -> StructField { name: name.into(), ty, doc: None, - default: None, } } @@ -88,22 +88,22 @@ fn empty_module(name: &str) -> Module { } } -fn api_of(modules: Vec) -> Api { - Api { - version: "0.6.0".into(), +fn api_of(modules: Vec) -> ResolvedApi { + ResolvedApi::assume_resolved(Api { + version: "0.7.0".into(), modules, generators: None, package: None, - } + }) } /// Render with the default namespace and prefix, as the driver would. -fn render(api: &Api) -> String { +fn render(api: &ResolvedApi) -> String { let model = BindingModel::build(api, "weaveffi"); render_cpp_header(&model, "weaveffi", "weaveffi.yml", "weaveffi.hpp") } -fn minimal_api() -> Api { +fn minimal_api() -> ResolvedApi { let mut m = empty_module("calculator"); m.functions = vec![func( "add", @@ -113,7 +113,7 @@ fn minimal_api() -> Api { api_of(vec![m]) } -fn contacts_api() -> Api { +fn contacts_api() -> ResolvedApi { let mut m = empty_module("contacts"); m.enums = vec![EnumDef { name: "ContactType".into(), @@ -150,7 +150,7 @@ fn contacts_api() -> Api { /// enum, struct, an interface with a factory constructor, /// sync/iterator/async methods, a static, and a nested module whose /// function takes the interface across modules. -fn kvstore_api() -> Api { +fn kvstore_api() -> ResolvedApi { let mut kv = empty_module("kv"); kv.errors = Some(ErrorDomain { name: "KvError".into(), @@ -1327,7 +1327,7 @@ fn interface_param_passing_between_modules() { // ── Rich enum tests ── -fn shapes_api() -> Api { +fn shapes_api() -> ResolvedApi { let mut m = empty_module("geometry"); m.enums = vec![EnumDef { name: "Shape".into(), @@ -1710,8 +1710,8 @@ fn async_error_settles_promise_with_typed_exception() { ); } -/// An async buffered result is borrowed: the trampoline decodes it inside -/// the callback and never frees it. +/// An async buffered result is owned by the consumer: the trampoline decodes +/// it inside the callback and then frees the producer allocation. #[test] fn async_buffered_result_decoded_in_callback() { let mut m = empty_module("feed"); @@ -1727,7 +1727,7 @@ fn async_buffered_result_decoded_in_callback() { let h = render(&api_of(vec![m])); assert!( h.contains("const uint8_t* result_ptr, size_t result_len"), - "callback should receive the borrowed buffer slots: {h}" + "callback should receive the owned buffer slots: {h}" ); let cb = &h[h.find("inline std::future fetch(").unwrap()..]; let cb = &cb[..cb.find("\n}\n").unwrap()]; @@ -1735,11 +1735,11 @@ fn async_buffered_result_decoded_in_callback() { cb.contains("detail::BufferReader result_r(result_ptr, result_len);") && cb.contains("Batch value = detail::read_Batch(result_r);") && cb.contains("p->set_value(std::move(value));"), - "borrowed result must decode inside the callback: {cb}" + "owned result must decode inside the callback: {cb}" ); assert!( - !cb.contains("weaveffi_free_bytes"), - "borrowed async buffers must never be freed: {cb}" + cb.contains("weaveffi_free_bytes(const_cast(result_ptr), result_len);"), + "owned async buffers must be freed after decoding: {cb}" ); } diff --git a/crates/weaveffi-gen-dart/src/calls.rs b/crates/weaveffi-gen-dart/src/calls.rs index 8121aeee..246dcaaf 100644 --- a/crates/weaveffi-gen-dart/src/calls.rs +++ b/crates/weaveffi-gen-dart/src/calls.rs @@ -429,8 +429,8 @@ pub(crate) fn render_listener( /// The (native, dart, name) slot triples an async completion callback carries /// after its `(context, err)` prefix. Bytes and buffered results arrive as -/// borrowed `(result, resultLen)` pairs; interfaces as adopted pointers; -/// everything else by value. +/// owned `(result, resultLen)` pairs the consumer frees; interfaces as +/// adopted pointers; everything else by value. fn async_cb_result_slots(ret: Option<&TypeRef>) -> Vec<(String, String, String)> { let Some(ty) = ret else { return vec![]; @@ -579,7 +579,7 @@ fn render_async_function( "final payload = _copyNativeBytes(err.ref.payloadPtr, err.ref.payloadLen);", ); } - w.line("_weaveffiErrorClear(err);"); + w.line("_weaveffiErrorFree(err);"); w.line(format!("completer.completeError({});", err.map_expr())); w.line("return;"); }); @@ -627,9 +627,11 @@ fn render_async_function( } /// Emit the callback statements that resolve the completer from the result -/// slots. Bytes and buffered results are borrowed for the callback's -/// duration, so they are copied (bytes) or decoded (buffered) here and never -/// freed; an owned interface result is instead adopted by its wrapper class. +/// 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<&TypeRef>, indent: &str) { let mut w = CodeWriter::two_space().with_depth(indent.len() / 2); let Some(ty) = ty else { @@ -638,21 +640,25 @@ fn emit_async_complete(out: &mut String, ty: Option<&TypeRef>, indent: &str) { return; }; match plan::ret_pass(Some(ty), "", "") { - // Decode inside the callback: the producer frees the encoding as - // soon as the callback returns. + // 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 value = {};", read_expr("resultReader", ty))); w.line("resultReader.expectEnd();"); w.line("completer.complete(value);"); } RetPass::Bytes => { - w.line("completer.complete(_copyNativeBytes(result, resultLen));"); + w.line("final resultData = _copyNativeBytes(result, resultLen);"); + w.line("_weaveffiFreeBytes(result, resultLen);"); + w.line("completer.complete(resultData);"); } - // Borrowed: copy before the callback returns, never free. + // Copy the owned C string, then release the producer allocation. RetPass::String => { - w.line("completer.complete(result.toDartString());"); + w.line("final value = result.toDartString();"); + w.line("_weaveffiFreeString(result);"); + w.line("completer.complete(value);"); } // The callback receives ownership of an object result; the wrapper // adopts the pointer and its `dispose()` owns the eventual destroy. diff --git a/crates/weaveffi-gen-dart/src/lib.rs b/crates/weaveffi-gen-dart/src/lib.rs index e46c569d..979170f9 100644 --- a/crates/weaveffi-gen-dart/src/lib.rs +++ b/crates/weaveffi-gen-dart/src/lib.rs @@ -28,6 +28,7 @@ mod tests; use camino::Utf8Path; use serde::{Deserialize, Serialize}; +use weaveffi_core::resolved::ResolvedApi; use weaveffi_core::backend::{LanguageBackend, OutputFile}; use weaveffi_core::capabilities::TargetCapabilities; use weaveffi_core::model::{ @@ -37,7 +38,6 @@ use weaveffi_core::model::{ use weaveffi_core::package::{PackageContext, PackagedFile}; use weaveffi_core::pkg; use weaveffi_core::utils::{render_prelude, render_trailer, CommentStyle}; -use weaveffi_ir::ir::Api; use crate::calls::{render_callback_typedef, render_function, render_listener}; use crate::entities::{render_enum, render_error, render_interface, render_struct}; @@ -106,7 +106,12 @@ impl DartGenerator { /// [`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: &Api, model: &BindingModel, config: &DartConfig) -> String { + 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 @@ -252,7 +257,7 @@ impl LanguageBackend for DartGenerator { fn files( &self, - api: &Api, + api: &ResolvedApi, model: &BindingModel, out_dir: &Utf8Path, config: &Self::Config, @@ -292,7 +297,7 @@ impl LanguageBackend for DartGenerator { fn package( &self, - api: &Api, + api: &ResolvedApi, model: &BindingModel, ctx: &PackageContext, out_dir: &Utf8Path, diff --git a/crates/weaveffi-gen-dart/src/runtime.rs b/crates/weaveffi-gen-dart/src/runtime.rs index 92460e83..24155d6b 100644 --- a/crates/weaveffi-gen-dart/src/runtime.rs +++ b/crates/weaveffi-gen-dart/src/runtime.rs @@ -369,6 +369,18 @@ pub(crate) fn render_error_plumbing(out: &mut String) { "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`. diff --git a/crates/weaveffi-gen-dart/src/tests.rs b/crates/weaveffi-gen-dart/src/tests.rs index 97ee7fb4..8a3a651f 100644 --- a/crates/weaveffi-gen-dart/src/tests.rs +++ b/crates/weaveffi-gen-dart/src/tests.rs @@ -1,6 +1,7 @@ //! Unit tests for the Dart generator: rendered-source assertions over //! representative APIs, plus slot-mapping and packaging checks. +use weaveffi_core::resolved::ResolvedApi; use camino::Utf8Path; use weaveffi_core::abi::lower_param; use weaveffi_core::backend::LanguageBackend; @@ -13,13 +14,13 @@ use weaveffi_ir::ir::{ use crate::types::{dart_type, input_slots, return_ffi, return_out_slots}; use crate::{DartConfig, DartGenerator}; -fn make_api(modules: Vec) -> Api { - Api { - version: "0.6.0".into(), +fn make_api(modules: Vec) -> ResolvedApi { + ResolvedApi::assume_resolved(Api { + version: "0.7.0".into(), modules, generators: None, package: None, - } + }) } fn simple_module(functions: Vec) -> Module { @@ -64,7 +65,6 @@ fn field(name: &str, ty: TypeRef) -> StructField { name: name.into(), ty, doc: None, - default: None, } } @@ -84,7 +84,7 @@ fn pb(name: &str, ty: TypeRef) -> ParamBinding { /// Build the binding model and render the module exactly as the driver /// does in production before calling [`LanguageBackend::files`]. Shadows /// the production renderer for the test suite. -fn render_dart_module(api: &Api, prefix: &str, input_basename: &str) -> String { +fn render_dart_module(api: &ResolvedApi, prefix: &str, input_basename: &str) -> String { let model = BindingModel::build(api, prefix); let config = DartConfig { prefix: Some(prefix.to_string()), @@ -1354,7 +1354,7 @@ fn dart_null_check_on_optional_return() { ); } -fn doc_api() -> Api { +fn doc_api() -> ResolvedApi { make_api(vec![Module { name: "docs".into(), functions: vec![Function { @@ -1372,7 +1372,6 @@ fn doc_api() -> Api { name: "id".into(), ty: TypeRef::I64, doc: Some("Stable id".into()), - default: None, }], }], enums: vec![EnumDef { @@ -1424,7 +1423,7 @@ fn dart_emits_doc_on_field() { /// A rich (algebraic) enum mirroring `samples/shapes`: a unit variant, an /// f64 payload, two f32 payloads, and a (string, u8) payload, plus a plain /// sibling enum and functions that take/return the rich enum by value. -fn rich_enum_api() -> Api { +fn rich_enum_api() -> ResolvedApi { make_api(vec![Module { name: "shapes".into(), functions: vec![ @@ -1462,7 +1461,6 @@ fn rich_enum_api() -> Api { name: "radius".into(), ty: TypeRef::F64, doc: Some("Radius in points".into()), - default: None, }], }, EnumVariant { @@ -1649,7 +1647,7 @@ fn rich_enum_functions_marshal_buffers() { /// exercising every member kind: a plain constructor named `new`, a /// throwing named constructor, throwing and non-throwing methods, an /// async throwing method, an iterator method, and a static. -fn store_api() -> Api { +fn store_api() -> ResolvedApi { use weaveffi_ir::ir::{ErrorCode, ErrorDomain, InterfaceDef}; fn f( name: &str, @@ -2272,6 +2270,7 @@ fn contacts_sample_renders_interface_and_domain() { // Generators run strictly post-resolution: rewrite every parsed // `Named` reference into its resolved kind first, as the CLI does. weaveffi_core::validate::resolve_type_refs(&mut api); + let api = ResolvedApi::assume_resolved(api); let dart = render_dart_module(&api, "weaveffi", "contacts.yml"); assert!( dart.contains("enum ContactType {"), @@ -2303,7 +2302,7 @@ fn contacts_sample_renders_interface_and_domain() { } /// One-function module helper for the ownership-audit tests below. -fn returning(name: &str, returns: TypeRef) -> Api { +fn returning(name: &str, returns: TypeRef) -> ResolvedApi { make_api(vec![simple_module(vec![func(name, vec![], Some(returns))])]) } @@ -2395,7 +2394,7 @@ fn optional_scalar_return_decodes_flag() { /// Async result buffers are borrowed for the callback's duration: the /// wrapper decodes them inside the callback and never frees them. #[test] -fn async_buffer_results_decode_and_never_free() { +fn async_buffer_results_decode_then_free() { let api = make_api(vec![simple_module(vec![ Function { r#async: true, @@ -2415,7 +2414,7 @@ fn async_buffer_results_decode_and_never_free() { dart.contains("Future> fetchNames()"), "missing async list wrapper: {dart}" ); - // The borrowed (ptr, len) pair is copied and decoded in the callback. + // The owned (ptr, len) pair is copied, freed, and decoded in the callback. assert!( dart.contains("final resultData = _copyNativeBytes(result, resultLen);") && dart.contains( @@ -2423,18 +2422,14 @@ fn async_buffer_results_decode_and_never_free() { ), "async buffered result must be decoded inside the callback: {dart}" ); - assert!( - dart.contains("completer.complete(_copyNativeBytes(result, resultLen));"), - "async bytes result must be copied: {dart}" - ); - // Borrowed: the callback must not release the producer's buffers. + // Owned: the callback copies and then releases the producer's buffers. let cb = &dart[dart .find("Future> fetchNames()") .expect("wrapper")..]; let cb = &cb[..cb.find("\n}").expect("end")]; assert!( - !cb.contains("_weaveffiFree"), - "async callback must never free borrowed result buffers: {cb}" + cb.contains("_weaveffiFreeBytes(result, resultLen);"), + "async callback must free the owned result buffer after copying: {cb}" ); } diff --git a/crates/weaveffi-gen-dotnet/src/calls.rs b/crates/weaveffi-gen-dotnet/src/calls.rs index 5ac03d4b..e548f47c 100644 --- a/crates/weaveffi-gen-dotnet/src/calls.rs +++ b/crates/weaveffi-gen-dotnet/src/calls.rs @@ -60,8 +60,8 @@ impl<'a> ErrCtx<'a> { /// The exception expression an async completion callback faults its /// `TaskCompletionSource` with. A domain exception decodes the error's - /// structured payload, copied from the borrowed error struct inside the - /// callback (the producer releases the original afterward). + /// 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(), @@ -605,7 +605,7 @@ pub(crate) fn render_iterator_wrapper_method( out.push_str(&w.finish()); } -/// True when an async result crosses the completion callback as a borrowed +/// True when an async result crosses the completion callback as an owned /// `ptr` + `len` pair: bytes and every buffered type (records, rich enums, /// lists, maps, and non-interface optionals). pub(crate) fn async_result_is_ptr_len(ty: &TypeRef) -> bool { @@ -683,14 +683,14 @@ pub(crate) fn render_async_wrapper_method( w.line("if (wErr.Code != 0)"); w.line("{"); w.scope(|w| { - // The error struct is borrowed for the callback's - // duration (the producer releases it afterward), so - // the message and payload are copied here and the - // error is never cleared by the consumer. + // 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;"); }); @@ -792,12 +792,12 @@ pub(crate) fn render_async_wrapper_method( } /// Emit the statements resolving the `TaskCompletionSource` from the -/// completion callback's result slots, honoring the `AsyncProtocol` borrowed +/// completion callback's result slots, honoring the `AsyncProtocol` owned /// results clause: string, bytes, and buffered result buffers are owned by -/// the producer and valid only for the callback's duration, so they are -/// deep-copied (and buffered results decoded) here and never freed. -/// Owned-object results (interfaces, typed handles) are the exception: the -/// callback receives ownership and the wrapper adopts the pointer. +/// the consumer, so they are deep-copied (and buffered results decoded) +/// here and then released through the runtime free symbols. Owned-object +/// results (interfaces, typed handles) transfer ownership instead: the +/// wrapper adopts the pointer. pub(crate) fn render_async_set_result(out: &mut String, ret: &Option, indent: &str) { let mut w = CodeWriter::four_space().with_depth(indent.len() / 4); if let Some(ty) = ret { @@ -806,6 +806,7 @@ pub(crate) fn render_async_set_result(out: &mut String, ret: &Option, i 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(&mut w, ty, "value", "resultBuf"); w.line("tcs.SetResult(value);"); out.push_str(&w.finish()); @@ -820,7 +821,9 @@ pub(crate) fn render_async_set_result(out: &mut String, ret: &Option, i w.line("tcs.SetResult(result != 0);"); } Some(TypeRef::StringUtf8 | TypeRef::BorrowedStr) => { - w.line("tcs.SetResult(Marshal.PtrToStringUTF8(result) ?? \"\");"); + w.line("var str = Marshal.PtrToStringUTF8(result) ?? \"\";"); + w.line("if (result != IntPtr.Zero) NativeMethods.weaveffi_free_string(result);"); + w.line("tcs.SetResult(str);"); } Some(TypeRef::Enum(name)) => { let cn = local_type_name(name); @@ -839,6 +842,7 @@ pub(crate) fn render_async_set_result(out: &mut String, ret: &Option, i 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);"); } // Only `Interface?` reaches here: a nullable owned object pointer. diff --git a/crates/weaveffi-gen-dotnet/src/lib.rs b/crates/weaveffi-gen-dotnet/src/lib.rs index d92e2e78..2dfd5125 100644 --- a/crates/weaveffi-gen-dotnet/src/lib.rs +++ b/crates/weaveffi-gen-dotnet/src/lib.rs @@ -28,12 +28,12 @@ mod types; use camino::Utf8Path; use serde::{Deserialize, Serialize}; +use weaveffi_core::resolved::ResolvedApi; use weaveffi_core::backend::{LanguageBackend, OutputFile}; use weaveffi_core::capabilities::TargetCapabilities; use weaveffi_core::model::{BindingModel, CallShape, ErrorBinding}; use weaveffi_core::package::{PackageContext, PackagedFile}; use weaveffi_core::utils::{render_prelude, render_trailer, CommentStyle}; -use weaveffi_ir::ir::Api; use crate::calls::render_wrapper_class; use crate::entities::{ @@ -121,7 +121,7 @@ impl LanguageBackend for DotnetGenerator { fn files( &self, - api: &Api, + api: &ResolvedApi, model: &BindingModel, out_dir: &Utf8Path, config: &Self::Config, @@ -161,7 +161,7 @@ impl LanguageBackend for DotnetGenerator { fn package( &self, - api: &Api, + api: &ResolvedApi, model: &BindingModel, ctx: &PackageContext, out_dir: &Utf8Path, diff --git a/crates/weaveffi-gen-dotnet/src/pinvoke.rs b/crates/weaveffi-gen-dotnet/src/pinvoke.rs index 0066c003..bfe9ea88 100644 --- a/crates/weaveffi-gen-dotnet/src/pinvoke.rs +++ b/crates/weaveffi-gen-dotnet/src/pinvoke.rs @@ -44,7 +44,8 @@ pub(crate) fn is_error_slot(slot: &AbiParam) -> bool { } /// Render the `NativeMethods` static class: the shared runtime imports -/// (`free_string`, `free_bytes`, `error_clear`) followed by every interface, +/// (`free_string`, `free_bytes`, `error_clear`, `error_free`) followed by +/// every interface, /// callback, listener, and function extern in declaration order. pub(crate) fn render_native_methods(out: &mut String, model: &BindingModel) { let mut w = CodeWriter::four_space().with_depth(1); @@ -62,6 +63,9 @@ pub(crate) fn render_native_methods(out: &mut String, model: &BindingModel) { 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 and rich enums are value types with no C symbols, so only diff --git a/crates/weaveffi-gen-dotnet/src/tests.rs b/crates/weaveffi-gen-dotnet/src/tests.rs index cd8aee8a..b07bf691 100644 --- a/crates/weaveffi-gen-dotnet/src/tests.rs +++ b/crates/weaveffi-gen-dotnet/src/tests.rs @@ -1,6 +1,8 @@ //! Output-content tests for the .NET generator: fixtures mirroring the //! sample IDLs plus assertions over the rendered C# source and manifests. +use weaveffi_ir::ir::Api; +use weaveffi_core::resolved::ResolvedApi; use super::*; use crate::types::{cs_type, pinvoke_type, safe_cs_name}; use weaveffi_core::codegen::Generator; @@ -11,7 +13,7 @@ use weaveffi_ir::ir::{ /// Test shim matching the pre-0.5.0 signature: builds the [`BindingModel`] /// here so the production `render_csharp` stays model-only. fn render_csharp( - api: &Api, + api: &ResolvedApi, namespace: &str, strip_module_prefix: bool, prefix: &str, @@ -97,13 +99,13 @@ fn package_emits_runtimes_and_rebinds_libname() { ); } -fn make_api(modules: Vec) -> Api { - Api { - version: "0.6.0".into(), +fn make_api(modules: Vec) -> ResolvedApi { + ResolvedApi::assume_resolved(Api { + version: "0.7.0".into(), modules, generators: None, package: None, - } + }) } fn simple_module(functions: Vec) -> Module { @@ -250,8 +252,8 @@ fn listeners_generate_register_unregister() { #[test] fn dotnet_record_is_plain_value_class() { - let api = Api { - version: "0.6.0".into(), + let api = ResolvedApi::assume_resolved(Api { + version: "0.7.0".into(), modules: vec![Module { name: "contacts".into(), functions: vec![], @@ -263,13 +265,11 @@ fn dotnet_record_is_plain_value_class() { name: "name".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }, StructField { name: "age".into(), ty: TypeRef::I32, doc: None, - default: None, }, ], }], @@ -282,7 +282,7 @@ fn dotnet_record_is_plain_value_class() { }], generators: None, package: None, - }; + }); let dir = tempfile::tempdir().unwrap(); let out = Utf8Path::from_path(dir.path()).unwrap(); DotnetGenerator @@ -643,13 +643,11 @@ fn struct_is_sealed_value_class_with_doc() { name: "first_name".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }, StructField { name: "age".into(), ty: TypeRef::I32, doc: None, - default: None, }, ], }], @@ -701,25 +699,21 @@ fn struct_has_property_getters() { name: "first_name".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }, StructField { name: "age".into(), ty: TypeRef::I32, doc: None, - default: None, }, StructField { name: "active".into(), ty: TypeRef::Bool, doc: None, - default: None, }, StructField { name: "role".into(), ty: TypeRef::Enum("Role".into()), doc: None, - default: None, }, ], }], @@ -789,7 +783,6 @@ fn struct_has_no_dispose_or_finalizer() { name: "id".into(), ty: TypeRef::I32, doc: None, - default: None, }], }], enums: vec![], @@ -833,13 +826,11 @@ fn struct_emits_no_pinvoke_declarations() { name: "first_name".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }, StructField { name: "age".into(), ty: TypeRef::I32, doc: None, - default: None, }, ], }], @@ -1233,7 +1224,6 @@ fn struct_optional_string_field() { name: "email".into(), ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)), doc: None, - default: None, }], }], enums: vec![], @@ -1375,25 +1365,21 @@ fn comprehensive_contacts_api() { name: "id".into(), ty: TypeRef::I64, doc: None, - default: None, }, StructField { name: "first_name".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }, StructField { name: "email".into(), ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)), doc: None, - default: None, }, StructField { name: "contact_type".into(), ty: TypeRef::Enum("ContactType".into()), doc: None, - default: None, }, ], }], @@ -1599,25 +1585,21 @@ fn generate_dotnet_with_structs() { name: "full_name".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }, StructField { name: "age".into(), ty: TypeRef::I32, doc: None, - default: None, }, StructField { name: "score".into(), ty: TypeRef::F64, doc: None, - default: None, }, StructField { name: "active".into(), ty: TypeRef::Bool, doc: None, - default: None, }, ], }], @@ -1816,25 +1798,21 @@ fn generate_dotnet_with_optionals() { name: "nickname".into(), ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)), doc: None, - default: None, }, StructField { name: "max_retries".into(), ty: TypeRef::Optional(Box::new(TypeRef::I32)), doc: None, - default: None, }, StructField { name: "threshold".into(), ty: TypeRef::Optional(Box::new(TypeRef::F64)), doc: None, - default: None, }, StructField { name: "enabled".into(), ty: TypeRef::Optional(Box::new(TypeRef::Bool)), doc: None, - default: None, }, ], }], @@ -1964,7 +1942,6 @@ fn generate_dotnet_with_lists() { name: "tags".into(), ty: TypeRef::List(Box::new(TypeRef::I32)), doc: None, - default: None, }], }], enums: vec![], @@ -2069,49 +2046,41 @@ fn generate_dotnet_full_contacts() { name: "id".into(), ty: TypeRef::Handle, doc: None, - default: None, }, StructField { name: "first_name".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }, StructField { name: "last_name".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }, StructField { name: "email".into(), ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)), doc: None, - default: None, }, StructField { name: "age".into(), ty: TypeRef::I32, doc: None, - default: None, }, StructField { name: "active".into(), ty: TypeRef::Bool, doc: None, - default: None, }, StructField { name: "contact_type".into(), ty: TypeRef::Enum("ContactType".into()), doc: None, - default: None, }, StructField { name: "scores".into(), ty: TypeRef::List(Box::new(TypeRef::I32)), doc: None, - default: None, }, ], }], @@ -2568,7 +2537,6 @@ fn dotnet_deeply_nested_optional() { name: "name".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }], }], enums: vec![], @@ -2667,7 +2635,6 @@ fn dotnet_enum_keyed_map() { name: "name".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }], }], enums: vec![EnumDef { @@ -2716,8 +2683,8 @@ fn dotnet_enum_keyed_map() { #[test] fn dotnet_typed_handle_type() { - let api = Api { - version: "0.6.0".into(), + let api = ResolvedApi::assume_resolved(Api { + version: "0.7.0".into(), modules: vec![Module { name: "contacts".into(), functions: vec![Function { @@ -2743,7 +2710,6 @@ fn dotnet_typed_handle_type() { name: "name".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }], }], enums: vec![], @@ -2755,7 +2721,7 @@ fn dotnet_typed_handle_type() { }], generators: None, package: None, - }; + }); let cs = render_csharp( &api, "WeaveFFI", @@ -2807,7 +2773,6 @@ fn dotnet_no_double_free_on_error() { name: "name".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }], }], enums: vec![], @@ -2885,7 +2850,6 @@ fn dotnet_null_check_on_optional_return() { name: "name".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }], }], enums: vec![], @@ -3058,7 +3022,7 @@ fn dotnet_async_pins_callback_for_lifetime() { /// A module with one async function per given return type, named `run0`, /// `run1`, ... in order, plus a `Contact` record for object results. -fn async_api(returns: Vec>) -> Api { +fn async_api(returns: Vec>) -> ResolvedApi { let functions = returns .into_iter() .enumerate() @@ -3084,7 +3048,6 @@ fn async_api(returns: Vec>) -> Api { name: "id".into(), ty: TypeRef::Handle, doc: None, - default: None, }], }], enums: vec![], @@ -3096,11 +3059,11 @@ fn async_api(returns: Vec>) -> Api { }]) } -/// Async result buffers are borrowed for the callback's duration -/// (`AsyncProtocol` clause 2): strings and bytes are deep-copied inside -/// the callback and never freed by the consumer. +/// Async result buffers are owned by the consumer (`AsyncProtocol` +/// clause 2): strings and bytes are deep-copied inside the callback and +/// then released through the runtime free symbols. #[test] -fn dotnet_async_borrowed_results_copied_never_freed() { +fn dotnet_async_owned_results_copied_then_freed() { let cs = render_csharp( &async_api(vec![ Some(TypeRef::StringUtf8), @@ -3113,38 +3076,39 @@ fn dotnet_async_borrowed_results_copied_never_freed() { "weaveffi.yml", "WeaveFFI.cs", ); - // String result: copied, not freed. + // String result: copied, then freed. assert!( - cs.contains("tcs.SetResult(Marshal.PtrToStringUTF8(result) ?? \"\");"), + cs.contains("var str = Marshal.PtrToStringUTF8(result) ?? \"\";") + && cs.contains("tcs.SetResult(str);"), "async string result must copy: {cs}" ); assert!( - !cs.contains("weaveffi_free_string(result)"), - "async string result must not be freed by the consumer: {cs}" + cs.contains("NativeMethods.weaveffi_free_string(result);"), + "async string result is owned and must be freed after copying: {cs}" ); - // Bytes result: copied via the (result, resultLen) pair, not freed. + // Bytes result: copied via the (result, resultLen) pair, then freed. assert!( cs.contains("Marshal.Copy(result, arr, 0, (int)resultLen);"), "async bytes result must copy: {cs}" ); assert!( - !cs.contains("weaveffi_free_bytes(result"), - "async bytes result must not be freed by the consumer: {cs}" + cs.contains("NativeMethods.weaveffi_free_bytes(result, resultLen);"), + "async bytes result is owned and must be freed after copying: {cs}" ); - // Buffered optional result: the borrowed buffer is copied and - // decoded inside the callback, never freed by the consumer. + // Buffered optional result: the owned buffer is copied, freed, and + // decoded inside the callback. assert!( cs.contains("Marshal.Copy(result, resultBuf, 0, (int)resultLen);") && cs.contains("long? value = null;") && cs.contains("var valueValue = valueReader.ReadI64();") && cs.contains("tcs.SetResult(value);"), - "async optional result must decode the borrowed buffer: {cs}" + "async optional result must decode the owned buffer: {cs}" ); } -/// Record, list, and map async results all arrive as one borrowed -/// `(result, resultLen)` value buffer: the callback copies and decodes -/// it before completing the task, and never frees it. +/// Record, list, and map async results all arrive as one owned +/// `(result, resultLen)` value buffer: the callback copies, frees, and +/// decodes it before completing the task. #[test] fn dotnet_async_buffered_results_decoded() { let cs = render_csharp( @@ -3168,7 +3132,7 @@ fn dotnet_async_buffered_results_decoded() { cs.contains("IntPtr result, UIntPtr resultLen"), "async buffered delegate must carry the length slot: {cs}" ); - // Record result decoded from the borrowed copy. + // Record result decoded from the local copy. assert!( cs.contains("var value = Contact.ReadFrom(valueReader);") && cs.contains("tcs.SetResult(value);"), @@ -3195,12 +3159,10 @@ fn dotnet_async_buffered_results_decoded() { !cs.contains("resultKeys") && !cs.contains("resultValues"), "parallel map buffers must be gone: {cs}" ); - // No release calls anywhere in this API: every native buffer here is - // an async result, borrowed for the callback's duration. + // Every owned value buffer is released after the copy. assert!( - !cs.contains("NativeMethods.weaveffi_free_string(") - && !cs.contains("NativeMethods.weaveffi_free_bytes("), - "async result buffers are borrowed and must not be freed: {cs}" + cs.contains("NativeMethods.weaveffi_free_bytes(result, resultLen);"), + "async result buffers are owned and must be freed after copying: {cs}" ); } @@ -3408,7 +3370,7 @@ fn deprecated_function_generates_annotation() { ); } -fn doc_api() -> Api { +fn doc_api() -> ResolvedApi { make_api(vec![Module { name: "docs".into(), functions: vec![Function { @@ -3434,7 +3396,6 @@ fn doc_api() -> Api { name: "id".into(), ty: TypeRef::I64, doc: Some("Stable id".into()), - default: None, }], }], enums: vec![EnumDef { @@ -3582,7 +3543,7 @@ fn dotnet_custom_prefix_threads_to_user_symbols() { ); } -fn shapes_api() -> Api { +fn shapes_api() -> ResolvedApi { let shape = EnumDef { name: "Shape".into(), doc: Some("An algebraic shape".into()), @@ -3601,7 +3562,6 @@ fn shapes_api() -> Api { name: "radius".into(), ty: TypeRef::F64, doc: None, - default: None, }], }, EnumVariant { @@ -3613,13 +3573,11 @@ fn shapes_api() -> Api { name: "width".into(), ty: TypeRef::F32, doc: None, - default: None, }, StructField { name: "height".into(), ty: TypeRef::F32, doc: None, - default: None, }, ], }, @@ -3632,13 +3590,11 @@ fn shapes_api() -> Api { name: "label".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }, StructField { name: "count".into(), ty: TypeRef::U8, doc: None, - default: None, }, ], }, @@ -3837,7 +3793,7 @@ fn rich_enum_generates_sum_type() { /// A `kv` module exercising the 0.5.0 surface: a declared error domain, a /// `Store` interface (real ctor, named factory, sync/iterator/async /// methods, a static), and free functions with mixed `throws`. -fn kv_api() -> Api { +fn kv_api() -> ResolvedApi { use weaveffi_ir::ir::{ErrorCode, ErrorDomain, InterfaceDef}; let f = |name: &str, params: Vec, @@ -3943,13 +3899,11 @@ fn kv_api() -> Api { name: "key".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }, StructField { name: "attempts".into(), ty: TypeRef::I32, doc: None, - default: None, }, ], }, @@ -4312,8 +4266,8 @@ fn render_fixture(name: &str) -> String { .join(name); let text = std::fs::read_to_string(&path) .unwrap_or_else(|e| panic!("failed to read fixture {}: {e}", path.display())); - let mut api = weaveffi_ir::parse::parse_api_str(&text, "yml").expect("fixture must parse"); - weaveffi_core::validate::validate_api(&mut api, None).expect("fixture must validate"); + let api = weaveffi_ir::parse::parse_api_str(&text, "yml").expect("fixture must parse"); + let api = weaveffi_core::validate::validate_api(api, None).expect("fixture must validate"); render_csharp(&api, "WeaveFFI", true, "weaveffi", name, "WeaveFFI.cs") } @@ -4478,13 +4432,11 @@ fn keyword_identifiers_are_escaped() { name: "default".into(), ty: TypeRef::I32, doc: None, - default: None, }, StructField { name: "normal".into(), ty: TypeRef::Bool, doc: None, - default: None, }, ], }); @@ -4587,7 +4539,7 @@ fn error_mapping_reserves_negative_codes_for_runtime() { #[test] fn manifest_metadata_is_xml_escaped() { use weaveffi_ir::ir::Package; - let mut api = make_api(vec![simple_module(vec![])]); + let mut api = make_api(vec![simple_module(vec![])]).api().clone(); api.package = Some(Package { name: "my-kv".into(), version: "1.2.3".into(), @@ -4597,6 +4549,7 @@ fn manifest_metadata_is_xml_escaped() { homepage: Some("https://example.com/?a=1&b=2".into()), repository: None, }); + let api = ResolvedApi::assume_resolved(api); let config = DotnetConfig::default(); let package = crate::package::resolve_dotnet_package(&api, &config); let csproj = crate::package::render_csproj(&package, "weaveffi.yml", "WeaveFFI.csproj"); diff --git a/crates/weaveffi-gen-go/src/calls.rs b/crates/weaveffi-gen-go/src/calls.rs index 188c0c94..93d6f494 100644 --- a/crates/weaveffi-gen-go/src/calls.rs +++ b/crates/weaveffi-gen-go/src/calls.rs @@ -363,12 +363,10 @@ fn async_outcome_type(prefix: &str, f: &FnBinding) -> String { /// 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 borrowed for the -/// callback's duration per the shared async protocol: they are decoded or -/// deep copied here and never freed (the producer releases them after the -/// callback returns). Owned interface results are the exception: the callback -/// receives ownership and the wrapper adopts the pointer (its `Close` calls -/// the destroy symbol). +/// 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. Owned interface results are +/// adopted by the wrapper instead (its `Close` calls the destroy symbol). fn emit_async_result_send( out: &mut String, ret: &Option, @@ -385,26 +383,28 @@ fn emit_async_result_send( match plan::ret_pass(Some(ty), module, prefix) { RetPass::Void => unreachable!("a present return type is never void"), RetPass::Buffer => { - // Borrowed for the callback's duration: decode, do not free. - w.line("rRes := &wvReader{buf: wvBorrowBuffer(result_ptr, result_len)}"); + // 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, prefix, module); w.line("rRes.expectEnd()"); w.line(format!("ch <- {outcome}{{val: val}}")); } RetPass::String => { - // Borrowed for the callback's duration: copy, do not free. + // 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 => { - // Borrowed for the callback's duration: copy, do not free. + // 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}}")); } @@ -504,9 +504,9 @@ pub(crate) fn render_async_function( // 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("wvTakeError(err)") + err.map_call("wvTakeBoxedError(err)") } else { - "wvBrandError(wvTakeError(err))".to_string() + "wvBrandError(wvTakeBoxedError(err))".to_string() }; w.line(format!("//export {tramp}")); w.block( diff --git a/crates/weaveffi-gen-go/src/lib.rs b/crates/weaveffi-gen-go/src/lib.rs index 9a25c9b3..4b819897 100644 --- a/crates/weaveffi-gen-go/src/lib.rs +++ b/crates/weaveffi-gen-go/src/lib.rs @@ -27,6 +27,7 @@ mod types; use camino::Utf8Path; use heck::ToUpperCamelCase; use serde::{Deserialize, Serialize}; +use weaveffi_core::resolved::ResolvedApi; use weaveffi_core::abi::is_buffered; use weaveffi_core::backend::{LanguageBackend, OutputFile}; use weaveffi_core::capabilities::TargetCapabilities; @@ -35,7 +36,7 @@ use weaveffi_core::package::{PackageContext, PackagedFile}; use weaveffi_core::pkg; use weaveffi_core::plan::{elem_free, ElemFree}; use weaveffi_core::utils::{render_prelude, render_trailer, wrapper_name, CommentStyle}; -use weaveffi_ir::ir::{Api, TypeRef}; +use weaveffi_ir::ir::TypeRef; use crate::calls::{ collect_trampoline_externs, render_async_function, render_callback_trampoline, render_function, @@ -121,7 +122,7 @@ impl LanguageBackend for GoGenerator { fn files( &self, - api: &Api, + api: &ResolvedApi, model: &BindingModel, out_dir: &Utf8Path, config: &Self::Config, @@ -157,7 +158,7 @@ impl LanguageBackend for GoGenerator { fn package( &self, - api: &Api, + api: &ResolvedApi, model: &BindingModel, ctx: &PackageContext, out_dir: &Utf8Path, @@ -274,7 +275,7 @@ fn scan_imports(model: &BindingModel) -> Imports { /// Render the complete generated Go source file: the cgo preamble, imports, /// runtime prelude, and every module's entities and wrappers. pub(crate) fn render_go( - api: &Api, + api: &ResolvedApi, model: &BindingModel, prefix: &str, strip_module_prefix: bool, diff --git a/crates/weaveffi-gen-go/src/package.rs b/crates/weaveffi-gen-go/src/package.rs index bce5cbad..edb40fef 100644 --- a/crates/weaveffi-gen-go/src/package.rs +++ b/crates/weaveffi-gen-go/src/package.rs @@ -2,12 +2,12 @@ //! bundling per-platform shared libraries with a rewritten cgo preamble. use camino::Utf8Path; +use weaveffi_core::resolved::ResolvedApi; use weaveffi_core::model::BindingModel; use weaveffi_core::package::{PackageContext, PackagedFile}; use weaveffi_core::pkg; use weaveffi_core::platform::Platform; use weaveffi_core::utils::{render_prelude, render_trailer, CommentStyle}; -use weaveffi_ir::ir::Api; use crate::{render_go, GoConfig}; @@ -112,7 +112,7 @@ alongside Go, for example `weaveffi package --target c,go`). /// preamble, `go.mod`, the packaged README, and one bundled shared library /// per platform. pub(crate) fn package_files( - api: &Api, + api: &ResolvedApi, model: &BindingModel, ctx: &PackageContext, out_dir: &Utf8Path, diff --git a/crates/weaveffi-gen-go/src/runtime.rs b/crates/weaveffi-gen-go/src/runtime.rs index 224b4ce0..af8e0ec6 100644 --- a/crates/weaveffi-gen-go/src/runtime.rs +++ b/crates/weaveffi-gen-go/src/runtime.rs @@ -70,6 +70,29 @@ pub(crate) fn render_error_infra(out: &mut String) { ); 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 {", "}", @@ -96,9 +119,9 @@ pub(crate) fn render_error_infra(out: &mut String) { /// 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 returns released with -/// `weaveffi_free_bytes`, `wvBorrowBuffer` for borrowed callback/async -/// buffers the producer frees). +/// 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 diff --git a/crates/weaveffi-gen-go/src/tests.rs b/crates/weaveffi-gen-go/src/tests.rs index 9bf13914..8be6c067 100644 --- a/crates/weaveffi-gen-go/src/tests.rs +++ b/crates/weaveffi-gen-go/src/tests.rs @@ -1,6 +1,7 @@ //! Output-content tests for the Go generator: fixtures mirroring the //! sample IDLs plus assertions over the rendered source. +use weaveffi_core::resolved::ResolvedApi; use camino::Utf8Path; use weaveffi_core::codegen::Generator; use weaveffi_core::errors::ERROR_BRAND; @@ -13,13 +14,13 @@ use super::*; // ── Fixture helpers ── -fn api_of(modules: Vec) -> Api { - Api { - version: "0.6.0".into(), +fn api_of(modules: Vec) -> ResolvedApi { + ResolvedApi::assume_resolved(Api { + version: "0.7.0".into(), modules, generators: None, package: None, - } + }) } fn module(name: &str) -> Module { @@ -69,7 +70,6 @@ fn field(name: &str, ty: TypeRef) -> StructField { name: name.into(), ty, doc: None, - default: None, } } @@ -93,16 +93,16 @@ fn variant(name: &str, value: i32, fields: Vec) -> EnumVariant { } /// Render with the default surface: `weaveffi` prefix, stripping on. -fn rg(api: &Api) -> String { +fn rg(api: &ResolvedApi) -> String { rg_with(api, "weaveffi", true) } -fn rg_with(api: &Api, prefix: &str, strip: bool) -> String { +fn rg_with(api: &ResolvedApi, prefix: &str, strip: bool) -> String { let model = BindingModel::build(api, prefix); render_go(api, &model, prefix, strip, "weaveffi.yml") } -fn calculator_api() -> Api { +fn calculator_api() -> ResolvedApi { let mut m = module("calculator"); m.functions = vec![ func_of( @@ -123,7 +123,7 @@ fn calculator_api() -> Api { /// sync/async/iterator methods, a static), the `KvError` domain, the /// `Entry` record, the eviction listener, and the nested `kv.stats` /// submodule taking a cross-module interface parameter. -fn kv_api() -> Api { +fn kv_api() -> ResolvedApi { let mut stats = module("stats"); stats.structs = vec![StructDef { name: "Stats".into(), @@ -239,7 +239,7 @@ fn kv_api() -> Api { /// Mirrors `samples/contacts/contacts.yml`, standing in for the CLI test /// (`cli_go.rs`) while the workspace binary is blocked on other generator /// crates mid-overhaul. -fn contacts_api() -> Api { +fn contacts_api() -> ResolvedApi { let mut m = module("contacts"); m.enums = vec![EnumDef { name: "ContactType".into(), @@ -307,7 +307,7 @@ fn contacts_api() -> Api { /// A module with one rich (algebraic) enum used across params and /// returns. -fn shapes_api() -> Api { +fn shapes_api() -> ResolvedApi { let mut m = module("shapes"); m.enums = vec![EnumDef { name: "Shape".into(), @@ -906,7 +906,7 @@ fn optional_scalar_return_decodes_buffer() { // ── Throwing functions ── -fn store_api() -> Api { +fn store_api() -> ResolvedApi { let mut m = module("store"); m.errors = Some(ErrorDomain { name: "StoreError".into(), @@ -1517,17 +1517,17 @@ fn go_async_generates_blocking_wrapper() { "plain async wrapper must panic on a reported error: {go}" ); assert!( - go.contains("ch <- wvOutcomeIoRead{err: wvBrandError(wvTakeError(err))}"), + go.contains("ch <- wvOutcomeIoRead{err: wvBrandError(wvTakeBoxedError(err))}"), "plain async trampoline brands the error, never the domain: {go}" ); assert!( go.contains("return outcome.val"), "plain async wrapper returns the outcome value: {go}" ); - // The completion callback borrows its result buffers: copy, no free. + // The completion callback owns its result buffers: copy, then free. assert!( - !go.contains("C.weaveffi_free_string(result)"), - "borrowed async result buffers must not be freed: {go}" + go.contains("C.weaveffi_free_string(result)"), + "owned async result buffers must be freed after copying: {go}" ); assert!( go.contains("val = C.GoString(result)"), @@ -1565,7 +1565,7 @@ fn async_cancellable_passes_null_token() { } #[test] -fn async_record_result_decodes_borrowed_buffer() { +fn async_record_result_decodes_owned_buffer() { let mut m = module("metrics"); m.structs = vec![StructDef { name: "Stats".into(), @@ -1585,17 +1585,13 @@ fn async_record_result_decodes_borrowed_buffer() { "buffered async callback carries borrowed ptr + len slots: {go}" ); assert!( - go.contains("rRes := &wvReader{buf: wvBorrowBuffer(result_ptr, result_len)}"), - "async result buffer is borrowed, never freed: {go}" + go.contains("rRes := &wvReader{buf: wvCopyBuffer(result_ptr, result_len)}"), + "async result buffer is owned: copied, then freed: {go}" ); assert!( go.contains("val = wvUnpackStats(rRes)"), "async record result decodes inside the trampoline: {go}" ); - assert!( - !go.contains("wvCopyBuffer(result_ptr"), - "the producer frees the async result buffer, not the consumer: {go}" - ); assert!( go.contains("func Load() Stats {"), "async record wrapper returns the value struct: {go}" @@ -1858,7 +1854,7 @@ fn interface_async_method_throws() { "launch passes s.ptr then the NULL cancel token: {go}" ); assert!( - go.contains("ch <- wvOutcomeKvStoreCompact{err: wvMapKv(wvTakeError(err))}"), + go.contains("ch <- wvOutcomeKvStoreCompact{err: wvMapKv(wvTakeBoxedError(err))}"), "trampoline maps the domain error: {go}" ); assert!( @@ -2379,7 +2375,7 @@ fn string_list_return_decodes_from_buffer() { // ── Docs ── -fn doc_api() -> Api { +fn doc_api() -> ResolvedApi { let mut m = module("docs"); m.functions = vec![Function { name: "do_thing".into(), @@ -2404,7 +2400,6 @@ fn doc_api() -> Api { name: "id".into(), ty: TypeRef::I64, doc: Some("Stable id".into()), - default: None, }], }]; m.enums = vec![EnumDef { diff --git a/crates/weaveffi-gen-node/src/addon.rs b/crates/weaveffi-gen-node/src/addon.rs index af4f5182..e1c619f5 100644 --- a/crates/weaveffi-gen-node/src/addon.rs +++ b/crates/weaveffi-gen-node/src/addon.rs @@ -902,24 +902,29 @@ fn render_async_machinery( 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(" weaveffi_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(" weaveffi_free_bytes((uint8_t*)result, result_len);\n"); } - // The buffer is borrowed for the callback's duration, so the bytes - // are copied here; the JS wrapper decodes them after the promise + // 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(" weaveffi_free_bytes((uint8_t*)result_ptr, result_len);\n"); } // Owned-object results (interfaces, typed handles, iterators) are // adopted by the receiver, so the pointer stays valid across the @@ -929,6 +934,9 @@ fn render_async_machinery( } } 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(" weaveffi_error_free(err);\n"); out.push_str(" napi_call_threadsafe_function(ctx->tsfn, ctx, napi_tsfn_blocking);\n"); out.push_str("}\n\n"); diff --git a/crates/weaveffi-gen-node/src/lib.rs b/crates/weaveffi-gen-node/src/lib.rs index a71c79ad..fce4cb69 100644 --- a/crates/weaveffi-gen-node/src/lib.rs +++ b/crates/weaveffi-gen-node/src/lib.rs @@ -35,12 +35,12 @@ mod tests; use camino::Utf8Path; use serde::{Deserialize, Serialize}; +use weaveffi_core::resolved::ResolvedApi; 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_ir::ir::Api; use crate::addon::render_addon_c; use crate::entities::render_node_index; @@ -120,7 +120,7 @@ impl LanguageBackend for NodeGenerator { fn files( &self, - api: &Api, + api: &ResolvedApi, model: &BindingModel, out_dir: &Utf8Path, config: &Self::Config, @@ -158,7 +158,7 @@ impl LanguageBackend for NodeGenerator { fn package( &self, - api: &Api, + api: &ResolvedApi, model: &BindingModel, ctx: &PackageContext, out_dir: &Utf8Path, diff --git a/crates/weaveffi-gen-node/src/tests.rs b/crates/weaveffi-gen-node/src/tests.rs index d041b2bd..4de1cb3f 100644 --- a/crates/weaveffi-gen-node/src/tests.rs +++ b/crates/weaveffi-gen-node/src/tests.rs @@ -1,5 +1,6 @@ //! Unit tests: golden-content assertions over every emitted file. +use weaveffi_core::resolved::ResolvedApi; use super::*; use crate::addon::render_addon_c; use crate::entities::render_node_index; @@ -64,13 +65,13 @@ fn package_uses_optional_dependencies_per_platform() { ); } -fn make_api(modules: Vec) -> Api { - Api { - version: "0.6.0".into(), +fn make_api(modules: Vec) -> ResolvedApi { + ResolvedApi::assume_resolved(Api { + version: "0.7.0".into(), modules, generators: None, package: None, - } + }) } fn make_module(name: &str) -> Module { @@ -92,7 +93,6 @@ fn field(name: &str, ty: TypeRef) -> StructField { name: name.into(), ty, doc: None, - default: None, } } @@ -133,19 +133,19 @@ fn contact_struct() -> StructDef { /// Test-only bridge from an inline [`Api`] literal to the model the /// production path receives from the driver. -fn build_model(api: &Api) -> BindingModel { +fn build_model(api: &ResolvedApi) -> BindingModel { BindingModel::build(api, "weaveffi") } -fn index_for(api: &Api, strip: bool) -> String { +fn index_for(api: &ResolvedApi, strip: bool) -> String { render_node_index(&build_model(api), strip, "weaveffi.yml") } -fn dts_for(api: &Api, strip: bool) -> String { +fn dts_for(api: &ResolvedApi, strip: bool) -> String { render_node_dts(&build_model(api), strip, "weaveffi.yml") } -fn addon_for(api: &Api, strip: bool) -> String { +fn addon_for(api: &ResolvedApi, strip: bool) -> String { render_addon_c(&build_model(api), strip, "weaveffi.yml") } @@ -1231,7 +1231,6 @@ fn doc_module() -> Module { name: "id".into(), ty: TypeRef::I64, doc: Some("Stable id".into()), - default: None, }], }], enums: vec![EnumDef { diff --git a/crates/weaveffi-gen-python/src/calls.rs b/crates/weaveffi-gen-python/src/calls.rs index 1ef21345..b82e7b23 100644 --- a/crates/weaveffi-gen-python/src/calls.rs +++ b/crates/weaveffi-gen-python/src/calls.rs @@ -20,7 +20,8 @@ use weaveffi_core::wire::{self, WireType}; use weaveffi_ir::ir::TypeRef; use crate::codec::{ - py_decode_borrowed_expr, py_pack_fn_name, py_read_expr, py_unpack_fn_name, py_write_stmts, + 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_doc, emit_docstring, emit_fn_docstring}; use crate::entities::{py_checker_name, py_error_factory_name}; @@ -472,45 +473,56 @@ fn py_async_cb_trailing_fields(ret: &Option) -> Vec<(String, String)> { None => vec![], Some(ty) => abi::callback_result_params(ty, "") .into_iter() - .map(|p| (p.name, py_ctype(&p.ty))) + .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 -/// borrowed `result` slots into the idiomatic value and store it in -/// `_state["val"]`. Borrowed buffers (strings, bytes, value buffers) are -/// copied and never freed; the producer releases them after the callback -/// returns. An owned interface pointer is adopted by its wrapper class. +/// 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 borrowed `(result_ptr, result_len)` pair holds one encoded - // value; copy and decode it before the producer reclaims it. + // 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_borrowed_expr("result_ptr", "result_len", ty) + py_decode_owned_expr("result_ptr", "result_len", ty) )); } RetPass::String => { - // `result` arrives as `bytes` (ctypes copies `c_char_p` callback - // arguments), so decoding is already a deep copy of the borrowed - // producer buffer. The producer frees it; the wrapper must not. + // 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\"] = _bytes_to_string(result) or \"\"\n" + "{ind}_state[\"val\"] = _take_string(result) or \"\"\n" )); } RetPass::Bytes => { - // Copy the borrowed buffer; the producer owns and frees it. + // 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\"] = bytes(result[:_n])\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 @@ -563,9 +575,10 @@ fn append_async_success_handler(out: &mut String, ret: &Option, ind: &s /// `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 copies borrowed result buffers before -/// returning (owned object pointers are adopted instead), then resolves the -/// future via `call_soon_threadsafe`. A throwing callable maps the completion +/// 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 @@ -604,10 +617,10 @@ fn render_async_ffi_call_body( 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: convert (copying\n" + "{ind} # Fires exactly once, on a producer thread: take ownership of\n" )); out.push_str(&format!( - "{ind} # borrowed buffers) here, then hop back to the event loop.\n" + "{ind} # the result here, then hop back to the event loop.\n" )); out.push_str(&format!( "{ind} _state = {{\"err\": None, \"val\": None}}\n" @@ -623,7 +636,7 @@ fn render_async_ffi_call_body( )); } out.push_str(&format!( - "{ind} _lib.weaveffi_error_clear(ctypes.byref(err.contents))\n" + "{ind} _lib.weaveffi_error_free(err)\n" )); out.push_str(&format!("{ind} _state[\"err\"] = {err_expr}\n")); out.push_str(&format!("{ind} else:\n")); diff --git a/crates/weaveffi-gen-python/src/codec.rs b/crates/weaveffi-gen-python/src/codec.rs index 7f4467b5..57b242db 100644 --- a/crates/weaveffi-gen-python/src/codec.rs +++ b/crates/weaveffi-gen-python/src/codec.rs @@ -181,6 +181,18 @@ pub(crate) fn py_decode_borrowed_expr(ptr: &str, len: &str, ty: &TypeRef) -> Str } } +/// 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: &TypeRef) -> String { + let data = format!("_take_buffer(ctypes.cast({ptr}, ctypes.c_void_p).value, {len})"); + match wire::classify(ty) { + 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) { diff --git a/crates/weaveffi-gen-python/src/entities.rs b/crates/weaveffi-gen-python/src/entities.rs index bfb9c38f..cb6d370b 100644 --- a/crates/weaveffi-gen-python/src/entities.rs +++ b/crates/weaveffi-gen-python/src/entities.rs @@ -353,21 +353,10 @@ pub(crate) fn render_struct(out: &mut String, s: &StructBinding) { out.push_str(&w.finish()); } -/// Emit dataclass field lines (`name: hint` or `name: hint = default`), with -/// field docs as leading comments. Declared defaults apply only to the -/// longest trailing run of fields that all have representable literals, so a -/// defaulted field never precedes a required one (which Python rejects). +/// Emit dataclass field lines (`name: hint`), with field docs as leading +/// comments. fn render_dataclass_fields(w: &mut CodeWriter, fields: &[FieldBinding]) { - let literals: Vec> = fields - .iter() - .map(|f| { - f.default - .as_ref() - .and_then(|d| py_default_literal(&f.ty, d)) - }) - .collect(); - let start = py_default_suffix_start(fields); - for (i, f) in fields.iter().enumerate() { + for f in fields { if let Some(d) = &f.doc { let trimmed = d.trim(); if !trimmed.is_empty() { @@ -376,66 +365,7 @@ fn render_dataclass_fields(w: &mut CodeWriter, fields: &[FieldBinding]) { } } } - let hint = py_type_hint(&f.ty); - match &literals[i] { - Some(lit) if i >= start => { - w.line(format!("{}: {} = {}", py_field(&f.name), hint, lit)); - } - _ => { - w.line(format!("{}: {}", py_field(&f.name), hint)); - } - } - } -} - -/// The index where the trailing run of defaulted dataclass fields starts: -/// every field at or past it carries a representable default literal, so -/// only that suffix receives defaults (Python rejects a defaulted field -/// ahead of a required one). -pub(crate) fn py_default_suffix_start(fields: &[FieldBinding]) -> usize { - let mut start = fields.len(); - while start > 0 { - let f = &fields[start - 1]; - let has_literal = f - .default - .as_ref() - .and_then(|d| py_default_literal(&f.ty, d)) - .is_some(); - if !has_literal { - break; - } - start -= 1; - } - start -} - -/// The Python literal for one declared field default, or `None` when the -/// value has no simple literal spelling (in which case the dataclass field -/// stays required). -fn py_default_literal(ty: &TypeRef, value: &serde_yaml::Value) -> Option { - match (ty, value) { - (TypeRef::Bool, serde_yaml::Value::Bool(b)) => { - Some(if *b { "True" } else { "False" }.to_string()) - } - ( - TypeRef::I8 - | TypeRef::I16 - | TypeRef::I32 - | TypeRef::U8 - | TypeRef::U16 - | TypeRef::U32 - | TypeRef::I64 - | TypeRef::U64 - | TypeRef::F32 - | TypeRef::F64 - | TypeRef::Handle, - serde_yaml::Value::Number(n), - ) => Some(n.to_string()), - (TypeRef::StringUtf8 | TypeRef::BorrowedStr, serde_yaml::Value::String(s)) => { - Some(format!("\"{}\"", py_str_literal(s))) - } - (TypeRef::Optional(_), serde_yaml::Value::Null) => Some("None".to_string()), - _ => None, + w.line(format!("{}: {}", py_field(&f.name), py_type_hint(&f.ty))); } } diff --git a/crates/weaveffi-gen-python/src/lib.rs b/crates/weaveffi-gen-python/src/lib.rs index ae32dc29..426e331f 100644 --- a/crates/weaveffi-gen-python/src/lib.rs +++ b/crates/weaveffi-gen-python/src/lib.rs @@ -23,6 +23,7 @@ mod tests; use camino::Utf8Path; use serde::{Deserialize, Serialize}; +use weaveffi_core::resolved::ResolvedApi; use weaveffi_core::backend::{LanguageBackend, OutputFile}; use weaveffi_core::capabilities::TargetCapabilities; use weaveffi_core::model::{ @@ -32,7 +33,6 @@ use weaveffi_core::model::{ use weaveffi_core::package::{PackageContext, PackagedFile}; use weaveffi_core::pkg; use weaveffi_core::utils::{render_prelude, render_trailer, CommentStyle}; -use weaveffi_ir::ir::Api; use crate::calls::{render_callable, render_callback_type, render_listener, FnScope}; use crate::entities::{render_enum, render_error, render_interface, render_struct}; @@ -249,7 +249,7 @@ impl LanguageBackend for PythonGenerator { fn files( &self, - api: &Api, + api: &ResolvedApi, model: &BindingModel, out_dir: &Utf8Path, config: &Self::Config, @@ -298,7 +298,7 @@ impl LanguageBackend for PythonGenerator { fn package( &self, - api: &Api, + api: &ResolvedApi, model: &BindingModel, ctx: &PackageContext, out_dir: &Utf8Path, @@ -373,7 +373,7 @@ weaveffi_core::impl_generator_via_backend!(PythonGenerator); /// [`PythonGenerator::render_py_source`]); retained for direct use in tests. #[cfg(test)] fn render_python_module( - api: &Api, + api: &ResolvedApi, strip_module_prefix: bool, prefix: &str, input_basename: &str, diff --git a/crates/weaveffi-gen-python/src/runtime.rs b/crates/weaveffi-gen-python/src/runtime.rs index 4a7049b3..7d5cdb4e 100644 --- a/crates/weaveffi-gen-python/src/runtime.rs +++ b/crates/weaveffi-gen-python/src/runtime.rs @@ -52,6 +52,10 @@ def _load_library() -> ctypes.CDLL: _lib = _load_library() _lib.weaveffi_error_clear.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] _lib.weaveffi_error_clear.restype = None +# Async completion callbacks receive a heap-boxed error the consumer owns; +# weaveffi_error_free releases the message, the payload, and the box. +_lib.weaveffi_error_free.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] +_lib.weaveffi_error_free.restype = None # The free helpers take raw addresses (`c_void_p`) so wrappers can release # owned producer allocations they hold as plain integers or typed pointers. _lib.weaveffi_free_string.argtypes = [ctypes.c_void_p] diff --git a/crates/weaveffi-gen-python/src/stubs.rs b/crates/weaveffi-gen-python/src/stubs.rs index a6913f5e..46f2b2ff 100644 --- a/crates/weaveffi-gen-python/src/stubs.rs +++ b/crates/weaveffi-gen-python/src/stubs.rs @@ -8,7 +8,7 @@ use weaveffi_core::model::{ use weaveffi_core::utils::{render_prelude, render_trailer, CommentStyle}; use crate::docs::emit_doc; -use crate::entities::{py_code_class_name, py_default_suffix_start}; +use crate::entities::py_code_class_name; use crate::types::{ py_callable_hint, py_field, py_member_name, py_name, py_type_hint, py_wrapper_fn_name, }; @@ -209,8 +209,7 @@ fn render_pyi_rich_enum(out: &mut String, e: &EnumBinding) { /// `.pyi` stub for a record: a dataclass-shaped value class with typed field /// attributes and the generated constructor, mirroring -/// [`crate::entities::render_struct`]. Declared defaults surface as `= ...` -/// on the trailing constructor parameters that carry them. +/// [`crate::entities::render_struct`]. fn render_pyi_struct(out: &mut String, s: &StructBinding) { out.push('\n'); emit_doc(out, &s.doc, ""); @@ -220,16 +219,12 @@ fn render_pyi_struct(out: &mut String, s: &StructBinding) { emit_doc(out, &field.doc, " "); out.push_str(&format!(" {}: {}\n", py_field(&field.name), py_ty)); } - let start = py_default_suffix_start(&s.fields); let mut params = vec!["self".to_string()]; - params.extend(s.fields.iter().enumerate().map(|(i, f)| { - let hint = py_type_hint(&f.ty); - if i >= start { - format!("{}: {} = ...", py_field(&f.name), hint) - } else { - format!("{}: {}", py_field(&f.name), hint) - } - })); + 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(", ") diff --git a/crates/weaveffi-gen-python/src/tests.rs b/crates/weaveffi-gen-python/src/tests.rs index 50d621da..21fc09e6 100644 --- a/crates/weaveffi-gen-python/src/tests.rs +++ b/crates/weaveffi-gen-python/src/tests.rs @@ -2,6 +2,7 @@ //! generated source text and are the byte-compatibility guard for the //! crate. +use weaveffi_core::resolved::ResolvedApi; use super::*; use crate::stubs::render_pyi_module; use crate::types::{py_ctypes_scalar, py_type_hint}; @@ -11,13 +12,13 @@ use weaveffi_ir::ir::{ Api, EnumDef, EnumVariant, Function, Module, Param, StructDef, StructField, TypeRef, }; -fn make_api(modules: Vec) -> Api { - Api { - version: "0.6.0".into(), +fn make_api(modules: Vec) -> ResolvedApi { + ResolvedApi::assume_resolved(Api { + version: "0.7.0".into(), modules, generators: None, package: None, - } + }) } fn simple_module(functions: Vec) -> Module { @@ -39,7 +40,7 @@ fn generator_name_is_python() { assert_eq!(Generator::name(&PythonGenerator), "python"); } -fn ping_api() -> Api { +fn ping_api() -> ResolvedApi { make_api(vec![simple_module(vec![Function { name: "ping".into(), params: vec![], @@ -78,7 +79,6 @@ fn python_keywords_escape_with_trailing_underscore() { name: "from".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }], }]; let api = make_api(vec![module]); @@ -505,13 +505,11 @@ fn struct_dataclass_value_type() { name: "name".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }, StructField { name: "age".into(), ty: TypeRef::I32, doc: None, - default: None, }, ], }], @@ -578,8 +576,8 @@ fn struct_dataclass_value_type() { #[test] fn buffered_record_param_packs() { - let api = Api { - version: "0.6.0".into(), + let api = ResolvedApi::assume_resolved(Api { + version: "0.7.0".into(), modules: vec![Module { name: "contacts".into(), functions: vec![Function { @@ -606,13 +604,11 @@ fn buffered_record_param_packs() { name: "name".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }, StructField { name: "age".into(), ty: TypeRef::I32, doc: None, - default: None, }, ], }], @@ -625,7 +621,7 @@ fn buffered_record_param_packs() { }], generators: None, package: None, - }; + }); let dir = tempfile::tempdir().unwrap(); let out = Utf8Path::from_path(dir.path()).unwrap(); PythonGenerator @@ -1087,7 +1083,6 @@ fn struct_optional_string_field() { name: "email".into(), ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)), doc: None, - default: None, }], }], enums: vec![], @@ -1134,7 +1129,6 @@ fn struct_enum_field() { name: "role".into(), ty: TypeRef::Enum("Role".into()), doc: None, - default: None, }], }], enums: vec![], @@ -1193,25 +1187,21 @@ fn comprehensive_contacts_api() { name: "id".into(), ty: TypeRef::I64, doc: None, - default: None, }, StructField { name: "first_name".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }, StructField { name: "email".into(), ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)), doc: None, - default: None, }, StructField { name: "contact_type".into(), ty: TypeRef::Enum("ContactType".into()), doc: None, - default: None, }, ], }], @@ -1444,7 +1434,6 @@ fn struct_bytes_field() { name: "data".into(), ty: TypeRef::Bytes, doc: None, - default: None, }], }], enums: vec![], @@ -1502,31 +1491,26 @@ fn python_generates_type_stubs() { name: "id".into(), ty: TypeRef::I64, doc: None, - default: None, }, StructField { name: "name".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }, StructField { name: "email".into(), ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)), doc: None, - default: None, }, StructField { name: "tags".into(), ty: TypeRef::List(Box::new(TypeRef::StringUtf8)), doc: None, - default: None, }, StructField { name: "metadata".into(), ty: TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::I32)), doc: None, - default: None, }, ], }], @@ -1748,25 +1732,21 @@ fn generate_python_with_structs() { name: "id".into(), ty: TypeRef::I64, doc: None, - default: None, }, StructField { name: "first_name".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }, StructField { name: "last_name".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }, StructField { name: "email".into(), ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)), doc: None, - default: None, }, ], }], @@ -2221,31 +2201,26 @@ fn generate_python_pyi_types() { name: "id".into(), ty: TypeRef::I64, doc: None, - default: None, }, StructField { name: "first_name".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }, StructField { name: "email".into(), ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)), doc: None, - default: None, }, StructField { name: "tags".into(), ty: TypeRef::List(Box::new(TypeRef::StringUtf8)), doc: None, - default: None, }, StructField { name: "scores".into(), ty: TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::I32)), doc: None, - default: None, }, ], }], @@ -2388,31 +2363,26 @@ fn generate_python_full_contacts() { name: "id".into(), ty: TypeRef::I64, doc: None, - default: None, }, StructField { name: "first_name".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }, StructField { name: "last_name".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }, StructField { name: "email".into(), ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)), doc: None, - default: None, }, StructField { name: "contact_type".into(), ty: TypeRef::Enum("ContactType".into()), doc: None, - default: None, }, ], }], @@ -2886,7 +2856,6 @@ fn python_deeply_nested_optional() { name: "name".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }], }], enums: vec![], @@ -2971,7 +2940,6 @@ fn python_enum_keyed_map() { name: "name".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }], }], enums: vec![EnumDef { @@ -3013,8 +2981,8 @@ fn python_enum_keyed_map() { #[test] fn python_typed_handle_type() { - let api = Api { - version: "0.6.0".into(), + let api = ResolvedApi::assume_resolved(Api { + version: "0.7.0".into(), modules: vec![Module { name: "contacts".into(), functions: vec![Function { @@ -3040,7 +3008,6 @@ fn python_typed_handle_type() { name: "name".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }], }], enums: vec![], @@ -3052,7 +3019,7 @@ fn python_typed_handle_type() { }], generators: None, package: None, - }; + }); let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml"); // A typed handle is an opaque token: hinted as int, passed raw. assert!( @@ -3100,7 +3067,6 @@ fn python_no_double_free_on_error() { name: "name".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }], }], enums: vec![], @@ -3203,7 +3169,6 @@ fn python_null_check_on_optional_return() { name: "name".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }], }], enums: vec![], @@ -3268,10 +3233,12 @@ fn python_async_function_is_async_def() { code.contains("_fut = _loop.create_future()"), "should create a future: {code}" ); + // The string result slot is `c_void_p` (a `c_char_p` slot would + // auto-convert to `bytes` and lose the pointer `_take_string` frees). assert!( code.contains( "_cb_type = ctypes.CFUNCTYPE(None, ctypes.c_void_p, \ - ctypes.POINTER(_WeaveFFIErrorStruct), ctypes.c_char_p)" + ctypes.POINTER(_WeaveFFIErrorStruct), ctypes.c_void_p)" ), "should build the CFUNCTYPE trampoline: {code}" ); @@ -3497,7 +3464,6 @@ fn python_cross_module_struct() { name: "value".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }], }], enums: vec![], @@ -3763,7 +3729,7 @@ fn deprecated_function_generates_annotation() { ); } -fn doc_api() -> Api { +fn doc_api() -> ResolvedApi { make_api(vec![Module { name: "docs".into(), functions: vec![Function { @@ -3789,7 +3755,6 @@ fn doc_api() -> Api { name: "id".into(), ty: TypeRef::I64, doc: Some("Stable id".into()), - default: None, }], }], enums: vec![EnumDef { @@ -3897,7 +3862,7 @@ fn python_custom_prefix_threads_to_user_symbols() { /// non-throwing free function, and a `Store` interface exercising the /// canonical `new` constructor, a factory constructor, an instance method /// with a string parameter and return, and a static. -fn kv_api() -> Api { +fn kv_api() -> ResolvedApi { use weaveffi_ir::ir::{ErrorCode, ErrorDomain, InterfaceDef}; let fn_lit = @@ -3975,7 +3940,7 @@ fn kv_api() -> Api { #[test] fn python_interface_async_and_iterator_members() { use weaveffi_ir::ir::InterfaceDef; - let mut api = kv_api(); + let mut api = kv_api().api().clone(); api.modules[0].interfaces = vec![InterfaceDef { name: "Store".into(), doc: None, @@ -4031,6 +3996,7 @@ fn python_interface_async_and_iterator_members() { since: None, }], }]; + let api = ResolvedApi::assume_resolved(api); let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml"); // Async method: the wrapper itself is the async def, and the launcher @@ -4536,7 +4502,6 @@ fn rich_enum_sum_type() { name: "radius".into(), ty: TypeRef::F64, doc: None, - default: None, }], }, EnumVariant { @@ -4548,13 +4513,11 @@ fn rich_enum_sum_type() { name: "width".into(), ty: TypeRef::F64, doc: None, - default: None, }, StructField { name: "height".into(), ty: TypeRef::F64, doc: None, - default: None, }, ], }, @@ -4685,13 +4648,11 @@ fn error_payload_decoding() { name: "key".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }, StructField { name: "attempts".into(), ty: TypeRef::I32, doc: None, - default: None, }, ], }, @@ -4780,7 +4741,6 @@ fn listener_buffered_record_param_decodes() { name: "kind".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }], }], enums: vec![], @@ -4847,7 +4807,6 @@ fn async_function_returns_buffered_record() { name: "name".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }], }], enums: vec![], @@ -4863,18 +4822,14 @@ fn async_function_returns_buffered_record() { py.contains("async def fetch_contact(id: int) -> \"Contact\":"), "missing async wrapper: {py}" ); - // The borrowed result buffer is copied and decoded inside the - // completion trampoline; the producer frees it afterward. + // The owned result buffer is copied and freed by `_take_buffer`, then + // decoded inside the completion trampoline. assert!( py.contains( "_state[\"val\"] = _unpack_Contact(\ - ctypes.string_at(result_ptr, result_len) if result_ptr else b\"\")" + _take_buffer(ctypes.cast(result_ptr, ctypes.c_void_p).value, result_len))" ), - "trampoline should decode the borrowed result buffer: {py}" - ); - assert!( - !py.contains("weaveffi_free_bytes(result_ptr"), - "the consumer must not free the borrowed result: {py}" + "trampoline should decode the owned result buffer: {py}" ); // Decode failures surface through the future rather than escaping // the C callback. @@ -4912,7 +4867,6 @@ fn iterator_buffered_record_elements() { name: "name".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }], }], enums: vec![], @@ -4947,77 +4901,6 @@ fn iterator_buffered_record_elements() { ); } -#[test] -fn struct_field_defaults_become_dataclass_defaults() { - let api = make_api(vec![Module { - name: "inventory".into(), - functions: vec![], - structs: vec![StructDef { - name: "Item".into(), - doc: None, - fields: vec![ - StructField { - name: "name".into(), - ty: TypeRef::StringUtf8, - doc: None, - default: None, - }, - StructField { - name: "quantity".into(), - ty: TypeRef::U32, - doc: None, - default: Some(serde_yaml::Value::Number(0.into())), - }, - StructField { - name: "active".into(), - ty: TypeRef::Bool, - doc: None, - default: Some(serde_yaml::Value::Bool(true)), - }, - StructField { - name: "weight".into(), - ty: TypeRef::F64, - doc: None, - default: Some(serde_yaml::Value::Number(1.5.into())), - }, - ], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml"); - - // The trailing defaulted run surfaces as dataclass defaults; the - // required leading field stays bare. - assert!(py.contains("name: str\n"), "missing required field: {py}"); - assert!( - py.contains("quantity: int = 0"), - "missing int default: {py}" - ); - assert!( - py.contains("active: bool = True"), - "missing bool default: {py}" - ); - assert!( - py.contains("weight: float = 1.5"), - "missing float default: {py}" - ); - - // The stub marks defaulted constructor parameters with `= ...`. - let pyi = render_pyi_module(&BindingModel::build(&api, "weaveffi"), true, "weaveffi.yml"); - assert!( - pyi.contains( - "def __init__(self, name: str, quantity: int = ..., \ - active: bool = ..., weight: float = ...) -> None: ..." - ), - "stub should mark defaults: {pyi}" - ); -} - /// A kitchen-sink API touching every buffered surface at once (record and /// rich enum params and returns, nested optional/list/map fields, error /// payloads, a buffered listener, an async buffered method, and a record @@ -5101,19 +4984,16 @@ fn kitchen_sink_api_generates() { name: "name".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }, StructField { name: "quantity".into(), ty: TypeRef::U32, doc: None, - default: Some(serde_yaml::Value::Number(0.into())), }, StructField { name: "data".into(), ty: TypeRef::Bytes, doc: None, - default: None, }, StructField { name: "nested".into(), @@ -5122,7 +5002,6 @@ fn kitchen_sink_api_generates() { Box::new(TypeRef::F64), ))))), doc: None, - default: None, }, ], }], @@ -5144,7 +5023,6 @@ fn kitchen_sink_api_generates() { name: "radius".into(), ty: TypeRef::F64, doc: None, - default: None, }], }, ], @@ -5207,7 +5085,6 @@ fn kitchen_sink_api_generates() { name: "reason".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }], }], }), diff --git a/crates/weaveffi-gen-ruby/src/calls.rs b/crates/weaveffi-gen-ruby/src/calls.rs index 56b31d0b..4576d7e8 100644 --- a/crates/weaveffi-gen-ruby/src/calls.rs +++ b/crates/weaveffi-gen-ruby/src/calls.rs @@ -491,9 +491,11 @@ fn render_sync_function_wrapper( /// 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`], the trampoline copies borrowed -/// result buffers before returning and never frees them; the error slot -/// follows the function's [`ErrorStrategy`]. +/// [`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, @@ -561,14 +563,13 @@ fn render_async_function_wrapper( w.line("code = err[:code]"); w.line("msg = err[:message].null? ? '' : err[:message].read_string"); if typed_error { - // The payload buffer is borrowed for the callback's - // duration: copy it before clearing the slot. + // 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_clear(err_ptr)")); + w.line(format!("{q}weaveffi_error_free(err_ptr)")); w.line(format!("queue << {error_expr}")); }); w.line("else"); @@ -616,11 +617,10 @@ fn render_async_function_wrapper( /// `result_ptr`/`result_len` for a buffered value. /// /// Per the async completion contract ([`weaveffi_core::plan::AsyncProtocol`]), -/// string, bytes, and buffered result buffers are producer-owned and borrowed -/// for the callback's duration: the callback deep-copies or decodes them -/// before returning and never frees them. Owned interface results are the -/// exception: the callback receives ownership, so the pointer is adopted by -/// a finalizer-bearing wrapper. +/// 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, @@ -634,9 +634,9 @@ fn render_async_result_push( w.line("queue << nil"); } RetPass::Buffer => { - // Borrowed buffer: decode inside the callback, never free. A - // decode failure surfaces through the queue so the caller thread - // raises it. + // 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| { @@ -644,6 +644,9 @@ fn render_async_result_push( "_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"); @@ -655,12 +658,20 @@ fn render_async_result_push( w.line("end"); } RetPass::String => { - // Borrowed for the callback's duration: copy, don't free. - w.line("queue << (result.null? ? '' : result.read_string)"); + // Owned by the consumer: copy, then free. + w.line("_wv_s = result.null? ? '' : result.read_string"); + w.line(format!( + "{m}weaveffi_free_string(result) unless result.null?" + )); + w.line("queue << _wv_s"); } RetPass::Bytes => { - // Borrowed for the callback's duration: copy, don't free. - w.line("queue << (result.null? ? ''.b : result.read_string(result_len))"); + // 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 diff --git a/crates/weaveffi-gen-ruby/src/lib.rs b/crates/weaveffi-gen-ruby/src/lib.rs index f9de197b..3b14bbb1 100644 --- a/crates/weaveffi-gen-ruby/src/lib.rs +++ b/crates/weaveffi-gen-ruby/src/lib.rs @@ -22,13 +22,13 @@ mod tests; use camino::Utf8Path; use serde::{Deserialize, Serialize}; +use weaveffi_core::resolved::ResolvedApi; 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::utils::{render_prelude, render_trailer, CommentStyle}; -use weaveffi_ir::ir::Api; use crate::calls::{ render_attach_function, render_callable, render_callback_decl, render_listener_ffi, @@ -122,7 +122,7 @@ impl LanguageBackend for RubyGenerator { fn files( &self, - api: &Api, + api: &ResolvedApi, model: &BindingModel, out_dir: &Utf8Path, config: &Self::Config, @@ -161,7 +161,7 @@ impl LanguageBackend for RubyGenerator { fn package( &self, - api: &Api, + api: &ResolvedApi, model: &BindingModel, ctx: &PackageContext, out_dir: &Utf8Path, diff --git a/crates/weaveffi-gen-ruby/src/runtime.rs b/crates/weaveffi-gen-ruby/src/runtime.rs index 2ed53e56..937eddfc 100644 --- a/crates/weaveffi-gen-ruby/src/runtime.rs +++ b/crates/weaveffi-gen-ruby/src/runtime.rs @@ -1,6 +1,7 @@ //! The fixed Ruby runtime the generated module carries: the library loader, //! the error surface, the value-buffer reader/writer pair, and the runtime -//! ABI attachments (`error_clear`, `free_string`, `free_bytes`). +//! ABI attachments (`error_clear`, `error_free`, `free_string`, +//! `free_bytes`). /// The exact `ffi_lib` loader block [`render_preamble`] emits in `generate` /// mode, so the packager can swap it for a bundled-first variant. @@ -81,6 +82,7 @@ module {module_name} out.push_str( " 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 diff --git a/crates/weaveffi-gen-ruby/src/tests.rs b/crates/weaveffi-gen-ruby/src/tests.rs index 6757ee0c..9b8e4056 100644 --- a/crates/weaveffi-gen-ruby/src/tests.rs +++ b/crates/weaveffi-gen-ruby/src/tests.rs @@ -1,6 +1,7 @@ //! Tests for the Ruby generator: rendering coverage across every entity //! and call shape, error-domain semantics, packaging, and identifier policy. +use weaveffi_core::resolved::ResolvedApi; use super::*; use crate::types::{rb_abi_types, rb_ffi_type}; use camino::Utf8Path; @@ -80,13 +81,13 @@ use weaveffi_ir::ir::{ StructDef, StructField, TypeRef, }; -fn make_api(modules: Vec) -> Api { - Api { - version: "0.6.0".to_string(), +fn make_api(modules: Vec) -> ResolvedApi { + ResolvedApi::assume_resolved(Api { + version: "0.7.0".to_string(), modules, generators: None, package: None, - } + }) } fn simple_module(name: &str, functions: Vec) -> Module { @@ -105,7 +106,7 @@ fn simple_module(name: &str, functions: Vec) -> Module { /// Build the model (test-only; the driver builds it in production) and /// render with the default naming (module-prefix stripping on). -fn render(api: &Api, module_name: &str, prefix: &str) -> String { +fn render(api: &ResolvedApi, module_name: &str, prefix: &str) -> String { let model = BindingModel::build(api, prefix); render_ruby_module(&model, module_name, true, "weaveffi.rb", "weaveffi.yml") } @@ -139,7 +140,7 @@ fn str_param(name: &str) -> Param { /// constructor, a factory constructor, methods (sync throwing, sync /// non-throwing, async), a static, plus throwing and non-throwing free /// functions. -fn kv_api() -> Api { +fn kv_api() -> ResolvedApi { let mut m = simple_module( "kv", vec![ @@ -363,13 +364,11 @@ fn error_payload_fields_decode_into_attributes() { name: "key".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }, StructField { name: "attempts".into(), ty: TypeRef::U32, doc: None, - default: None, }, ], }], @@ -731,13 +730,11 @@ fn renders_struct_as_value_class() { name: "id".into(), ty: TypeRef::I64, doc: None, - default: None, }, StructField { name: "name".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }, ], }], @@ -791,13 +788,11 @@ fn struct_codec_packs_and_unpacks_fields_in_order() { name: "x".into(), ty: TypeRef::F64, doc: None, - default: None, }, StructField { name: "label".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }, ], }], @@ -1164,7 +1159,6 @@ fn struct_return_wraps_in_class() { name: "id".into(), ty: TypeRef::I64, doc: None, - default: None, }], }], enums: vec![], @@ -1242,20 +1236,20 @@ fn async_function_generates_blocking_wrapper() { code.contains("# Blocks the current thread until the async producer completes"), "blocking doc: {code}" ); - // The completion callback copies the borrowed result buffer and must - // not free it: the producer owns callback result buffers. + // The completion callback copies the owned result string and then + // releases the producer allocation. assert!( code.contains("result.read_string"), "result copied in callback: {code}" ); assert!( - !code.contains("weaveffi_free_string(result)"), - "borrowed callback buffer must not be freed: {code}" + code.contains("weaveffi_free_string(result) unless result.null?"), + "owned callback string must be freed after copying: {code}" ); } #[test] -fn async_bytes_result_copied_not_freed() { +fn async_bytes_result_copied_then_freed() { let api = make_api(vec![simple_module( "io", vec![Function { @@ -1276,8 +1270,8 @@ fn async_bytes_result_copied_not_freed() { "bytes copied in callback: {code}" ); assert!( - !code.contains("weaveffi_free_bytes(result, result_len)"), - "borrowed callback bytes must not be freed: {code}" + code.contains("weaveffi_free_bytes(result, result_len) unless result.null?"), + "owned callback bytes must be freed after copying: {code}" ); } @@ -1408,7 +1402,6 @@ fn iterator_of_records_adopts_each_element() { name: "key".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }], }], enums: vec![], @@ -1709,7 +1702,6 @@ fn list_of_structs_return() { name: "id".into(), ty: TypeRef::I64, doc: None, - default: None, }], }], enums: vec![], @@ -1766,9 +1758,9 @@ fn optional_struct_returns_nil_on_null() { // ── Comprehensive tests ── -fn contacts_api() -> Api { - Api { - version: "0.6.0".into(), +fn contacts_api() -> ResolvedApi { + ResolvedApi::assume_resolved(Api { + version: "0.7.0".into(), modules: vec![Module { name: "contacts".into(), functions: vec![ @@ -1872,31 +1864,26 @@ fn contacts_api() -> Api { name: "id".into(), ty: TypeRef::I64, doc: None, - default: None, }, StructField { name: "first_name".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }, StructField { name: "last_name".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }, StructField { name: "email".into(), ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)), doc: None, - default: None, }, StructField { name: "contact_type".into(), ty: TypeRef::Enum("ContactType".into()), doc: None, - default: None, }, ], }], @@ -1931,7 +1918,7 @@ fn contacts_api() -> Api { }], generators: None, package: None, - } + }) } #[test] @@ -2010,13 +1997,11 @@ fn generate_ruby_with_structs() { name: "first_name".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }, StructField { name: "last_name".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }, ], }], @@ -2390,7 +2375,6 @@ fn ruby_no_double_free_on_error() { name: "name".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }], }], enums: vec![], @@ -2512,7 +2496,7 @@ fn ruby_null_check_on_optional_return() { ); } -fn doc_api() -> Api { +fn doc_api() -> ResolvedApi { make_api(vec![Module { name: "docs".into(), functions: vec![Function { @@ -2539,7 +2523,6 @@ fn doc_api() -> Api { name: "id".into(), ty: TypeRef::I64, doc: Some("Stable id".into()), - default: None, }], }], enums: vec![EnumDef { @@ -2636,7 +2619,7 @@ fn ruby_custom_prefix_threads_to_user_symbols() { ); } -fn shapes_api() -> Api { +fn shapes_api() -> ResolvedApi { make_api(vec![Module { name: "shapes".into(), functions: vec![ @@ -2702,7 +2685,6 @@ fn shapes_api() -> Api { name: "radius".into(), ty: TypeRef::F64, doc: None, - default: None, }], }, EnumVariant { @@ -2714,13 +2696,11 @@ fn shapes_api() -> Api { name: "width".into(), ty: TypeRef::F32, doc: None, - default: None, }, StructField { name: "height".into(), ty: TypeRef::F32, doc: None, - default: None, }, ], }, @@ -2733,13 +2713,11 @@ fn shapes_api() -> Api { name: "label".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }, StructField { name: "count".into(), ty: TypeRef::U8, doc: None, - default: None, }, ], }, @@ -2965,13 +2943,11 @@ fn kitchen_sink_module_renders_coherently() { name: "key".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }, StructField { name: "hits".into(), ty: TypeRef::Optional(Box::new(TypeRef::U32)), doc: None, - default: None, }, ], }]; @@ -2987,7 +2963,6 @@ fn kitchen_sink_module_renders_coherently() { name: "entry".into(), ty: TypeRef::Record("Entry".into()), doc: None, - default: None, }], }, EnumVariant { @@ -3009,7 +2984,6 @@ fn kitchen_sink_module_renders_coherently() { name: "key".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }], }], }); @@ -3102,18 +3076,18 @@ fn async_buffered_result_decoded_inside_callback() { modules: vec![], }]); let code = render(&api, "WeaveFFI", "weaveffi"); - // The borrowed result buffer is decoded inside the callback and - // never freed (the producer frees after the callback returns). + // The owned result buffer is copied, freed, and decoded inside the + // callback. assert!( code.contains( "_wv_r = WvBufferReader.new(result_ptr.null? ? ''.b : result_ptr.read_string(result_len))" ), - "borrowed buffer copied and decoded: {code}" + "owned buffer copied and decoded: {code}" ); assert!(code.contains("queue << _wv_v"), "decoded push: {code}"); assert!( - !code.contains("weaveffi_free_bytes(result_ptr"), - "borrowed callback buffer must not be freed: {code}" + code.contains("weaveffi_free_bytes(result_ptr, result_len) unless result_ptr.null?"), + "owned callback buffer must be freed after copying: {code}" ); // A decode failure surfaces through the queue rather than raising // across the C callback boundary. @@ -3149,7 +3123,6 @@ fn listener_buffered_argument_decoded_before_dispatch() { name: "key".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }], }], ..simple_module("events", vec![]) @@ -3195,13 +3168,11 @@ fn keyword_params_and_fields_gain_trailing_underscore() { name: "end".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }, StructField { name: "normal".into(), ty: TypeRef::Bool, doc: None, - default: None, }, ], }], @@ -3294,7 +3265,7 @@ fn negative_runtime_codes_fall_through_to_generic_error() { fn gemspec_escapes_quotes_and_backslashes_in_metadata() { use weaveffi_ir::ir::Package; - let mut api = make_api(vec![simple_module("kv", vec![])]); + let mut api = make_api(vec![simple_module("kv", vec![])]).api().clone(); api.package = Some(Package { name: "my-kv".into(), version: "1.2.3".into(), @@ -3304,6 +3275,7 @@ fn gemspec_escapes_quotes_and_backslashes_in_metadata() { homepage: Some("https://example.com/it's".into()), repository: None, }); + let api = ResolvedApi::assume_resolved(api); let dir = tempfile::tempdir().unwrap(); let out_dir = Utf8Path::from_path(dir.path()).unwrap(); RubyGenerator diff --git a/crates/weaveffi-gen-swift/src/calls.rs b/crates/weaveffi-gen-swift/src/calls.rs index 382e6990..b96aaa3d 100644 --- a/crates/weaveffi-gen-swift/src/calls.rs +++ b/crates/weaveffi-gen-swift/src/calls.rs @@ -87,20 +87,28 @@ impl<'a> ErrCtx<'a> { /// The statements an async completion callback runs (after copying the /// runtime `code`/`msg` locals) when the error slot reports: copy the - /// payload and resume throwing the mapped domain error, resume with the - /// generic brand error, or trap. + /// 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!["fatalError(\"\\(code): \\(msg)\")".to_string()]; + 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![format!( - "contRef.value.resume(throwing: {ERROR_BRAND}.error(code: code, message: msg))" - )], + None => vec![ + "weaveffi_error_free(err)".to_string(), + format!( + "contRef.value.resume(throwing: {ERROR_BRAND}.error(code: code, message: msg))" + ), + ], } } @@ -882,11 +890,10 @@ fn async_callback_param_names(rp: &RetPass) -> &'static str { /// 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 borrowed for the -/// callback's duration: they're deep-copied or decoded before the callback -/// returns and never freed here. Owned-object results are the exception; the -/// callback receives ownership and the pointer is adopted by its wrapper -/// class. +/// 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, @@ -901,8 +908,8 @@ fn render_async_resume_result( } RetPass::Buffer => { let ty = f.ret.as_ref().expect("a buffered return carries a type"); - // Borrowed for the callback's duration: copy the bytes and decode - // inside the callback; the producer frees its own buffer after. + // 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")); @@ -914,6 +921,7 @@ fn render_async_resume_result( 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); @@ -931,14 +939,17 @@ fn render_async_resume_result( } w.dedent(); w.line("}"); - // The string is borrowed for the callback's duration: copy it, - // don't free it (the producer releases its own buffer). - w.line("contRef.value.resume(returning: String(cString: result))"); + // 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("contRef.value.resume(returning: Data(bytes: result, count: resultLen))"); + 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| { diff --git a/crates/weaveffi-gen-swift/src/lib.rs b/crates/weaveffi-gen-swift/src/lib.rs index 4364f436..1d755879 100644 --- a/crates/weaveffi-gen-swift/src/lib.rs +++ b/crates/weaveffi-gen-swift/src/lib.rs @@ -31,13 +31,14 @@ use std::collections::{HashMap, HashSet}; use camino::Utf8Path; use heck::ToUpperCamelCase; use serde::{Deserialize, Serialize}; +use weaveffi_core::resolved::ResolvedApi; 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::utils::{render_prelude, render_trailer, CommentStyle}; -use weaveffi_ir::ir::{Api, Module}; +use weaveffi_ir::ir::Module; use crate::entities::{render_swift_module_body, render_swift_module_types}; use crate::package::{ @@ -154,7 +155,7 @@ impl LanguageBackend for SwiftGenerator { fn files( &self, - api: &Api, + api: &ResolvedApi, model: &BindingModel, out_dir: &Utf8Path, config: &Self::Config, @@ -196,7 +197,7 @@ impl LanguageBackend for SwiftGenerator { fn package( &self, - api: &Api, + api: &ResolvedApi, model: &BindingModel, ctx: &PackageContext, out_dir: &Utf8Path, @@ -250,7 +251,7 @@ weaveffi_core::impl_generator_via_backend!(SwiftGenerator); /// 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: &Api, + api: &ResolvedApi, model: &BindingModel, c_prefix: &str, strip_module_prefix: bool, diff --git a/crates/weaveffi-gen-swift/src/tests.rs b/crates/weaveffi-gen-swift/src/tests.rs index 0335b36a..7774afed 100644 --- a/crates/weaveffi-gen-swift/src/tests.rs +++ b/crates/weaveffi-gen-swift/src/tests.rs @@ -1,3 +1,4 @@ +use weaveffi_core::resolved::ResolvedApi; use super::*; use crate::types::swift_type_for; use weaveffi_core::codegen::Generator; @@ -6,19 +7,19 @@ use weaveffi_ir::ir::{ StructField, TypeRef, }; -fn make_api(modules: Vec) -> Api { - Api { - version: "0.6.0".to_string(), +fn make_api(modules: Vec) -> ResolvedApi { + ResolvedApi::assume_resolved(Api { + version: "0.7.0".to_string(), modules, generators: None, package: None, - } + }) } /// Build the binding model and render the wrapper, exactly as the driver /// does in production before calling [`LanguageBackend::files`]. fn render( - api: &Api, + api: &ResolvedApi, c_prefix: &str, strip_module_prefix: bool, input_basename: &str, @@ -161,7 +162,6 @@ fn listener_decodes_buffered_argument() { name: "name".into(), ty: TypeRef::StringUtf8, doc: None, - default: None, }], }], enums: vec![], @@ -275,7 +275,6 @@ fn field(name: &str, ty: TypeRef) -> StructField { name: name.into(), ty, doc: None, - default: None, } } @@ -2155,17 +2154,17 @@ fn swift_async_buffered_result_decoded_in_callback() { modules: vec![], }]); let out = render(&api, "weaveffi", true, "weaveffi.yml", "WeaveFFI.swift"); - // The callback receives a borrowed (ptr, len) pair. + // The callback receives an owned (ptr, len) pair. assert!( out.contains("{ context, err, resultPtr, resultLen in"), - "callback must take the borrowed buffer pair: {out}" + "callback must take the owned buffer pair: {out}" ); // The buffer is copied and decoded inside the callback, before resuming. assert!( out.contains( "let resultBytes = [UInt8](UnsafeBufferPointer(start: resultPtr, count: resultLen))" ), - "callback must copy the borrowed buffer: {out}" + "callback must copy the owned buffer: {out}" ); assert!( out.contains("var resultReader = WvReader(bytes: resultBytes)"), @@ -2175,12 +2174,13 @@ fn swift_async_buffered_result_decoded_in_callback() { out.contains("contRef.value.resume(returning: v0)"), "callback must resume with the decoded value: {out}" ); - // The producer frees the buffer after the callback returns; the wrapper - // must not free it. + // The wrapper owns the buffer: it frees the producer allocation after + // copying. let cb_pos = out.find("fetch_ids_async").expect("async launcher present"); assert!( - !out[cb_pos..].contains("weaveffi_free_bytes"), - "async wrapper must not free the borrowed result buffer: {out}" + out[cb_pos..] + .contains("weaveffi_free_bytes(UnsafeMutablePointer(mutating: resultPtr), resultLen)"), + "async wrapper must free the owned result buffer after copying: {out}" ); } @@ -2278,7 +2278,7 @@ fn swift_nested_module_output() { } /// A module with an `iter` function, throwing or not. -fn iter_api(throws: bool) -> Api { +fn iter_api(throws: bool) -> ResolvedApi { make_api(vec![Module { name: "data".to_string(), functions: vec![Function { @@ -2584,7 +2584,7 @@ fn map_return_decodes_single_buffer() { } #[test] -fn async_string_result_is_copied_not_freed() { +fn async_string_result_is_copied_then_freed() { let api = make_api(vec![Module { name: "tasks".to_string(), functions: vec![Function { @@ -2607,16 +2607,17 @@ fn async_string_result_is_copied_not_freed() { modules: vec![], }]); let out = render(&api, "weaveffi", true, "weaveffi.yml", "WeaveFFI.swift"); - // The callback's string is borrowed for the callback's duration: it - // is copied into the resumed value and never freed by the wrapper. + // The callback's string is owned by the consumer: it is copied into the + // resumed value and then released with `weaveffi_free_string`. assert!( - out.contains("contRef.value.resume(returning: String(cString: result))"), + out.contains("let resultValue = String(cString: result)") + && out.contains("contRef.value.resume(returning: resultValue)"), "async string result must be copied before resuming: {out}" ); let cb_pos = out.find("fetch_async").expect("async launcher present"); assert!( - !out[cb_pos..].contains("weaveffi_free_string"), - "async wrapper must not free the borrowed result buffer: {out}" + out[cb_pos..].contains("weaveffi_free_string(result)"), + "async wrapper must free the owned result string after copying: {out}" ); } @@ -2654,7 +2655,7 @@ fn deprecated_function_generates_annotation() { ); } -fn doc_api() -> Api { +fn doc_api() -> ResolvedApi { make_api(vec![Module { name: "docs".into(), functions: vec![Function { @@ -2680,7 +2681,6 @@ fn doc_api() -> Api { name: "id".into(), ty: TypeRef::I64, doc: Some("Stable id".into()), - default: None, }], }], enums: vec![EnumDef { @@ -2775,7 +2775,7 @@ fn swift_emits_doc_on_param() { /// f64 payload, two f32 payloads, and a string+u8 payload), a plain C-style /// enum `Channel`, and the free functions that take/return `Shape` (lowered /// to `TypeRef::RichEnum`) plus the numerics smoke `sum_bytes`. -fn shapes_api() -> Api { +fn shapes_api() -> ResolvedApi { make_api(vec![Module { name: "shapes".into(), functions: vec![ @@ -2975,7 +2975,7 @@ fn rich_enum_functions_marshal_buffers() { /// exercising every member kind: a plain constructor named `new`, a /// throwing factory constructor, throwing and non-throwing methods, an /// async throwing method, and a static. -fn store_api() -> Api { +fn store_api() -> ResolvedApi { use weaveffi_ir::ir::InterfaceDef; fn f( name: &str, diff --git a/crates/weaveffi-gen-wasm/src/calls.rs b/crates/weaveffi-gen-wasm/src/calls.rs index c3ccf628..2d5756d6 100644 --- a/crates/weaveffi-gen-wasm/src/calls.rs +++ b/crates/weaveffi-gen-wasm/src/calls.rs @@ -761,11 +761,10 @@ pub(crate) fn async_cb_wasm_params(returns: Option<&TypeRef>) -> Vec<&'static st /// rejects with the typed error. /// /// The unwrap runs inside the completion callback, so it follows the async -/// borrowing contract: string, byte, and value buffers are producer-owned and -/// valid only for the callback's duration, so they are deep-copied or decoded -/// out of wasm memory and never freed here. Owned interface results are the -/// exception: the callback receives ownership and the pointer is adopted by -/// its wrapper class. +/// 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. Owned interface results are adopted by +/// their wrapper class instead. fn emit_async_unwrap( out: &mut String, indent: &str, @@ -788,13 +787,14 @@ fn emit_async_unwrap( let open = format!("_asyncContexts.set(ctxId, {{ {base}, unwrap: "); match plan::ret_pass(Some(ret), module, prefix) { RetPass::Void => unreachable!("a present return type is never void"), - // Borrowed: copy the encoding out of wasm memory inside the callback, - // decode, never free (the producer reclaims it afterwards). + // 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();"); @@ -808,8 +808,12 @@ fn emit_async_unwrap( w.line(plain); } RetPass::String => { - // Borrowed: copy out of wasm memory, never free. - w.line(format!("{open}(w, p) => _readCStr(w, p) }});")); + // 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_ret_name(ret)); @@ -822,10 +826,14 @@ fn emit_async_unwrap( } } RetPass::Bytes => { - // Borrowed: slice() deep-copies out of wasm memory, never free. - w.line(format!( - "{open}(w, ptr, len) => ptr === 0 || len === 0 ? new Uint8Array(0) : new Uint8Array(w.memory.buffer, ptr, len).slice() }});" - )); + // 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()); diff --git a/crates/weaveffi-gen-wasm/src/lib.rs b/crates/weaveffi-gen-wasm/src/lib.rs index fe4c68f3..ec354bdc 100644 --- a/crates/weaveffi-gen-wasm/src/lib.rs +++ b/crates/weaveffi-gen-wasm/src/lib.rs @@ -23,11 +23,11 @@ mod tests; use camino::Utf8Path; use serde::{Deserialize, Serialize}; +use weaveffi_core::resolved::ResolvedApi; use weaveffi_core::backend::{LanguageBackend, OutputFile}; use weaveffi_core::capabilities::TargetCapabilities; use weaveffi_core::model::BindingModel; use weaveffi_core::pkg; -use weaveffi_ir::ir::Api; use crate::dts::render_wasm_dts; use crate::entities::render_wasm_js_stub; @@ -112,7 +112,7 @@ impl LanguageBackend for WasmGenerator { fn files( &self, - api: &Api, + api: &ResolvedApi, model: &BindingModel, out_dir: &Utf8Path, config: &Self::Config, diff --git a/crates/weaveffi-gen-wasm/src/runtime.rs b/crates/weaveffi-gen-wasm/src/runtime.rs index 5ec5904e..9916425f 100644 --- a/crates/weaveffi-gen-wasm/src/runtime.rs +++ b/crates/weaveffi-gen-wasm/src/runtime.rs @@ -267,13 +267,14 @@ pub(crate) fn emit_error_slot_helpers(out: &mut String) { out.push_str("}\n\n"); } -/// Emit `_checkErrRef`, the borrowed-error checker the async completion -/// callbacks route through: the producer owns and frees the error struct, so -/// the slot is read but never deallocated here. +/// 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 borrowed (producer-owned) error carries a non-zero\n"); - out.push_str("// code. Used by async callbacks: the producer owns and frees the\n"); - out.push_str("// error struct, so the slot is read but never deallocated here.\n"); + 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" @@ -281,12 +282,13 @@ pub(crate) fn emit_check_err_ref(out: &mut String) { out.push_str("function _checkErrRef(wasm, errPtr, mkErr) {\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) return;\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( - " if (mkErr) throw mkErr(wasm, code, msg, dv.getUint32(errPtr + 8, true), dv.getUint32(errPtr + 12, true));\n", - ); - out.push_str(&format!(" throw new {ERROR_BRAND}(code, msg);\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"); } diff --git a/crates/weaveffi-gen-wasm/src/tests.rs b/crates/weaveffi-gen-wasm/src/tests.rs index 3c58afd5..fa91df07 100644 --- a/crates/weaveffi-gen-wasm/src/tests.rs +++ b/crates/weaveffi-gen-wasm/src/tests.rs @@ -1,6 +1,8 @@ //! Unit tests: golden-content assertions over the JS loader, the //! TypeScript declarations, the README, and the package manifest. +use weaveffi_ir::ir::Api; +use weaveffi_core::resolved::ResolvedApi; use super::*; use camino::Utf8Path; use weaveffi_core::codegen::Generator; @@ -13,28 +15,28 @@ use crate::entities::render_wasm_js_stub; use crate::package::render_wasm_readme; use crate::types::{ts_type_for, type_display, wasm_type, wasm_type_note}; -fn empty_api() -> Api { - Api { - version: "0.6.0".into(), +fn empty_api() -> ResolvedApi { + ResolvedApi::assume_resolved(Api { + version: "0.7.0".into(), modules: vec![], generators: None, package: None, - } + }) } -fn make_api(modules: Vec) -> Api { - Api { - version: "0.6.0".into(), +fn make_api(modules: Vec) -> ResolvedApi { + ResolvedApi::assume_resolved(Api { + version: "0.7.0".into(), modules, generators: None, package: None, - } + }) } /// Test-only shim: build the model (the driver's job in production) and /// render the JS stub with the historical argument order. fn js_stub_for( - api: &Api, + api: &ResolvedApi, module_name: &str, prefix: &str, input_basename: &str, @@ -55,7 +57,7 @@ fn js_stub_for( /// Test-only shim mirroring [`js_stub_for`] for the `.d.ts` renderer. fn dts_for( - api: &Api, + api: &ResolvedApi, module_name: &str, input_basename: &str, filename: &str, @@ -73,7 +75,7 @@ fn dts_for( } /// Test-only shim mirroring [`js_stub_for`] for the README renderer. -fn readme_for(api: &Api, prefix: &str, input_basename: &str, emscripten: bool) -> String { +fn readme_for(api: &ResolvedApi, prefix: &str, input_basename: &str, emscripten: bool) -> String { let model = BindingModel::build(api, prefix); render_wasm_readme(api, &model, prefix, input_basename, emscripten) } @@ -83,7 +85,6 @@ fn field(name: &str, ty: TypeRef) -> StructField { name: name.into(), ty, doc: None, - default: None, } } @@ -134,7 +135,7 @@ fn module(name: &str) -> Module { } } -fn sample_api() -> Api { +fn sample_api() -> ResolvedApi { make_api(vec![Module { functions: vec![Function { name: "add".into(), @@ -183,7 +184,7 @@ fn sample_api() -> Api { /// An API with a callback + listener, delivered synchronously through a /// long-lived function-table trampoline in the standard loader (and /// stubbed in Emscripten mode). -fn listener_api() -> Api { +fn listener_api() -> ResolvedApi { make_api(vec![Module { functions: vec![member("send", vec![str_param("text")], None, false, false)], callbacks: vec![weaveffi_ir::ir::CallbackDef { @@ -613,7 +614,7 @@ fn type_display_round_trips() { /// A `contacts` module with a string-to-optional-record lookup, reused by /// the API-reference and marshalling tests. -fn contacts_api() -> Api { +fn contacts_api() -> ResolvedApi { make_api(vec![Module { functions: vec![member( "find", @@ -1050,7 +1051,7 @@ fn wasm_enum_keyed_map() { /// A one-function API returning a record, exercising both the buffered /// parameter staging and the buffered return decode. -fn record_roundtrip_api() -> Api { +fn record_roundtrip_api() -> ResolvedApi { make_api(vec![Module { functions: vec![member( "save", @@ -1471,7 +1472,6 @@ fn doc_module() -> Module { name: "id".into(), ty: TypeRef::I64, doc: Some("Stable id".into()), - default: None, }], }], enums: vec![EnumDef { @@ -1584,7 +1584,7 @@ fn wasm_custom_prefix_threads_to_user_symbols() { /// sibling enum and free functions taking/returning the rich enum (already /// resolved to `TypeRef::RichEnum`) so the value-buffer marshalling is /// exercised too. -fn rich_enum_api() -> Api { +fn rich_enum_api() -> ResolvedApi { make_api(vec![Module { functions: vec![ member( @@ -1818,7 +1818,7 @@ fn wasm_rich_enum_readme() { } /// A one-function async API for the Emscripten stub tests. -fn async_api() -> Api { +fn async_api() -> ResolvedApi { make_api(vec![Module { functions: vec![Function { name: "compute".into(), @@ -2019,7 +2019,7 @@ fn dts_bytes_map_to_uint8array() { /// A kvstore-shaped module: a `Store` interface (canonical `new` plus an /// `open` factory, sync/iterator/async methods, one static), a `KvError` /// domain, and one non-throwing free function. -fn kv_api() -> Api { +fn kv_api() -> ResolvedApi { make_api(vec![Module { functions: vec![member("flush_all", vec![], None, false, false)], errors: Some(weaveffi_ir::ir::ErrorDomain { @@ -2282,13 +2282,18 @@ fn async_throwing_member_rejects_with_domain_error() { js.contains("if (errPtr !== 0) _checkErrRef(wasm, errPtr, ctx.mkErr);"), "{js}" ); - // The borrowed-error checker hands the payload slots to the factory. + // The boxed-error checker hands the payload slots to the factory, then + // releases the box before throwing. assert!( js.contains( - "if (mkErr) throw mkErr(wasm, code, msg, dv.getUint32(errPtr + 8, true), dv.getUint32(errPtr + 12, true));" + "const err = mkErr ? mkErr(wasm, code, msg, dv.getUint32(errPtr + 8, true), dv.getUint32(errPtr + 12, true)) : new WeaveFFIError(code, msg);" ), "{js}" ); + assert!( + js.contains("wasm.weaveffi_error_free(errPtr);"), + "the consumer must free the boxed async error: {js}" + ); // The launcher passes the cancel slot and callback as usual. assert!( js.contains( @@ -2455,7 +2460,7 @@ fn optional_interface_stays_nullable_pointer() { /// An error domain where one code declares payload fields, plus a /// throwing function so the checker path is generated. -fn payload_api() -> Api { +fn payload_api() -> ResolvedApi { make_api(vec![Module { functions: vec![member("login", vec![str_param("name")], None, true, false)], errors: Some(weaveffi_ir::ir::ErrorDomain { @@ -2635,14 +2640,14 @@ fn callback_buffered_argument_decoded_borrowed() { // --- Async completion contract: borrowed buffers are copied, not freed --- /// A one-module API with a single free function of the given return type. -fn returning_api(ret: TypeRef, is_async: bool) -> Api { +fn returning_api(ret: TypeRef, is_async: bool) -> ResolvedApi { make_api(vec![Module { functions: vec![member("get_it", vec![], Some(ret), false, is_async)], ..module("m") }]) } -fn js_for_api(api: &Api) -> String { +fn js_for_api(api: &ResolvedApi) -> String { js_stub_for( api, DEFAULT_MODULE_NAME, @@ -2654,39 +2659,40 @@ fn js_for_api(api: &Api) -> String { } #[test] -fn async_string_result_is_copied_not_freed() { +fn async_string_result_is_copied_then_freed() { let js = js_for_api(&returning_api(TypeRef::StringUtf8, true)); + assert!(js.contains("unwrap: (w, p) => {"), "{js}"); assert!( - js.contains("unwrap: (w, p) => _readCStr(w, p) });"), - "async string results are borrowed and must not be freed: {js}" + js.contains("const _s = _readCStr(w, p);"), + "async string results are copied out of wasm memory: {js}" ); assert!( - !js.contains("unwrap: (w, p) => _takeCStr"), - "async unwrap must not free the producer's string: {js}" + js.contains("if (p !== 0) w.weaveffi_free_string(p);"), + "async string results are owned and must be freed: {js}" ); } #[test] -fn async_bytes_result_is_copied_not_freed() { +fn async_bytes_result_is_copied_then_freed() { let js = js_for_api(&returning_api(TypeRef::Bytes, true)); assert!( - js.contains("new Uint8Array(w.memory.buffer, ptr, len).slice() });"), + js.contains("new Uint8Array(w.memory.buffer, ptr, len).slice();"), "async bytes results must be deep-copied: {js}" ); assert!( - !js.contains("unwrap: (w, ptr, len) => _takeBytes"), - "async unwrap must not free the producer's buffer: {js}" + js.contains("if (ptr !== 0) w.weaveffi_free_bytes(ptr, len);"), + "async bytes results are owned and must be freed: {js}" ); } #[test] -fn async_buffered_result_decoded_inside_callback_not_freed() { +fn async_buffered_result_decoded_inside_callback_then_freed() { let js = js_for_api(&returning_api( TypeRef::List(Box::new(TypeRef::StringUtf8)), true, )); - // The borrowed value buffer arrives as (ptr, len): the callback copies - // it out of wasm memory, decodes, and never frees it. + // The owned value buffer arrives as (ptr, len): the callback copies it + // out of wasm memory, frees the producer allocation, then decodes. assert!(js.contains("unwrap: (w, ptr, len) => {"), "{js}"); assert!( js.contains( @@ -2694,19 +2700,12 @@ fn async_buffered_result_decoded_inside_callback_not_freed() { ), "{js}" ); - assert!(js.contains("_arr.push(_rd.str())"), "{js}"); - assert!(js.contains("_rd.end();"), "{js}"); - // The unwrap closure binds the exports object as `w`, so any free - // call inside it would spell `w.weaveffi_free_bytes` (the shared - // `_takeBytes` helper legitimately frees owned returns elsewhere). - assert!( - !js.contains("w.weaveffi_free_bytes("), - "the producer frees the borrowed result buffer: {js}" - ); assert!( - !js.contains("unwrap: (w, ptr, len) => _takeBytes"), - "async unwrap must not adopt the borrowed buffer: {js}" + js.contains("if (ptr !== 0) w.weaveffi_free_bytes(ptr, len);"), + "the consumer frees the owned result buffer: {js}" ); + assert!(js.contains("_arr.push(_rd.str())"), "{js}"); + assert!(js.contains("_rd.end();"), "{js}"); // The completion callback carries (ctx, err, ptr, len): four i32s. assert!(js.contains("_cbPtr_i32_i32_i32_i32"), "{js}"); assert!( diff --git a/crates/weaveffi-ir/src/ir.rs b/crates/weaveffi-ir/src/ir.rs index cbef790c..56f9d986 100644 --- a/crates/weaveffi-ir/src/ir.rs +++ b/crates/weaveffi-ir/src/ir.rs @@ -25,7 +25,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.6.0"; +pub const CURRENT_SCHEMA_VERSION: &str = "0.7.0"; /// Every IR schema version the current tools accept. /// @@ -52,7 +52,7 @@ fn is_false(b: &bool) -> bool { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] #[schemars(description = "Top-level WeaveFFI API definition.")] pub struct Api { - /// IR schema version this document targets (for example `0.6.0`). + /// IR schema version this document targets (for example `0.7.0`). /// Validation rejects any value not listed in [`SUPPORTED_VERSIONS`]. pub version: String, /// Package identity used to name, version, and describe every generated @@ -117,8 +117,7 @@ pub struct Package { /// 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. -// `Eq` is omitted because a nested `StructField::default` holds `serde_yaml::Value` (an `f64`). -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[schemars( description = "A WeaveFFI module: a named group of functions, types, callbacks, listeners, and errors." )] @@ -584,8 +583,7 @@ impl JsonSchema for TypeRef { /// A C-style enum lowers across the C ABI by value as an integer, while an /// algebraic enum lowers as an opaque object with a tag getter plus per-variant /// constructors and field getters. -// `Eq` is omitted because a variant field's `default` may hold `serde_yaml::Value` (an `f64`), matching `StructDef`. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[schemars( description = "An enum type. C-style when every variant is a bare discriminant; an algebraic sum type when any variant declares fields." )] @@ -613,8 +611,7 @@ impl EnumDef { } /// A single variant of an [`EnumDef`]. -// `Eq` is omitted because a variant field's `default` may hold `serde_yaml::Value` (an `f64`). -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct EnumVariant { /// Variant name (for example `Red`). pub name: String, @@ -627,15 +624,13 @@ pub struct EnumVariant { pub doc: Option, /// Associated data carried by this variant. Empty for a unit variant or a /// C-style enum; non-empty makes the owning enum a sum type (see - /// [`EnumDef::is_rich`]). Variant fields reuse [`StructField`] but ignore - /// the `default` slot (a sum-type payload has no defaultable fields). + /// [`EnumDef::is_rich`]). #[serde(default, skip_serializing_if = "Vec::is_empty")] pub fields: Vec, } /// A struct (record) type with named fields. -// `Eq` is omitted because `StructField::default` holds `serde_yaml::Value` (an `f64`). -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[schemars(description = "A struct (record) type with named fields.")] pub struct StructDef { /// Struct type name (for example `Contact`). @@ -651,7 +646,7 @@ pub struct StructDef { /// A named field of a [`StructDef`], or the payload of an algebraic /// [`EnumVariant`]. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct StructField { /// Field name (for example `email`). pub name: String, @@ -662,19 +657,11 @@ pub struct StructField { /// `None` when undocumented. #[serde(default, skip_serializing_if = "Option::is_none")] pub doc: Option, - /// Default value used when the field is omitted, kept as a raw YAML value so - /// any literal the field's type accepts can be expressed. Ignored for - /// algebraic [`EnumVariant`] payloads, which aren't defaultable. `None` when - /// the field has no default. - #[serde(default, skip_serializing_if = "Option::is_none")] - #[schemars(with = "Option")] - pub default: Option, } /// A module's error domain: the named set of error codes its fallible functions /// can report. -// `Eq` is omitted because a code's payload fields hold `serde_yaml::Value`. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct ErrorDomain { /// Error domain name, used to name the generated error type (for example /// `ContactErrors`). @@ -684,8 +671,7 @@ pub struct ErrorDomain { } /// A single named error within an [`ErrorDomain`]. -// `Eq` is omitted because a payload field's `default` may hold `serde_yaml::Value` (an `f64`). -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct ErrorCode { /// Error code name, lowered to a variant or constant on the generated error /// type (for example `not_found`). @@ -700,8 +686,7 @@ pub struct ErrorCode { pub doc: Option, /// Structured payload fields this error carries beyond its code and /// message, serialized across the ABI in the error's payload buffer. - /// Empty for a plain code. Payload fields reuse [`StructField`] but - /// ignore the `default` slot. + /// Empty for a plain code. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub fields: Vec, } @@ -713,7 +698,7 @@ mod tests { #[test] fn struct_def_round_trip_yaml() { let yaml = r#" -version: "0.6.0" +version: "0.7.0" modules: - name: geometry functions: [] @@ -744,7 +729,7 @@ modules: #[test] fn struct_def_round_trip_json() { let json = r#"{ - "version": "0.6.0", + "version": "0.7.0", "modules": [{ "name": "geo", "functions": [], @@ -767,7 +752,7 @@ modules: #[test] fn structs_default_to_empty() { let yaml = r#" -version: "0.6.0" +version: "0.7.0" modules: - name: math functions: [] @@ -779,7 +764,7 @@ modules: #[test] fn package_block_round_trips_yaml() { let yaml = r#" -version: "0.6.0" +version: "0.7.0" package: name: kvstore version: 1.2.0 @@ -818,7 +803,7 @@ modules: #[test] fn package_is_optional() { let yaml = r#" -version: "0.6.0" +version: "0.7.0" modules: - name: math functions: [] @@ -833,7 +818,7 @@ modules: #[test] fn package_minimal_requires_name_and_version() { let yaml = r#" -version: "0.6.0" +version: "0.7.0" package: name: tiny version: 0.0.1 @@ -862,7 +847,6 @@ modules: [] name: "origin".to_string(), ty: TypeRef::Named("Point".to_string()), doc: None, - default: None, }; let json = serde_json::to_string(&field).unwrap(); let back: StructField = serde_json::from_str(&json).unwrap(); @@ -879,7 +863,7 @@ modules: [] #[test] fn enum_def_round_trip_yaml() { let yaml = r#" -version: "0.6.0" +version: "0.7.0" modules: - name: graphics functions: [] @@ -915,7 +899,7 @@ modules: #[test] fn enum_def_round_trip_json() { let json = r#"{ - "version": "0.6.0", + "version": "0.7.0", "modules": [{ "name": "status", "functions": [], @@ -939,7 +923,7 @@ modules: #[test] fn enums_default_to_empty() { let yaml = r#" -version: "0.6.0" +version: "0.7.0" modules: - name: math functions: [] @@ -993,7 +977,6 @@ modules: name: "radius".to_string(), ty: TypeRef::F64, doc: None, - default: None, }], }, EnumVariant { @@ -1017,19 +1000,16 @@ modules: name: "r".to_string(), ty: TypeRef::U32, doc: None, - default: None, }, StructField { name: "g".to_string(), ty: TypeRef::U32, doc: None, - default: None, }, StructField { name: "b".to_string(), ty: TypeRef::U32, doc: None, - default: None, }, ], }; @@ -1188,7 +1168,7 @@ modules: #[test] fn typeref_optional_yaml_deser() { let yaml = r#" -version: "0.6.0" +version: "0.7.0" modules: - name: contacts functions: @@ -1211,7 +1191,7 @@ modules: #[test] fn typeref_list_yaml_deser() { let yaml = r#" -version: "0.6.0" +version: "0.7.0" modules: - name: contacts functions: @@ -1304,7 +1284,7 @@ modules: #[test] fn typeref_map_yaml_deser() { let yaml = r#" -version: "0.6.0" +version: "0.7.0" modules: - name: contacts functions: @@ -1453,7 +1433,7 @@ modules: #[test] fn typeref_borrowed_yaml_deser() { let yaml = r#" -version: "0.6.0" +version: "0.7.0" modules: - name: io functions: @@ -1482,7 +1462,7 @@ modules: #[test] fn generators_field_parses_from_yaml() { let yaml = r#" -version: "0.6.0" +version: "0.7.0" modules: - name: math functions: [] @@ -1503,7 +1483,7 @@ generators: #[test] fn generators_defaults_to_none() { let yaml = r#" -version: "0.6.0" +version: "0.7.0" modules: - name: math functions: [] @@ -1524,7 +1504,7 @@ modules: #[test] fn callback_def_round_trip_yaml() { let yaml = r#" -version: "0.6.0" +version: "0.7.0" modules: - name: events functions: [] @@ -1549,7 +1529,7 @@ modules: #[test] fn listener_def_round_trip_yaml() { let yaml = r#" -version: "0.6.0" +version: "0.7.0" modules: - name: events functions: [] @@ -1573,7 +1553,7 @@ modules: #[test] fn callbacks_and_listeners_default_to_empty() { let yaml = r#" -version: "0.6.0" +version: "0.7.0" modules: - name: math functions: [] @@ -1615,7 +1595,7 @@ modules: #[test] fn error_code_fields_default_to_empty() { let yaml = r#" -version: "0.6.0" +version: "0.7.0" modules: - name: kv functions: [] @@ -1637,7 +1617,7 @@ modules: #[test] fn error_code_payload_fields_round_trip() { let yaml = r#" -version: "0.6.0" +version: "0.7.0" modules: - name: kv functions: [] @@ -1668,7 +1648,7 @@ modules: #[test] fn param_mutable_defaults_to_false() { let yaml = r#" -version: "0.6.0" +version: "0.7.0" modules: - name: io functions: @@ -1684,7 +1664,7 @@ modules: #[test] fn param_mutable_true_round_trip() { let yaml = r#" -version: "0.6.0" +version: "0.7.0" modules: - name: io functions: @@ -1705,7 +1685,7 @@ modules: #[test] fn param_mutable_false_explicit() { let json = r#"{ - "version": "0.6.0", + "version": "0.7.0", "modules": [{ "name": "io", "functions": [{ @@ -1721,7 +1701,7 @@ modules: #[test] fn deprecated_and_since_default_to_none() { let yaml = r#" -version: "0.6.0" +version: "0.7.0" modules: - name: math functions: @@ -1737,7 +1717,7 @@ modules: #[test] fn deprecated_and_since_round_trip() { let yaml = r#" -version: "0.6.0" +version: "0.7.0" modules: - name: math functions: @@ -1758,38 +1738,13 @@ modules: assert_eq!(f2.since.as_deref(), Some("0.1.0")); } - #[test] - fn struct_field_default_value_round_trip() { - let yaml = r#" -version: "0.6.0" -modules: - - name: contacts - functions: [] - structs: - - name: Contact - fields: - - name: name - type: string - - name: age - type: i32 - default: 0 -"#; - let api: Api = serde_yaml::from_str(yaml).unwrap(); - let fields = &api.modules[0].structs[0].fields; - assert!(fields[0].default.is_none()); - assert_eq!( - fields[1].default, - Some(serde_yaml::Value::Number(serde_yaml::Number::from(0))) - ); - } - #[test] fn serialization_omits_defaulted_fields() { // A minimal API whose every optional/defaulted field is at its // default must serialize without emitting those fields, so the // canonical IDL produced by `weaveffi format`/`extract` stays terse. let api = Api { - version: "0.6.0".into(), + version: "0.7.0".into(), modules: vec![Module { name: "calc".into(), functions: vec![Function { @@ -1893,7 +1848,7 @@ modules: #[test] fn interface_round_trip_yaml() { let yaml = r#" -version: "0.6.0" +version: "0.7.0" modules: - name: kv interfaces: @@ -1946,7 +1901,7 @@ modules: #[test] fn interfaces_default_to_empty() { let yaml = r#" -version: "0.6.0" +version: "0.7.0" modules: - name: math functions: [] @@ -1958,7 +1913,7 @@ modules: #[test] fn throws_defaults_to_false_and_round_trips() { let yaml = r#" -version: "0.6.0" +version: "0.7.0" modules: - name: kv functions: diff --git a/crates/weaveffi-ir/src/parse.rs b/crates/weaveffi-ir/src/parse.rs index d8de4b89..5d6f48ab 100644 --- a/crates/weaveffi-ir/src/parse.rs +++ b/crates/weaveffi-ir/src/parse.rs @@ -174,7 +174,7 @@ mod tests { fn expected_api() -> Api { Api { - version: "0.6.0".to_string(), + version: "0.7.0".to_string(), modules: vec![Module { name: "math".to_string(), functions: vec![Function { @@ -217,7 +217,7 @@ mod tests { #[test] fn parse_yaml_round_trip() { let yaml = r#" -version: "0.6.0" +version: "0.7.0" modules: - name: math functions: @@ -236,7 +236,7 @@ modules: #[test] fn parse_json_round_trip() { let json = r#"{ - "version": "0.6.0", + "version": "0.7.0", "modules": [{ "name": "math", "functions": [{ @@ -256,7 +256,7 @@ modules: #[test] fn parse_toml_round_trip() { let toml_str = r#" -version = "0.6.0" +version = "0.7.0" [[modules]] name = "math" @@ -280,7 +280,7 @@ type = "i32" #[test] fn parse_missing_optional_fields() { let yaml = r#" -version: "0.6.0" +version: "0.7.0" modules: - name: math functions: @@ -299,7 +299,7 @@ modules: fn parse_contacts_sample() { let yaml = std::fs::read_to_string("../../samples/contacts/contacts.yml").unwrap(); let api = parse_api_str(&yaml, "yml").unwrap(); - assert_eq!(api.version, "0.6.0"); + assert_eq!(api.version, "0.7.0"); assert_eq!(api.modules.len(), 1); let m = &api.modules[0]; @@ -327,7 +327,7 @@ modules: ); assert_eq!(s.fields[4].ty, TypeRef::Named("ContactType".to_string())); - // The 0.6.0 surface replaces the handle-based free functions with the + // The 0.7.0 surface replaces the handle-based free functions with the // ContactBook interface and a typed error domain. assert!(m.functions.is_empty()); @@ -447,7 +447,7 @@ modules: #[test] fn parse_struct_definitions() { let yaml = r#" -version: "0.6.0" +version: "0.7.0" modules: - name: contacts functions: [] @@ -476,7 +476,7 @@ modules: #[test] fn parse_enum_definitions() { let yaml = r#" -version: "0.6.0" +version: "0.7.0" modules: - name: colors functions: [] @@ -505,7 +505,7 @@ modules: #[test] fn parse_optional_types() { let yaml = r#" -version: "0.6.0" +version: "0.7.0" modules: - name: ops functions: @@ -528,7 +528,7 @@ modules: #[test] fn parse_list_types() { let yaml = r#" -version: "0.6.0" +version: "0.7.0" modules: - name: ops functions: @@ -548,7 +548,7 @@ modules: #[test] fn parse_struct_ref_in_function() { let yaml = r#" -version: "0.6.0" +version: "0.7.0" modules: - name: contacts functions: @@ -571,7 +571,7 @@ modules: #[test] fn parse_complex_nested_types() { let yaml = r#" -version: "0.6.0" +version: "0.7.0" modules: - name: ops functions: @@ -599,7 +599,7 @@ modules: #[test] fn doc_example_primitives() { let yaml = r#" -version: "0.6.0" +version: "0.7.0" modules: - name: demo functions: @@ -663,7 +663,7 @@ modules: #[test] fn doc_example_structs() { let yaml = r#" -version: "0.6.0" +version: "0.7.0" modules: - name: geometry structs: @@ -719,7 +719,7 @@ modules: #[test] fn doc_example_enums() { let yaml = r#" -version: "0.6.0" +version: "0.7.0" modules: - name: contacts enums: @@ -758,7 +758,7 @@ modules: #[test] fn doc_example_optionals() { let yaml = r#" -version: "0.6.0" +version: "0.7.0" modules: - name: contacts structs: @@ -801,7 +801,7 @@ modules: #[test] fn doc_example_lists() { let yaml = r#" -version: "0.6.0" +version: "0.7.0" modules: - name: ops structs: @@ -834,7 +834,7 @@ modules: #[test] fn doc_example_nested_types() { let yaml = r#" -version: "0.6.0" +version: "0.7.0" modules: - name: ops structs: @@ -878,7 +878,7 @@ modules: #[test] fn doc_example_error_domain() { let yaml = r#" -version: "0.6.0" +version: "0.7.0" modules: - name: contacts errors: @@ -931,7 +931,7 @@ modules: #[test] fn doc_example_complete_contacts() { let yaml = r#" -version: "0.6.0" +version: "0.7.0" modules: - name: contacts enums: @@ -973,7 +973,7 @@ modules: return: i32 "#; let api = parse_api_str(yaml, "yaml").unwrap(); - assert_eq!(api.version, "0.6.0"); + assert_eq!(api.version, "0.7.0"); let m = &api.modules[0]; assert_eq!(m.name, "contacts"); assert_eq!(m.enums.len(), 1); @@ -997,7 +997,7 @@ modules: #[test] fn parse_function_with_map_param() { let yaml = r#" -version: "0.6.0" +version: "0.7.0" modules: - name: store functions: @@ -1018,7 +1018,7 @@ modules: #[test] fn parse_function_with_map_return() { let yaml = r#" -version: "0.6.0" +version: "0.7.0" modules: - name: store structs: @@ -1046,14 +1046,14 @@ modules: fn parse_inventory_sample() { let yaml = std::fs::read_to_string("../../samples/inventory/inventory.yml").unwrap(); let api = parse_api_str(&yaml, "yml").unwrap(); - assert_eq!(api.version, "0.6.0"); + assert_eq!(api.version, "0.7.0"); assert_eq!(api.modules.len(), 2); let products = &api.modules[0]; assert_eq!(products.name, "products"); assert_eq!(products.enums.len(), 1); assert_eq!(products.structs.len(), 1); - // The 0.6.0 surface replaces the handle-based free functions with the + // The 0.7.0 surface replaces the handle-based free functions with the // Catalog interface. assert!(products.functions.is_empty()); assert_eq!(products.interfaces.len(), 1); @@ -1179,7 +1179,7 @@ modules: fn parse_async_demo_sample() { let yaml = std::fs::read_to_string("../../samples/async-demo/async_demo.yml").unwrap(); let api = parse_api_str(&yaml, "yml").unwrap(); - assert_eq!(api.version, "0.6.0"); + assert_eq!(api.version, "0.7.0"); assert_eq!(api.modules.len(), 1); let m = &api.modules[0]; @@ -1243,7 +1243,7 @@ modules: #[test] fn parse_idl_doc_map_example() { let yaml = r#" -version: "0.6.0" +version: "0.7.0" modules: - name: example structs: @@ -1308,7 +1308,7 @@ modules: #[test] fn parse_nested_modules() { let yaml = r#" -version: "0.6.0" +version: "0.7.0" modules: - name: parent functions: diff --git a/crates/weaveffi-macros/src/codegen/async_fns.rs b/crates/weaveffi-macros/src/codegen/async_fns.rs index bb15c3bd..5d8f2863 100644 --- a/crates/weaveffi-macros/src/codegen/async_fns.rs +++ b/crates/weaveffi-macros/src/codegen/async_fns.rs @@ -3,11 +3,16 @@ //! //! This is the producer half of the completion contract stated by //! [`weaveffi_core::plan::AsyncProtocol`]: the callback fires exactly once, -//! from an arbitrary producer thread; borrowed result buffers (strings, -//! arrays) are freed by the producer after the callback returns, so the -//! consumer must copy them inside the callback; owned-object results transfer -//! ownership to the consumer. The callback's `err` slot follows the owning -//! function's [`ErrorStrategy`](weaveffi_core::plan::ErrorStrategy). +//! 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 result is released with +//! `weaveffi_free_bytes`; an owned-object result transfers ownership. 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). use proc_macro2::TokenStream; use quote::quote; @@ -108,44 +113,44 @@ fn lift_async_input( } /// Lower the future's output into the completion callback's *result* arguments -/// (the slots after `context` and `err`), returning -/// `(preamble, args, postamble)`. +/// (the slots after `context` and `err`), returning `(preamble, args)`. /// -/// The postamble runs *after* the callback returns and releases every buffer -/// the callback merely borrowed, per the plan's async contract -/// ([`weaveffi_core::plan::AsyncProtocol`]): strings and value buffers are -/// producer-owned and freed here (the consumer copies or decodes inside the -/// callback); an owned interface result transfers ownership to the consumer -/// and is not freed. `bytes` results are not yet supported. +/// 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 buffer with `weaveffi_free_bytes`, +/// and an owned interface result is adopted (the consumer eventually calls +/// `_destroy`). Ownership transfer is what lets consumers defer decoding past +/// the callback's return. `bytes` results are not yet supported. fn async_result_args( ty: &TypeRef, value: TokenStream, -) -> syn::Result<(TokenStream, Vec, TokenStream)> { +) -> syn::Result<(TokenStream, Vec)> { let none = TokenStream::new(); - // A buffered result crosses as a borrowed `(ptr, len)` pair the consumer - // decodes inside the callback; the encoding drops when the local `Vec` - // goes out of scope after the callback returns. + // A buffered result crosses as an owned `(ptr, len)` pair the consumer + // decodes at its leisure and releases with `weaveffi_free_bytes`. if weaveffi_core::abi::is_buffered(ty) { return Ok(( - quote!(let __wv_res_buf = ::weaveffi::abi::encode_value(&(#value));), - vec![quote!(__wv_res_buf.as_ptr()), quote!(__wv_res_buf.len())], - none, + quote! { + let __wv_res_buf = ::weaveffi::abi::encode_value(&(#value)).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)], )); } Ok(match ty { - t if is_copy(t) && !matches!(t, TypeRef::Enum(_)) => (none.clone(), vec![value], none), - TypeRef::Enum(_) => (none.clone(), vec![quote!((#value) as i32)], none), + t if is_copy(t) && !matches!(t, TypeRef::Enum(_)) => (none.clone(), vec![value]), + TypeRef::Enum(_) => (none.clone(), vec![quote!((#value) as i32)]), + // An owned string the consumer releases with `weaveffi_free_string`. TypeRef::StringUtf8 | TypeRef::BorrowedStr => ( quote!(let __wv_res = ::weaveffi::abi::string_to_c_ptr(&(#value));), vec![quote!(__wv_res)], - quote!(::weaveffi::abi::free_string(__wv_res);), ), // An owned-object result: the callback adopts the pointer (the plan's // `result_adopt`) and the consumer eventually calls `_destroy`. TypeRef::Interface(_) => ( none.clone(), vec![quote!(::std::boxed::Box::into_raw(::std::boxed::Box::new(#value)))], - none, ), _ => return Err(unsupported("async return", "result type")), }) @@ -212,8 +217,11 @@ pub(crate) fn gen_async_function( ::weaveffi::abi::MARSHAL_ERROR_CODE, "self is null", ); - callback(context, &mut __wv_e #(, #sentinels)*); - ::weaveffi::abi::error_clear(&mut __wv_e); + callback( + context, + ::std::boxed::Box::into_raw(::std::boxed::Box::new(__wv_e)) + #(, #sentinels)* + ); return; } let __wv_self_addr = __wv_self as usize; @@ -242,21 +250,26 @@ pub(crate) fn gen_async_function( } let is_throws = throws(f); - let (result_pre, success_args, result_post) = match &f.ret { + let (result_pre, success_args) = match &f.ret { Some(ty) => async_result_args(ty, quote!(__wv_val))?, - None => (TokenStream::new(), Vec::new(), TokenStream::new()), + None => (TokenStream::new(), Vec::new()), }; - // Borrowed result buffers are released only after the callback returns: - // the consumer copies inside the callback, per the async plan contract. + // Result buffers and strings transfer ownership to the consumer, which + // releases them with the runtime free symbols, per the async plan + // contract. let success_call = quote! { #result_pre callback(__wv_ctx as *mut ::std::ffi::c_void, ::std::ptr::null_mut() #(, #success_args)*); - #result_post }; + // A non-null `err` is heap-boxed: the consumer copies what it needs and + // releases it with `weaveffi_error_free`. let fail_call = quote! { - callback(__wv_ctx as *mut ::std::ffi::c_void, &mut __wv_e #(, #sentinels)*); - ::weaveffi::abi::error_clear(&mut __wv_e); + callback( + __wv_ctx as *mut ::std::ffi::c_void, + ::std::boxed::Box::into_raw(::std::boxed::Box::new(__wv_e)) + #(, #sentinels)* + ); }; let dispatch = if is_throws { diff --git a/crates/weaveffi-macros/src/codegen/mod.rs b/crates/weaveffi-macros/src/codegen/mod.rs index 5f3ea18a..3aaff480 100644 --- a/crates/weaveffi-macros/src/codegen/mod.rs +++ b/crates/weaveffi-macros/src/codegen/mod.rs @@ -75,6 +75,10 @@ pub fn expand_module(item_mod: &syn::ItemMod) -> syn::Result { }; weaveffi_core::validate::resolve_type_refs(&mut api); resolve_sibling_named_refs(&mut api); + // After `resolve_type_refs` plus the sibling rewrite above, no `Named` + // reference remains, which is exactly the contract `assume_resolved` + // asserts (see the doc on `resolve_sibling_named_refs`). + let api = weaveffi_core::ResolvedApi::assume_resolved(api); // 2. Build the canonical lowered model. Nested modules are flattened into // one binding each, keyed here by their path segments (e.g. `["kv", diff --git a/crates/weaveffi/tests/runtime.rs b/crates/weaveffi/tests/runtime.rs index 2b66f61c..6d8622e9 100644 --- a/crates/weaveffi/tests/runtime.rs +++ b/crates/weaveffi/tests/runtime.rs @@ -650,8 +650,8 @@ fn async_struct_result_completes_via_callback() { use std::time::Duration; type Msg = (bool, i64, String); - // The buffered result is borrowed for the callback's duration: decode - // inside the callback (the producer frees the encoding afterward). + // 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, @@ -660,11 +660,15 @@ fn async_struct_result_completes_via_callback() { ) { 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(); @@ -693,6 +697,10 @@ fn async_result_ok_and_err_paths() { 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(); } diff --git a/docs/src/architecture.md b/docs/src/architecture.md index 15006844..21bb2dd3 100644 --- a/docs/src/architecture.md +++ b/docs/src/architecture.md @@ -18,7 +18,8 @@ Parse ── weaveffi-ir::parse (IDL) | weaveffi-bridge (.rs): builds an │ ▼ Validate ── weaveffi-core::validate: rejects errors, collects warnings, - │ and rewrites every parsed type reference to its resolved kind + │ rewrites every parsed type reference to its resolved kind, + │ and wraps the result in a `ResolvedApi` proof type │ ▼ Resolve ── weaveffi-cli `CliConfig`: merges --config TOML and the @@ -174,9 +175,14 @@ pass (`weaveffi_core::validate::resolve_type_refs`, run by `validate_api` after the rule checks pass) rewrites each occurrence into `Record` (a struct), `RichEnum` (an algebraic enum), `Enum` (a C-style enum), or `Interface`, and qualifies cross-module references -with the owning module's dot-joined path. Generators run strictly -post-resolution: no `Named` reference remains, and a generator may -treat one as a bug (the core ABI lowering panics on it). `Record` and +with the owning module's dot-joined path. The post-resolution +guarantee is carried in the type system: +`weaveffi_core::resolved::ResolvedApi` wraps an `Api` that has passed +validation and resolution, and it is the only way to reach +`BindingModel::build`, the `LanguageBackend` trait, and the +`Orchestrator`. Generators therefore run strictly post-resolution: no +`Named` reference remains, and a generator may treat one as a bug (the +core ABI lowering panics on it). `Record` and `RichEnum` are *buffered* types (`weaveffi_core::abi::is_buffered` returns true for both, along with optionals, lists, and maps): they cross the ABI by value as one serialized `(const uint8_t*, size_t)` @@ -184,10 +190,9 @@ pair (see the [Value Buffer Protocol](reference/value-buffers.md)), while `Enum` lowers by value as an integer. Every IR type derives `Debug`, `Clone`, `PartialEq`, `Serialize`, and -`Deserialize`. `Eq` is derived where possible; a few types (`Api`, -`Module`, `StructDef`, `StructField`) intentionally omit `Eq` because -they transitively contain `f64` (in default values) or -`serde_yaml::Value`. +`Deserialize`. `Eq` is derived everywhere except `Api` itself, which +omits it because its `generators` overrides hold `toml::Value`, which +contains `f64`. `TypeRef` (de)serializes as a string with custom syntax (`i32`, `handle`, `[T]`, `{K:V}`, `T?`, `&str`, `&[u8]`). The parser is @@ -196,7 +201,7 @@ JSON Schema export rely on it. ### Schema versioning -`CURRENT_SCHEMA_VERSION` (currently `"0.6.0"`) lives in +`CURRENT_SCHEMA_VERSION` (currently `"0.7.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: @@ -211,10 +216,15 @@ external contract; this section is the implementation note. ## Validation -`weaveffi_core::validate::validate_api` is the single entry point. -It returns a `Vec` (errors that must be fixed before -generation) and a separate `Vec` (advisory; the -`lint` subcommand surfaces these). +`weaveffi_core::validate::validate_api` is the single entry point. It +consumes the parsed `Api` and returns +`Result`: on success the API has +passed every rule check and type-reference resolution, and the +`ResolvedApi` proof type is the only way into the generation stage; on +failure the diagnostics carry every error found, each renderable as an +actionable miette report. Advisory warnings are separate: +`validate::collect_warnings` returns `Vec`, which +the `lint` subcommand surfaces. Errors enforced today: @@ -339,10 +349,10 @@ pub trait Generator: Send + Sync { fn name(&self) -> &'static str; /// Render the bindings under `out_dir`. - fn generate(&self, api: &Api, out_dir: &Utf8Path, config: &Self::Config) -> Result<()>; + fn generate(&self, api: &ResolvedApi, out_dir: &Utf8Path, config: &Self::Config) -> Result<()>; /// Files `generate` would write (used by `--dry-run` and `diff`). - fn output_files(&self, api: &Api, out_dir: &Utf8Path, config: &Self::Config) -> Vec; + fn output_files(&self, api: &ResolvedApi, out_dir: &Utf8Path, config: &Self::Config) -> Vec; } ``` @@ -370,7 +380,7 @@ pub trait LanguageBackend: Send + Sync { /// The single required hook: assemble every output file. Rendering is /// pure; the driver performs the actual writes. - fn files(&self, api: &Api, model: &BindingModel, + fn files(&self, api: &ResolvedApi, model: &BindingModel, out_dir: &Utf8Path, config: &Self::Config) -> Vec; /// Canonical per-module walk (error → enums → structs → interfaces → @@ -390,11 +400,13 @@ backend's `prefix`), calls `files`, and writes each `OutputFile` (creating parent directories). `backend::output_files` calls the same `files` and returns the sorted path list, so `generate` and `output_files` are derived from a single source and **cannot drift**. -Python is the reference single-pass backend (it overrides the per-entity -hooks and composes `emit_members`); Ruby, .NET, Node, and Android are -multi-pass (their FFI declarations, wrapper classes, and secondary -surfaces such as the JNI C shim are emitted in their own passes inside -`files`). +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 Android, Node, and Wasm each render +parallel files (Kotlin + JNI C, addon C + JS, 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 @@ -411,8 +423,8 @@ managed by the `CodeWriter` toolkit (see below) rather than by hand-rolled - `codegen::writer::CodeWriter`: the structured code-emission toolkit (see [The `CodeWriter` emission toolkit](#the-codewriter-emission-toolkit)). - `codegen::common`: module-tree traversal (`walk_modules`, - `walk_modules_with_path`), the `is_c_pointer_type` ABI classifier, - doc-comment emission (`emit_doc`), and `pascal_case` naming. + `walk_modules_with_path`), doc-comment emission (`emit_doc`), and + `pascal_case` naming. - `plan`: the marshalling plan, the language-neutral calling contracts every backend renders (see [The marshalling plan](#the-marshalling-plan)). - `wire`: the canonical classification of every IR type into its wire @@ -589,16 +601,17 @@ independently (and inconsistently): destroy-exactly-once handle lifecycle. - **Async.** `AsyncProtocol` (`AsyncBinding::protocol`): the completion-callback contract: the callback fires exactly once from an - arbitrary producer thread, borrowed result buffers are valid only for - the callback's duration, and owned-object results are adopted by the - consumer. + 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. 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 free -borrowed result buffers after the callback returns, iterator thunks +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`, 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 diff --git a/docs/src/generators/android.md b/docs/src/generators/android.md index 1058fee9..ebf71a43 100644 --- a/docs/src/generators/android.md +++ b/docs/src/generators/android.md @@ -47,7 +47,7 @@ layer bridges them to the C ABI. ## Example IDL → generated code ```yaml -version: "0.6.0" +version: "0.7.0" modules: - name: contacts enums: @@ -447,13 +447,15 @@ static void weaveffi_tasks_run_task_jni_cb(void* context, weaveffi_error* err, The completion callback fires exactly once, on a producer thread. Result buffers passed to it (strings, byte arrays, and buffered values, -which arrive as a `(result_ptr, result_len)` pair) are borrowed from -the producer for the callback's duration, so the shim copies them into -Java objects (`NewStringUTF`, `SetByteArrayRegion`) inside the callback -and never frees them; the Kotlin layer then decodes buffered results -into their value types. An owned interface result is the exception: the -callback receives ownership, resumes the continuation with the raw -handle, and the suspend wrapper adopts it. An exception thrown by the +which arrive as a `(result_ptr, result_len)` pair) are owned by the +consumer, so the shim copies them into Java objects (`NewStringUTF`, +`SetByteArrayRegion`) and then releases them with +`weaveffi_free_string` or `weaveffi_free_bytes`; the Kotlin layer then +decodes buffered results into their value types. A reported error is +heap-boxed: the shim copies its fields and releases it with +`weaveffi_error_free`. An owned interface result transfers ownership +too: the callback resumes the continuation with the raw handle, and +the suspend wrapper adopts it. An exception thrown by the resumed coroutine goes through the same `weaveffi_jni_handle_uncaught` path as listener exceptions (see [Callbacks and listeners](#callbacks-and-listeners)). diff --git a/docs/src/generators/c.md b/docs/src/generators/c.md index b594e10b..07b5210f 100644 --- a/docs/src/generators/c.md +++ b/docs/src/generators/c.md @@ -79,7 +79,7 @@ helpers, is rewritten with the new prefix. ## Example IDL → generated code ```yaml -version: "0.6.0" +version: "0.7.0" modules: - name: contacts enums: @@ -492,15 +492,14 @@ The launcher returns immediately; WeaveFFI invokes the callback exactly once, with either a result or a populated error, from the producer's worker thread. -Ownership inside the callback follows the async contract. Result +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) are borrowed: they stay owned by the producer and are valid -only for the callback's duration, so copy or decode anything you need -before returning and don't free them. Owned interface results are the -exception: the callback receives ownership of the object pointer and -must eventually pass it to the matching `_destroy`. The `err` struct is -likewise borrowed; copy its code, message, and payload inside the -callback. +results), then release them with `weaveffi_free_string` or +`weaveffi_free_bytes`. Owned interface results transfer ownership of +the object pointer, which you must eventually pass to the matching +`_destroy`. A non-null `err` is heap-boxed: copy its code, message, +and payload, then release it exactly once with `weaveffi_error_free`. For `cancellable: true` functions the launcher gains a `weaveffi_cancel_token*` slot before the callback, and the runtime diff --git a/docs/src/generators/cpp.md b/docs/src/generators/cpp.md index 905ebed0..b21be933 100644 --- a/docs/src/generators/cpp.md +++ b/docs/src/generators/cpp.md @@ -48,7 +48,7 @@ directory can be dropped into any CMake build. ## Example IDL → generated code ```yaml -version: "0.6.0" +version: "0.7.0" modules: - name: contacts enums: @@ -429,12 +429,14 @@ inline std::future fetch_contact(int32_t id) { Use it with `.get()` (blocking) or compose with your event loop. The completion lambda runs exactly once, on an arbitrary producer thread; it -completes (or rejects) the promise and then deletes it. Result buffers -passed to the callback (strings, bytes, buffered values, and the error -message) are borrowed from the producer for the callback's duration, so -the lambda copies or unpacks them into C++ values before returning and -never frees them. An owned interface result is the exception: the -callback receives ownership and adopts the pointer into a RAII wrapper. An async callable with `throws: true` rejects with the +completes (or rejects) the promise and then deletes it. Everything +passed to the callback is owned by the consumer: the lambda copies or +unpacks string, bytes, and buffered-value results into C++ values and +then releases them with `weaveffi_free_string` or +`weaveffi_free_bytes`, and it releases a heap-boxed error with +`weaveffi_error_free` after copying its fields. An owned interface +result transfers ownership too: the +callback adopts the pointer into a RAII wrapper. An async callable with `throws: true` rejects with the module's typed domain exception (`detail::make_kv_error` and friends); one without `throws` rejects with the generic `WeaveFFIError` only when the producer has a bug. diff --git a/docs/src/generators/dart.md b/docs/src/generators/dart.md index 07ff74c4..cfdf1a42 100644 --- a/docs/src/generators/dart.md +++ b/docs/src/generators/dart.md @@ -48,7 +48,7 @@ Booleans cross as `Int32` (`0`/`1`) and the wrapper converts both ways. ## Example IDL → generated code ```yaml -version: "0.6.0" +version: "0.7.0" modules: - name: contacts enums: @@ -496,12 +496,15 @@ Future runTask(String name) { if (err.address != 0 && err.ref.code != 0) { final code = err.ref.code; final msg = err.ref.message.toDartString(); - _weaveffiErrorClear(err); + _weaveffiErrorFree(err); completer.completeError(_mapTaskException(code, msg)); return; } - // TaskResult is a record: decode the borrowed value buffer. - completer.complete(_unpackTaskResult(resultPtr, resultLen)); + // TaskResult is a record: copy the owned value buffer, free it, + // then decode from the copy. + final resultData = _copyNativeBytes(resultPtr, resultLen); + _weaveffiFreeBytes(resultPtr, resultLen); + completer.complete(_unpackTaskResult(_BufferReader(resultData))); } catch (e) { completer.completeError(e); } finally { @@ -527,14 +530,18 @@ once; input buffers are released in `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 borrows -string, bytes, and buffered results (records, rich enums, optionals, -lists, and maps arrive as a `(resultPtr, resultLen)` pair), so the -callback body copies or decodes them into Dart values before it returns -and never frees them (the producer does, after the callback returns). -An owned interface result is the exception: the callback receives -ownership and the wrapper adopts the pointer; its `dispose()` owns the -eventual destroy. +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 (`_mapTaskException` above, diff --git a/docs/src/generators/dotnet.md b/docs/src/generators/dotnet.md index 53d7ce39..92d93e26 100644 --- a/docs/src/generators/dotnet.md +++ b/docs/src/generators/dotnet.md @@ -52,7 +52,7 @@ package named `kvstore` produces `Kvstore.cs` inside ## Example IDL → generated code ```yaml -version: "0.6.0" +version: "0.7.0" modules: - name: contacts enums: @@ -517,11 +517,13 @@ public static async Task RunTask(string name) `WeaveFFIException`. - Result ownership follows the async contract: string, bytes, and buffered results (records, rich enums, optionals, arrays, maps, - arriving as a `(result, resultLen)` pair) are borrowed for the - callback's duration, so the callback copies or decodes them into - managed values and never frees them (the producer does after the - callback returns). An owned interface result is the exception: the - callback receives ownership and the wrapper adopts the pointer. + 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 `NativeMethods.weaveffi_free_string` or + `NativeMethods.weaveffi_free_bytes`. A reported error is heap-boxed + and released with `NativeMethods.weaveffi_error_free` after its + fields are copied. An owned interface result transfers ownership + too: the wrapper adopts the pointer. Async interface methods follow the same pattern as instance methods: `await store.Compact()` returns `Task`. diff --git a/docs/src/generators/go.md b/docs/src/generators/go.md index 2e2484fe..c67c26cf 100644 --- a/docs/src/generators/go.md +++ b/docs/src/generators/go.md @@ -53,7 +53,7 @@ Booleans map to `C._Bool`, matching CGo's representation of `_Bool`. ## Example IDL → generated code ```yaml -version: "0.6.0" +version: "0.7.0" modules: - name: contacts enums: @@ -185,7 +185,7 @@ The Go module path defaults to `weaveffi`; override it via the generator config: ```yaml -version: "0.6.0" +version: "0.7.0" modules: - name: math functions: @@ -520,11 +520,12 @@ func (s *Store) Compact() (int64, error) { The completion callback fires exactly once, on a producer thread. The trampoline removes the channel from the registry with `wvCallbackTake` (one-shot), converts the C error or result inside the callback (result -buffers such as strings and buffered values are borrowed from the -producer for the callback's duration, so the trampoline copies or -decodes them into Go memory and never frees them; an owned interface -result is adopted into a wrapper instead), and sends a single -`wvOutcome…` value: +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`; an owned interface result is +adopted into a wrapper instead), and sends a single `wvOutcome…` +value: ```go //export goWv_weaveffi_kv_Store_compact_callback @@ -535,7 +536,7 @@ func goWv_weaveffi_kv_Store_compact_callback(context unsafe.Pointer, err *C.weav } ch := v.(chan wvOutcomeKvStoreCompact) if err != nil && err.code != 0 { - ch <- wvOutcomeKvStoreCompact{err: wvMapKv(wvTakeError(err))} + ch <- wvOutcomeKvStoreCompact{err: wvMapKv(wvTakeBoxedError(err))} return } ch <- wvOutcomeKvStoreCompact{val: int64(result)} diff --git a/docs/src/generators/node.md b/docs/src/generators/node.md index 20624bcf..19b70428 100644 --- a/docs/src/generators/node.md +++ b/docs/src/generators/node.md @@ -51,7 +51,7 @@ camelCased function wrappers. ## Example IDL → generated code ```yaml -version: "0.6.0" +version: "0.7.0" modules: - name: contacts enums: @@ -353,11 +353,15 @@ static void weaveffi_tasks_run_task_napi_cb(void* context, weaveffi_error* err, if (err != NULL && err->code != 0) { ctx->err_code = err->code; ctx->err_msg = err->message ? strdup(err->message) : strdup("unknown error"); - } else if (result_ptr != NULL && result_len > 0) { - ctx->result = (uint8_t*)malloc(result_len); - memcpy(ctx->result, result_ptr, result_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); } ``` @@ -365,11 +369,13 @@ static void weaveffi_tasks_run_task_napi_cb(void* context, weaveffi_error* err, The completion callback fires exactly once, on the producer thread. Result buffers passed to it (strings, byte buffers, and the serialized value buffers of buffered results such as the `TaskResult` record here) -are borrowed for the callback's duration, so the callback deep-copies -them (note the `strdup` and `memcpy` above) before returning and never -frees them. Owned interface results are the exception: the callback -receives ownership of the object pointer, which the settle callback -wraps into the JS-side owner. +are owned by the consumer, so the callback deep-copies them (note the +`strdup` and `memcpy` above) 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. Owned interface results transfer ownership too: the +callback receives the object pointer, which the settle callback wraps +into the JS-side owner. Rejected promises carry the C error message plus a numeric `code` property; the loader rebrands the rejection into the module's typed diff --git a/docs/src/generators/python.md b/docs/src/generators/python.md index 31810059..1dc876d7 100644 --- a/docs/src/generators/python.md +++ b/docs/src/generators/python.md @@ -65,7 +65,7 @@ standard fixed-width boolean type across ABIs. ## Example IDL → generated code ```yaml -version: "0.6.0" +version: "0.7.0" modules: - name: contacts enums: @@ -494,7 +494,7 @@ async def compact(self) -> int: 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_clear(ctypes.byref(err.contents)) + _lib.weaveffi_error_free(err) _state["err"] = _kv_error_from(_code, _msg) else: _state["val"] = result @@ -522,11 +522,14 @@ async def compact(self) -> int: 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 producer and valid -only for the callback's duration, so the wrapper copies or decodes them -inside the callback and never frees them. Owned interface results are -the exception: the callback receives ownership and adopts the pointer -into a wrapper class. Conversion happens on the producer thread; the +value buffers of buffered results) are owned by the consumer, so the +wrapper copies or decodes them inside the callback and then releases +them with `weaveffi_free_string` or `weaveffi_free_bytes` (the +`_take_string` and `_take_buffer` helpers pair the copy with the +free). A reported error is heap-boxed and released with +`weaveffi_error_free` after its fields are copied. Owned interface +results transfer ownership too: the callback adopts the pointer into a +wrapper class. 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. diff --git a/docs/src/generators/ruby.md b/docs/src/generators/ruby.md index 8775fa58..e0a00b37 100644 --- a/docs/src/generators/ruby.md +++ b/docs/src/generators/ruby.md @@ -57,7 +57,7 @@ directions. ## Example IDL → generated code ```yaml -version: "0.6.0" +version: "0.7.0" modules: - name: contacts enums: @@ -513,11 +513,13 @@ def self.run_task(name) ) do |_context, err_ptr, result_ptr, result_len| err = err_ptr.null? ? nil : ErrorStruct.new(err_ptr) if err && err[:code] != 0 - # ... read code/message, weaveffi_error_clear ... + # ... read code/message/payload, then weaveffi_error_free ... queue << task_error_from(code, msg) else - # TaskResult is a record: decode the borrowed value buffer. - queue << _wv_read_task_result(WvBufferReader.new(result_ptr.read_string(result_len))) + # TaskResult is a record: copy the owned buffer, free it, decode. + reader = WvBufferReader.new(result_ptr.read_string(result_len)) + weaveffi_free_bytes(result_ptr, result_len) unless result_ptr.null? + queue << _wv_read_task_result(reader) end end weaveffi_tasks_run_task_async(name, callback, FFI::Pointer::NULL) @@ -542,13 +544,14 @@ mid-flight. Result ownership follows the async contract: string, bytes, and buffered results (records, rich enums, optionals, arrays, and maps, -arriving as a pointer-plus-length pair) are borrowed for the callback's -duration, so the callback copies or decodes them into Ruby values -before it returns and never frees them; the producer does after the -callback returns. An owned interface result is the exception: the -callback receives ownership, and the wrapper adopts the pointer into -its `FFI::AutoPointer`, so the destructor runs on GC or an explicit -`destroy`. +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 wrapper adopts the +pointer into its `FFI::AutoPointer`, so the destructor runs on GC or +an explicit `destroy`. For functions marked `cancellable: true` the C launcher takes an extra cancel-token parameter. The wrapper always passes `FFI::Pointer::NULL` diff --git a/docs/src/generators/swift.md b/docs/src/generators/swift.md index ac7c32e0..04cb1e15 100644 --- a/docs/src/generators/swift.md +++ b/docs/src/generators/swift.md @@ -53,7 +53,7 @@ stays buildable under any name. ## Example IDL → generated code ```yaml -version: "0.6.0" +version: "0.7.0" modules: - name: contacts enums: @@ -396,10 +396,14 @@ public static func runTask(name: String) async throws -> TaskResult { 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) contRef.value.resume(throwing: mapTasks(code: code, message: msg)) } else { - // TaskResult is a record: decode the borrowed value buffer. - var reader = WvReader(ptr: resultPtr, len: resultLen) + // TaskResult is a record: copy the owned value buffer, + // free it, then decode from the copy. + let resultBytes = [UInt8](UnsafeBufferPointer(start: resultPtr, count: resultLen)) + weaveffi_free_bytes(UnsafeMutablePointer(mutating: resultPtr), resultLen) + var reader = WvReader(bytes: resultBytes) contRef.value.resume(returning: wvReadTaskResult(&reader)) } }, ctx) @@ -411,12 +415,14 @@ public static func runTask(name: String) async throws -> TaskResult { 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 (strings, bytes, and buffered -values) are borrowed from the producer for the callback's duration: the -wrapper copies or decodes them (for example `String(cString:)` on the -error message, or `wvReadTaskResult` above) before the callback returns -and never frees them. An owned interface result is the exception: the -callback adopts the pointer into a new wrapper instance, whose `deinit` -eventually frees it. +values) are owned by the consumer: the wrapper copies or decodes them +(for example `String(cString:)`, or the byte-array copy above) and +then releases them with `weaveffi_free_string` or +`weaveffi_free_bytes`. A reported error is heap-boxed: the wrapper +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 instance, +whose `deinit` eventually frees it. `run_task` declares `throws: true`, so the continuation rejects with the typed `TaskError` (via `mapTasks`). An async callable without `throws` is diff --git a/docs/src/getting-started.md b/docs/src/getting-started.md index 66059eb6..934f5a74 100644 --- a/docs/src/getting-started.md +++ b/docs/src/getting-started.md @@ -40,7 +40,7 @@ Describe the API once in a language-neutral IDL. Create `math.yml` with a record and a function: ```yaml -version: "0.6.0" +version: "0.7.0" package: name: my-math version: "0.1.0" diff --git a/docs/src/guides/async.md b/docs/src/guides/async.md index dd8f728d..e230f63d 100644 --- a/docs/src/guides/async.md +++ b/docs/src/guides/async.md @@ -15,9 +15,9 @@ future is the module's typed domain error (see the 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 the result it -receives is either borrowed (copy it inside the callback) or adopted -(own it and destroy it later). See +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. @@ -43,7 +43,7 @@ Avoid async for: ### 1. Declare the function in the IDL ```yaml -version: "0.6.0" +version: "0.7.0" modules: - name: net errors: @@ -110,22 +110,24 @@ pub extern "C" fn weaveffi_net_fetch_data_async( let ctx = context as usize; std::thread::spawn(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); - // The string is borrowed by the callback: the producer frees it - // after the callback returns, so the consumer must have copied. - abi::free_string(payload); }); } ``` The async launcher symbol always carries the `_async` suffix (`weaveffi_net_fetch_data_async`), keeping the name free for a possible -synchronous variant. Note who frees the result: buffer results -(strings, bytes, and the serialized value buffers of buffered results -such as records, rich enums, lists, and maps) are owned by the -producer, which releases them after the callback returns; the -macro-generated launchers do exactly this. Owned interface results are -the exception: the callback receives ownership of the object pointer. +synchronous variant. Note who frees the result: everything passed to +the callback is owned by the consumer. String results are released +with `weaveffi_free_string`, byte and serialized value buffers (the +buffered results such as records, rich enums, lists, and maps) with +`weaveffi_free_bytes`, and a reported error (heap-boxed on the failure +path) with `weaveffi_error_free`; owned interface results transfer +ownership of the object pointer. The macro-generated launchers +allocate results this way for you, and every generated wrapper frees +them for you. See [Result ownership and threading](#result-ownership-and-threading). ### 3. Call it from each target @@ -246,16 +248,17 @@ The completion contract has three clauses, stated once in future, a JS `Promise`, a Swift continuation, a C# `TaskCompletionSource`, a Go channel) exactly once and then releases the registration. -2. **Borrowed results.** Result buffers passed to the callback - (strings, bytes, and buffered results delivered as a +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 producer and valid **only - for the callback's duration**: the wrapper copies or decodes them - before the callback returns and must not free them. The producer - releases them after the callback returns; the macro-generated - launchers do this for you. Owned interface results (including - `Interface?`) are the exception: the callback receives ownership and - adopts the pointer into the wrapper's disposal idiom, which + 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. Owned interface results + (including `Interface?`) transfer ownership 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, so the wrapper hops back to its native scheduler @@ -264,16 +267,15 @@ The completion contract has three clauses, stated once in Swift continuation) rather than resolving inline where the target's runtime forbids it. -The error struct passed to the callback is also producer-owned and -borrowed for the callback's duration: the wrapper copies the code and -message inside the callback, and the producer releases the message -afterward. A wrapper may also call `weaveffi_error_clear` itself; the -clear is idempotent (it nulls the message pointer), so the producer's -own release stays safe. +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 every buffer before returning, adopt object -pointers, and never free a borrowed result. +your callback: copy or decode every buffer and then free it, release a +non-null error with `weaveffi_error_free`, and adopt object pointers. ### Per-target async surface @@ -350,11 +352,15 @@ For every async-capable target: - **Returning `null` instead of invoking the callback**: the contract is that the callback fires **exactly once** for every async call, including on cancellation. -- **Holding a result pointer past the callback**: buffer results are - producer-owned and freed as soon as the callback returns. Copy the - data inside the callback; a stashed pointer dangles. -- **Freeing a borrowed result inside the callback**: strings, bytes, - and serialized value buffers belong to the producer, which frees - them itself. Freeing them in the callback double-frees. The only - pointers the callback owns are interface results, which it must - eventually `_destroy` exactly once. +- **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. diff --git a/docs/src/guides/config.md b/docs/src/guides/config.md index bcde0161..9aae1892 100644 --- a/docs/src/guides/config.md +++ b/docs/src/guides/config.md @@ -60,7 +60,7 @@ module-prefixed wrapper names across every supporting target. ### 2. Embed `generators:` in the IDL ```yaml -version: "0.6.0" +version: "0.7.0" modules: - name: math functions: diff --git a/docs/src/guides/errors.md b/docs/src/guides/errors.md index 44ee464f..29169f14 100644 --- a/docs/src/guides/errors.md +++ b/docs/src/guides/errors.md @@ -41,7 +41,7 @@ Reach for this guide when: ### Declare a domain and opt in with `throws` ```yaml -version: "0.6.0" +version: "0.7.0" modules: - name: contacts errors: diff --git a/docs/src/guides/memory.md b/docs/src/guides/memory.md index 67faff54..8f0e4fa9 100644 --- a/docs/src/guides/memory.md +++ b/docs/src/guides/memory.md @@ -141,14 +141,14 @@ Everything above describes **synchronous** returns: the consumer receives an owned value and owes the matching release call after copying it. -**Async results invert the buffer rule.** The buffers passed to an -async completion callback (strings, bytes, and the serialized value -buffers of buffered results) are borrowed: they stay owned by the -producer, are valid only for the callback's duration, and are freed by -the producer after the callback returns. The consumer copies or decodes -inside the callback and must not free them. Owned interface results -(including `Interface?`) are the exception in both directions: the -callback receives ownership, adopts the pointer, and eventually calls +**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. Owned interface results (including `Interface?`) transfer +ownership too: the callback adopts the pointer and eventually calls `_destroy`, exactly as a synchronous interface return would. See [Result ownership and threading](async.md#result-ownership-and-threading). @@ -220,11 +220,11 @@ 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 nulls -the pointer, so clearing an already-cleared slot is safe. That matters -for async completion callbacks, where the error struct is borrowed -from the producer (which releases the message itself after the -callback returns); a consumer that clears it anyway causes no -double-free. +the pointer, 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. ### Thread safety @@ -252,9 +252,10 @@ queue.sync { | Interface instance | Rust | `*_destroy` | Call exactly once; methods borrow | | Iterator handle | Rust | the iterator's `_destroy` | Exactly once: on exhaustion or abandonment | | Iterator element | Rust | `weaveffi_free_string` / `weaveffi_free_bytes` / nothing | Each `_next` yields a consumer-owned element | -| Async result buffer | Rust | none (borrowed) | Producer frees after the callback returns; copy or decode inside it | +| Async result buffer | Rust | `weaveffi_free_string` / `weaveffi_free_bytes` | Consumer-owned: copy or decode, then free | | Async interface result | Rust | `*_destroy` | Callback adopts ownership | -| Error message and payload | Rust | `weaveffi_error_clear` | Clears code, frees message and payload; idempotent | +| 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 | ## Pitfalls @@ -279,10 +280,10 @@ queue.sync { `[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. -- **Freeing an async result buffer**: buffers passed to a completion - callback are producer-owned and freed by the producer after the - callback returns. Copy inside the callback; freeing there - double-frees. +- **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 cannot double-destroy; diff --git a/docs/src/guides/producer-macro.md b/docs/src/guides/producer-macro.md index c22a4788..569a9a26 100644 --- a/docs/src/guides/producer-macro.md +++ b/docs/src/guides/producer-macro.md @@ -104,8 +104,9 @@ the generated IDL and every binding. Call `weaveffi::export_runtime!()` exactly once in the crate (not per module). It emits the fixed C ABI runtime symbols (`weaveffi_free_string`, -`weaveffi_free_bytes`, `weaveffi_error_clear`, the cancel-token helpers, and -the arena) that every binding links against. +`weaveffi_free_bytes`, `weaveffi_error_clear`, `weaveffi_error_free`, +the cancel-token helpers, and the arena) that every binding links +against. ## How values cross the boundary diff --git a/docs/src/reference/idl.md b/docs/src/reference/idl.md index 627e0b4f..2be3de06 100644 --- a/docs/src/reference/idl.md +++ b/docs/src/reference/idl.md @@ -26,7 +26,7 @@ and objects: ```text # yaml-language-server: $schema=./weaveffi.schema.json -version: "0.6.0" +version: "0.7.0" package: name: my_app version: "1.0.0" @@ -50,7 +50,7 @@ A complete, validating example lives at the bottom of this page in the | Field | Type | Required | Description | |--------------|-----------------------------|----------|------------------------------------------| -| `version` | string | yes | Schema version; only the current version (`"0.6.0"`) is accepted | +| `version` | string | yes | Schema version; only the current version (`"0.7.0"`) is accepted | | `package` | Package | no | Publishable identity stamped into every generated manifest (see [Package metadata](#package-metadata)) | | `modules` | array of Module | yes | One or more modules | | `generators` | map of string to object | no | Per-generator configuration (see [generators section](#generators-section)) | @@ -144,7 +144,7 @@ spelling. ### Package example ```yaml -version: "0.6.0" +version: "0.7.0" package: name: kvstore version: "1.0.0" @@ -196,7 +196,7 @@ parameters and return types. ### Primitive examples ```yaml -version: "0.6.0" +version: "0.7.0" modules: - name: primitives structs: @@ -276,7 +276,7 @@ knows how to spell the handle's type. At the C ABI level, `handle` is still a `uint64_t`. ```yaml -version: "0.6.0" +version: "0.7.0" modules: - name: sessions structs: @@ -326,12 +326,11 @@ Each field: | `name` | string | yes | Field name | | `type` | TypeRef | yes | Field type | | `doc` | string | no | Documentation string | -| `default` | value | no | Default value for this field | ### Struct example ```yaml -version: "0.6.0" +version: "0.7.0" modules: - name: geometry structs: @@ -358,10 +357,8 @@ modules: fields: - name: timeout type: i32 - default: 30 - name: retries type: i32 - default: 3 - name: label type: "string?" @@ -417,7 +414,7 @@ Each variant: ### Enum example ```yaml -version: "0.6.0" +version: "0.7.0" modules: - name: contacts enums: @@ -451,7 +448,7 @@ Swift `enum` with associated values). A *unit* variant (no `fields`) and a *data variant may coexist in the same enum. ```yaml -version: "0.6.0" +version: "0.7.0" modules: - name: shapes enums: @@ -563,7 +560,7 @@ positions. A trimmed version of the `kvstore` sample's `Store` interface: ```yaml -version: "0.6.0" +version: "0.7.0" modules: - name: kv errors: @@ -653,7 +650,7 @@ the default is null. ### Optional example ```yaml -version: "0.6.0" +version: "0.7.0" modules: - name: contacts structs: @@ -695,7 +692,7 @@ Wrap a type in `[T]` brackets to declare a list (variable-length sequence). ### List example ```yaml -version: "0.6.0" +version: "0.7.0" modules: - name: lists structs: @@ -740,7 +737,7 @@ valid `TypeRef`. ### Map example ```yaml -version: "0.6.0" +version: "0.7.0" modules: - name: maps structs: @@ -824,7 +821,7 @@ because every composite serializes into one value buffer: ### Nested type example ```yaml -version: "0.6.0" +version: "0.7.0" modules: - name: nested structs: @@ -871,7 +868,7 @@ one at a time and are suitable for large or streaming result sets. ### Iterator example ```yaml -version: "0.6.0" +version: "0.7.0" modules: - name: streaming structs: @@ -925,7 +922,7 @@ invokes a caller-provided function. ### Callback example ```yaml -version: "0.6.0" +version: "0.7.0" modules: - name: events functions: [] @@ -965,7 +962,7 @@ with subscribe/unsubscribe lifecycle management. ### Listener example ```yaml -version: "0.6.0" +version: "0.7.0" modules: - name: events functions: [] @@ -993,7 +990,7 @@ modules. ### Nested module example ```yaml -version: "0.6.0" +version: "0.7.0" modules: - name: app functions: @@ -1042,7 +1039,7 @@ For example, a nested `stats` module can take the parent `kv` module's `Store` interface as a parameter: ```yaml -version: "0.6.0" +version: "0.7.0" modules: - name: kv errors: @@ -1089,7 +1086,7 @@ Functions can be marked as asynchronous. See the behaviour. ```yaml -version: "0.6.0" +version: "0.7.0" modules: - name: net errors: @@ -1128,7 +1125,7 @@ return a list instead or make the function synchronous. Mark a function as deprecated with a migration message: ```yaml -version: "0.6.0" +version: "0.7.0" modules: - name: legacy functions: @@ -1150,7 +1147,7 @@ Generators propagate the deprecation message to the target language Mark a parameter as mutable when the callee may modify it in-place: ```yaml -version: "0.6.0" +version: "0.7.0" modules: - name: buffers functions: @@ -1171,7 +1168,7 @@ directly in the IDL file. This is an alternative to using a separate TOML configuration file with `--config`. ```yaml -version: "0.6.0" +version: "0.7.0" modules: - name: math functions: @@ -1242,7 +1239,7 @@ A full IDL combining structs, enums, optionals, lists, an interface, and a typed error domain (a trimmed version of the `contacts` sample): ```yaml -version: "0.6.0" +version: "0.7.0" modules: - name: contacts enums: @@ -1395,7 +1392,7 @@ generator re-cases them into its own idiom. ### Structured error payloads A code may declare `fields:` using the same shape as struct fields (`name`, -`type`, `doc`; the `default` slot is ignored). When a matching error is +`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 @@ -1425,7 +1422,7 @@ Declaring a domain reserves the codes; a function, method, or constructor joins the typed error path by declaring `throws: true`: ```yaml -version: "0.6.0" +version: "0.7.0" modules: - name: contacts errors: @@ -1510,7 +1507,7 @@ Per-target syntax: Example IDL: ```yaml -version: "0.6.0" +version: "0.7.0" modules: - name: docs structs: diff --git a/docs/src/reference/memory-error.md b/docs/src/reference/memory-error.md index 38ec2c31..48f07eaf 100644 --- a/docs/src/reference/memory-error.md +++ b/docs/src/reference/memory-error.md @@ -96,15 +96,15 @@ iteration ran to exhaustion or was abandoned early. ## Async completion callbacks -Result buffers passed to an async completion callback (strings, bytes, -and serialized value buffers for buffered results) are borrowed: the -producer owns them, they are valid only for the callback's duration, and -the producer frees them after the callback returns. Copy or decode inside -the callback; do not free them. Owned interface results are the exception: -the callback receives ownership of the object pointer and must eventually -call `_destroy`. The `err` struct is likewise borrowed; copy its code, -message, and payload inside the callback (clearing it anyway is safe -because the clear is idempotent). +Everything passed to an async completion callback is owned by the +consumer. Result buffers (strings, bytes, and serialized value buffers +for buffered results) are copied or decoded, then released with +`weaveffi_free_string` or `weaveffi_free_bytes`. Owned interface +results transfer ownership of the object pointer, which the consumer +must eventually `_destroy`. A non-null `err` is heap-boxed: copy its +code, message, and payload, then release the box exactly once with +`weaveffi_error_free` (which frees the message, the payload, and the +box itself). ## Handles and interfaces diff --git a/docs/src/reference/value-buffers.md b/docs/src/reference/value-buffers.md index 5c8d9f93..a4a90b7f 100644 --- a/docs/src/reference/value-buffers.md +++ b/docs/src/reference/value-buffers.md @@ -93,11 +93,11 @@ The consumer decodes the buffer and then releases it with `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 a *borrowed* -`(const uint8_t* result_ptr, size_t result_len)` pair. The producer frees the -encoding after the callback returns, so the consumer must decode (or copy) -inside the callback. Owned interface results are the exception: the callback -adopts the object pointer. +**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 and listener arguments.** Buffered callback arguments are borrowed `(ptr, len)` pairs valid only for the duration of the dispatch. diff --git a/docs/src/stability.md b/docs/src/stability.md index 32088e72..ea12cb75 100644 --- a/docs/src/stability.md +++ b/docs/src/stability.md @@ -52,7 +52,7 @@ contract." Things that have already changed during 0.x: 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.6.0` moved records, rich enums, optionals, lists, and maps to +- 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 @@ -103,7 +103,7 @@ tied to `weaveffi-ir`'s minor version: each `weaveffi-ir` minor bump corresponds to at most one schema version bump. [`CURRENT_SCHEMA_VERSION`](https://github.com/weavefoundry/weaveffi/blob/main/crates/weaveffi-ir/src/ir.rs) in `crates/weaveffi-ir/src/ir.rs` is the source of truth; the current -schema version is `0.6.0`. +schema version is `0.7.0`. Pre-1.0, **only the current schema version is accepted** (`SUPPORTED_VERSIONS` contains exactly `CURRENT_SCHEMA_VERSION`), so a diff --git a/docs/src/tutorials/android.md b/docs/src/tutorials/android.md index f2e070f8..7071ba2f 100644 --- a/docs/src/tutorials/android.md +++ b/docs/src/tutorials/android.md @@ -24,7 +24,7 @@ emulator or a physical device. Save as `greeter.yml`: ```yaml -version: "0.6.0" +version: "0.7.0" modules: - name: greeter errors: diff --git a/docs/src/tutorials/node.md b/docs/src/tutorials/node.md index 8b951a2e..a17274af 100644 --- a/docs/src/tutorials/node.md +++ b/docs/src/tutorials/node.md @@ -22,7 +22,7 @@ package shape ready to publish. Save as `greeter.yml`: ```yaml -version: "0.6.0" +version: "0.7.0" modules: - name: greeter errors: diff --git a/docs/src/tutorials/python.md b/docs/src/tutorials/python.md index 799f0395..de9e944c 100644 --- a/docs/src/tutorials/python.md +++ b/docs/src/tutorials/python.md @@ -20,7 +20,7 @@ script. Save as `greeter.yml`: ```yaml -version: "0.6.0" +version: "0.7.0" modules: - name: greeter errors: diff --git a/docs/src/tutorials/swift.md b/docs/src/tutorials/swift.md index 0ff2289c..9543beb0 100644 --- a/docs/src/tutorials/swift.md +++ b/docs/src/tutorials/swift.md @@ -24,7 +24,7 @@ simulator. Save as `greeter.yml`: ```yaml -version: "0.6.0" +version: "0.7.0" modules: - name: greeter errors: diff --git a/samples/async-demo/async_demo.yml b/samples/async-demo/async_demo.yml index b195b233..b8dd5239 100644 --- a/samples/async-demo/async_demo.yml +++ b/samples/async-demo/async_demo.yml @@ -1,4 +1,4 @@ -version: "0.6.0" +version: "0.7.0" package: name: async-demo version: "1.0.0" diff --git a/samples/calculator/calculator.yml b/samples/calculator/calculator.yml index 88b02d04..bf7eac52 100644 --- a/samples/calculator/calculator.yml +++ b/samples/calculator/calculator.yml @@ -1,4 +1,4 @@ -version: "0.6.0" +version: "0.7.0" package: name: calculator version: "1.0.0" diff --git a/samples/contacts/contacts.yml b/samples/contacts/contacts.yml index de8f38b5..f7236e77 100644 --- a/samples/contacts/contacts.yml +++ b/samples/contacts/contacts.yml @@ -1,4 +1,4 @@ -version: "0.6.0" +version: "0.7.0" package: name: contacts version: "1.0.0" diff --git a/samples/events/events.yml b/samples/events/events.yml index 03567937..b3e5a100 100644 --- a/samples/events/events.yml +++ b/samples/events/events.yml @@ -1,4 +1,4 @@ -version: "0.6.0" +version: "0.7.0" package: name: events version: "1.0.0" diff --git a/samples/inventory/inventory.yml b/samples/inventory/inventory.yml index b2aa75d2..e9be8864 100644 --- a/samples/inventory/inventory.yml +++ b/samples/inventory/inventory.yml @@ -1,4 +1,4 @@ -version: "0.6.0" +version: "0.7.0" package: name: inventory version: "1.0.0" diff --git a/samples/kvstore/kvstore.yml b/samples/kvstore/kvstore.yml index a2cdb3f0..fc319ab8 100644 --- a/samples/kvstore/kvstore.yml +++ b/samples/kvstore/kvstore.yml @@ -1,4 +1,4 @@ -version: "0.6.0" +version: "0.7.0" package: name: kvstore version: "1.0.0" diff --git a/samples/shapes/shapes.yml b/samples/shapes/shapes.yml index f0b52a32..d86df815 100644 --- a/samples/shapes/shapes.yml +++ b/samples/shapes/shapes.yml @@ -1,4 +1,4 @@ -version: "0.6.0" +version: "0.7.0" package: name: shapes version: "0.1.0" diff --git a/weaveffi.schema.json b/weaveffi.schema.json index 60ad731a..dff5d4eb 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.6.0`). Validation rejects any value not listed in [`SUPPORTED_VERSIONS`].", + "description": "IR schema version this document targets (for example `0.7.0`). Validation rejects any value not listed in [`SUPPORTED_VERSIONS`].", "type": "string" }, "package": { From ddfc27e85123c5aafbe5b12d6b0a036f9e527f8f Mon Sep 17 00:00:00 2001 From: Owen Carey <37121709+owenthcarey@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:01:36 -0700 Subject: [PATCH 2/3] fix: repair fuzz target build, unused dep, doc links, and formatting --- Cargo.lock | 1 - crates/weaveffi-cli/src/scaffold.rs | 2 +- crates/weaveffi-cli/tests/determinism.rs | 3 +- crates/weaveffi-cli/tests/snapshots.rs | 3 +- crates/weaveffi-core/Cargo.toml | 1 - crates/weaveffi-core/benches/codegen_bench.rs | 3 +- crates/weaveffi-core/src/cache.rs | 111 +++++++++++++++--- crates/weaveffi-core/src/codegen/mod.rs | 3 +- crates/weaveffi-core/src/model/mod.rs | 2 +- .../fuzz_targets/fuzz_validate.rs | 4 +- crates/weaveffi-gen-android/src/lib.rs | 2 +- crates/weaveffi-gen-android/src/tests.rs | 9 +- crates/weaveffi-gen-c/src/header.rs | 2 +- crates/weaveffi-gen-c/src/lib.rs | 4 +- crates/weaveffi-gen-c/src/tests.rs | 4 +- crates/weaveffi-gen-cpp/src/lib.rs | 2 +- crates/weaveffi-gen-cpp/src/tests.rs | 2 +- crates/weaveffi-gen-dart/src/lib.rs | 2 +- crates/weaveffi-gen-dart/src/tests.rs | 2 +- crates/weaveffi-gen-dotnet/src/calls.rs | 8 +- crates/weaveffi-gen-dotnet/src/lib.rs | 2 +- crates/weaveffi-gen-dotnet/src/tests.rs | 4 +- crates/weaveffi-gen-go/src/lib.rs | 2 +- crates/weaveffi-gen-go/src/package.rs | 2 +- crates/weaveffi-gen-go/src/tests.rs | 2 +- crates/weaveffi-gen-node/src/lib.rs | 2 +- crates/weaveffi-gen-node/src/tests.rs | 2 +- crates/weaveffi-gen-python/src/calls.rs | 4 +- crates/weaveffi-gen-python/src/lib.rs | 2 +- crates/weaveffi-gen-python/src/tests.rs | 2 +- crates/weaveffi-gen-ruby/src/lib.rs | 2 +- crates/weaveffi-gen-ruby/src/tests.rs | 2 +- crates/weaveffi-gen-swift/src/lib.rs | 2 +- crates/weaveffi-gen-swift/src/tests.rs | 2 +- crates/weaveffi-gen-wasm/src/lib.rs | 2 +- crates/weaveffi-gen-wasm/src/tests.rs | 4 +- 36 files changed, 145 insertions(+), 63 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 390c0002..c9638671 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1842,7 +1842,6 @@ dependencies = [ "rayon", "serde", "serde_json", - "serde_yaml", "sha2", "tempfile", "thiserror", diff --git a/crates/weaveffi-cli/src/scaffold.rs b/crates/weaveffi-cli/src/scaffold.rs index 03f2b4e8..f9473917 100644 --- a/crates/weaveffi-cli/src/scaffold.rs +++ b/crates/weaveffi-cli/src/scaffold.rs @@ -1,8 +1,8 @@ use std::fmt::Write; use weaveffi_core::abi::AbiParam; -use weaveffi_core::resolved::ResolvedApi; use weaveffi_core::model::{AbiFn, BindingModel, CallShape, ModuleBinding}; +use weaveffi_core::resolved::ResolvedApi; /// The body every generated stub carries until the producer fills it in. const TODO_BODY: &str = " todo!()\n"; diff --git a/crates/weaveffi-cli/tests/determinism.rs b/crates/weaveffi-cli/tests/determinism.rs index f2af347c..397f31df 100644 --- a/crates/weaveffi-cli/tests/determinism.rs +++ b/crates/weaveffi-cli/tests/determinism.rs @@ -35,8 +35,7 @@ fn load_kitchen_sink() -> 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())) + validate_api(api, None).unwrap_or_else(|e| panic!("validate fixture {}: {e}", path.display())) } fn collect_files_sorted(root: &Path) -> Vec { diff --git a/crates/weaveffi-cli/tests/snapshots.rs b/crates/weaveffi-cli/tests/snapshots.rs index 22eaaf34..f6bd9c43 100644 --- a/crates/weaveffi-cli/tests/snapshots.rs +++ b/crates/weaveffi-cli/tests/snapshots.rs @@ -37,8 +37,7 @@ fn load_api(file: &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())) + validate_api(api, None).unwrap_or_else(|e| panic!("validate fixture {}: {e}", path.display())) } fn collect_files_sorted(root: &Path) -> Vec { diff --git a/crates/weaveffi-core/Cargo.toml b/crates/weaveffi-core/Cargo.toml index 35168cd8..8d861ec5 100644 --- a/crates/weaveffi-core/Cargo.toml +++ b/crates/weaveffi-core/Cargo.toml @@ -16,7 +16,6 @@ thiserror = { workspace = true } camino = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } -serde_yaml = { workspace = true } sha2 = { workspace = true } miette = { workspace = true } rayon = { workspace = true } diff --git a/crates/weaveffi-core/benches/codegen_bench.rs b/crates/weaveffi-core/benches/codegen_bench.rs index 1c3875bf..b6da0204 100644 --- a/crates/weaveffi-core/benches/codegen_bench.rs +++ b/crates/weaveffi-core/benches/codegen_bench.rs @@ -278,8 +278,7 @@ fn bench_validate_kitchen_sink(c: &mut Criterion) { /// 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_core::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())); diff --git a/crates/weaveffi-core/src/cache.rs b/crates/weaveffi-core/src/cache.rs index d4b6922a..cf3155ee 100644 --- a/crates/weaveffi-core/src/cache.rs +++ b/crates/weaveffi-core/src/cache.rs @@ -155,9 +155,9 @@ fn migrate_legacy_cache(out_dir: &Utf8Path) -> Result<()> { mod tests { use super::*; use crate::codegen::{ConfiguredGenerator, Generator, Orchestrator, OrchestratorHooks}; + use crate::resolved::ResolvedApi; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; - use crate::resolved::ResolvedApi; use weaveffi_ir::ir::{Function, Module, Param, TypeRef}; /// Minimal serde-able config so the cache tests can exercise the @@ -399,7 +399,13 @@ mod tests { let gen = configured("counting", Arc::clone(&calls), TestConfig::default()); let orch = Orchestrator::new().with_generator(&gen); - orch.run(&ResolvedApi::assume_resolved(api.clone()), out_dir, &hooks, false).unwrap(); + orch.run( + &ResolvedApi::assume_resolved(api.clone()), + out_dir, + &hooks, + false, + ) + .unwrap(); assert!(out_dir.join(CACHE_DIR).join("counting.hash").exists()); assert_eq!(calls.load(Ordering::SeqCst), 1); @@ -415,10 +421,22 @@ mod tests { let gen = configured("counting", Arc::clone(&calls), TestConfig::default()); let orch = Orchestrator::new().with_generator(&gen); - orch.run(&ResolvedApi::assume_resolved(api.clone()), out_dir, &hooks, false).unwrap(); + orch.run( + &ResolvedApi::assume_resolved(api.clone()), + out_dir, + &hooks, + false, + ) + .unwrap(); assert_eq!(calls.load(Ordering::SeqCst), 1); - orch.run(&ResolvedApi::assume_resolved(api.clone()), out_dir, &hooks, false).unwrap(); + orch.run( + &ResolvedApi::assume_resolved(api.clone()), + out_dir, + &hooks, + false, + ) + .unwrap(); assert_eq!( calls.load(Ordering::SeqCst), 1, @@ -436,7 +454,13 @@ mod tests { let gen = configured("counting", Arc::clone(&calls), TestConfig::default()); let orch = Orchestrator::new().with_generator(&gen); - orch.run(&ResolvedApi::assume_resolved(api.clone()), out_dir, &hooks, false).unwrap(); + orch.run( + &ResolvedApi::assume_resolved(api.clone()), + out_dir, + &hooks, + false, + ) + .unwrap(); assert_eq!(calls.load(Ordering::SeqCst), 1); let mut modified_api = api; @@ -465,7 +489,13 @@ mod tests { since: None, }); - orch.run(&ResolvedApi::assume_resolved(modified_api.clone()), out_dir, &hooks, false).unwrap(); + orch.run( + &ResolvedApi::assume_resolved(modified_api.clone()), + out_dir, + &hooks, + false, + ) + .unwrap(); assert_eq!( calls.load(Ordering::SeqCst), 2, @@ -483,10 +513,22 @@ mod tests { let gen = configured("counting", Arc::clone(&calls), TestConfig::default()); let orch = Orchestrator::new().with_generator(&gen); - orch.run(&ResolvedApi::assume_resolved(api.clone()), out_dir, &hooks, true).unwrap(); + orch.run( + &ResolvedApi::assume_resolved(api.clone()), + out_dir, + &hooks, + true, + ) + .unwrap(); assert_eq!(calls.load(Ordering::SeqCst), 1); - orch.run(&ResolvedApi::assume_resolved(api.clone()), out_dir, &hooks, true).unwrap(); + orch.run( + &ResolvedApi::assume_resolved(api.clone()), + out_dir, + &hooks, + true, + ) + .unwrap(); assert_eq!( calls.load(Ordering::SeqCst), 2, @@ -506,7 +548,13 @@ mod tests { let gen = configured("counting", Arc::clone(&calls), TestConfig::default()); let orch = Orchestrator::new().with_generator(&gen); - orch.run(&ResolvedApi::assume_resolved(api.clone()), out_dir, &hooks, false).unwrap(); + orch.run( + &ResolvedApi::assume_resolved(api.clone()), + out_dir, + &hooks, + false, + ) + .unwrap(); assert_eq!( calls.load(Ordering::SeqCst), 1, @@ -529,14 +577,26 @@ mod tests { .with_generator(&s_gen); let api = minimal_api(); - orch.run(&ResolvedApi::assume_resolved(api.clone()), out_dir, &hooks, false).unwrap(); + orch.run( + &ResolvedApi::assume_resolved(api.clone()), + out_dir, + &hooks, + false, + ) + .unwrap(); assert_eq!(c_calls.load(Ordering::SeqCst), 1); assert_eq!(s_calls.load(Ordering::SeqCst), 1); // Invalidate only the C generator's cache; the API itself is unchanged. std::fs::remove_file(out_dir.join(CACHE_DIR).join("c.hash")).unwrap(); - orch.run(&ResolvedApi::assume_resolved(api.clone()), out_dir, &hooks, false).unwrap(); + orch.run( + &ResolvedApi::assume_resolved(api.clone()), + out_dir, + &hooks, + false, + ) + .unwrap(); assert_eq!( c_calls.load(Ordering::SeqCst), 2, @@ -605,7 +665,12 @@ mod tests { let gen = configured("c", Arc::clone(&calls), TestConfig::default()); Orchestrator::new() .with_generator(&gen) - .run(&ResolvedApi::assume_resolved(api.clone()), out_dir, &hooks, false) + .run( + &ResolvedApi::assume_resolved(api.clone()), + out_dir, + &hooks, + false, + ) .unwrap(); assert_eq!(calls.load(Ordering::SeqCst), 1); @@ -619,7 +684,12 @@ mod tests { ); Orchestrator::new() .with_generator(&gen2) - .run(&ResolvedApi::assume_resolved(api.clone()), out_dir, &hooks, false) + .run( + &ResolvedApi::assume_resolved(api.clone()), + out_dir, + &hooks, + false, + ) .unwrap(); assert_eq!( calls.load(Ordering::SeqCst), @@ -630,7 +700,12 @@ mod tests { // A third run with the same `changed` config should hit the cache again. Orchestrator::new() .with_generator(&gen2) - .run(&ResolvedApi::assume_resolved(api.clone()), out_dir, &hooks, false) + .run( + &ResolvedApi::assume_resolved(api.clone()), + out_dir, + &hooks, + false, + ) .unwrap(); assert_eq!( calls.load(Ordering::SeqCst), @@ -656,7 +731,13 @@ mod tests { let stale = hash_api_for_generator(&api, "c"); write_generator_cache(out_dir, "c", &stale).unwrap(); - orch.run(&ResolvedApi::assume_resolved(api.clone()), out_dir, &hooks, false).unwrap(); + orch.run( + &ResolvedApi::assume_resolved(api.clone()), + out_dir, + &hooks, + false, + ) + .unwrap(); assert_eq!( calls.load(Ordering::SeqCst), 1, diff --git a/crates/weaveffi-core/src/codegen/mod.rs b/crates/weaveffi-core/src/codegen/mod.rs index 07159dda..62cdf16a 100644 --- a/crates/weaveffi-core/src/codegen/mod.rs +++ b/crates/weaveffi-core/src/codegen/mod.rs @@ -84,8 +84,7 @@ pub trait Generator: Send + Sync { /// /// Returns an error if the target cannot render the API or cannot write its /// output files (for example a filesystem failure). - fn generate(&self, api: &ResolvedApi, out_dir: &Utf8Path, config: &Self::Config) - -> Result<()>; + fn generate(&self, api: &ResolvedApi, out_dir: &Utf8Path, config: &Self::Config) -> Result<()>; /// Files that [`generate`](Generator::generate) would write, relative /// to (or anchored under) `out_dir`. Used by `--dry-run` and `diff`. diff --git a/crates/weaveffi-core/src/model/mod.rs b/crates/weaveffi-core/src/model/mod.rs index 5c2d3c8b..247fb1cf 100644 --- a/crates/weaveffi-core/src/model/mod.rs +++ b/crates/weaveffi-core/src/model/mod.rs @@ -1,5 +1,5 @@ //! The **binding model**: a normalized, fully-lowered view of a -//! [`ResolvedApi`](crate::resolved::ResolvedApi) that every language backend +//! [`ResolvedApi`] that every language backend //! consumes. //! //! Before this module existed, each of the eleven generators re-walked the IR, diff --git a/crates/weaveffi-fuzz/fuzz_targets/fuzz_validate.rs b/crates/weaveffi-fuzz/fuzz_targets/fuzz_validate.rs index 4ff74214..d77db238 100644 --- a/crates/weaveffi-fuzz/fuzz_targets/fuzz_validate.rs +++ b/crates/weaveffi-fuzz/fuzz_targets/fuzz_validate.rs @@ -13,10 +13,10 @@ fuzz_target!(|data: &[u8]| { let Ok(s) = std::str::from_utf8(data) else { return; }; - let Ok(mut api) = parse_api_str(s, "yaml") else { + let Ok(api) = parse_api_str(s, "yaml") else { return; }; - let _ = validate_api(&mut api, None); + let _ = validate_api(api, None); }); #[cfg(not(fuzzing))] diff --git a/crates/weaveffi-gen-android/src/lib.rs b/crates/weaveffi-gen-android/src/lib.rs index 713a730a..347d4351 100644 --- a/crates/weaveffi-gen-android/src/lib.rs +++ b/crates/weaveffi-gen-android/src/lib.rs @@ -23,11 +23,11 @@ mod types; use camino::Utf8Path; use serde::{Deserialize, Serialize}; use std::fmt::Write as _; -use weaveffi_core::resolved::ResolvedApi; use weaveffi_core::backend::{LanguageBackend, OutputFile}; use weaveffi_core::capabilities::TargetCapabilities; use weaveffi_core::model::{BindingModel, CallShape, CallbackBinding}; 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, render_listener_api}; diff --git a/crates/weaveffi-gen-android/src/tests.rs b/crates/weaveffi-gen-android/src/tests.rs index 1cb18eb6..842974ed 100644 --- a/crates/weaveffi-gen-android/src/tests.rs +++ b/crates/weaveffi-gen-android/src/tests.rs @@ -1,11 +1,11 @@ //! Unit tests for the Android backend: rendering assertions over the Kotlin //! wrapper and the JNI C bridge, plus file-set and config-variant checks. -use weaveffi_core::resolved::ResolvedApi; use camino::Utf8Path; use weaveffi_core::codegen::common::pascal_case; use weaveffi_core::codegen::Generator; use weaveffi_core::model::BindingModel; +use weaveffi_core::resolved::ResolvedApi; use weaveffi_ir::ir::{ Api, EnumDef, EnumVariant, ErrorCode, ErrorDomain, Function, Module, Param, StructDef, StructField, TypeRef, @@ -555,7 +555,12 @@ fn list_of_string_return_is_buffered() { /// A single-module API with one free function, for return-marshalling /// tests. -fn make_fn_api(name: &str, params: Vec, returns: Option, throws: bool) -> ResolvedApi { +fn make_fn_api( + name: &str, + params: Vec, + returns: Option, + throws: bool, +) -> ResolvedApi { make_api(vec![Module { name: "m".to_string(), functions: vec![Function { diff --git a/crates/weaveffi-gen-c/src/header.rs b/crates/weaveffi-gen-c/src/header.rs index d1971587..b1000f11 100644 --- a/crates/weaveffi-gen-c/src/header.rs +++ b/crates/weaveffi-gen-c/src/header.rs @@ -9,10 +9,10 @@ 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, }; -use weaveffi_core::resolved::ResolvedApi; use crate::idents::escape_module_param_names; diff --git a/crates/weaveffi-gen-c/src/lib.rs b/crates/weaveffi-gen-c/src/lib.rs index ef229a96..b6d93661 100644 --- a/crates/weaveffi-gen-c/src/lib.rs +++ b/crates/weaveffi-gen-c/src/lib.rs @@ -1,7 +1,7 @@ //! C header generator for WeaveFFI. //! //! Emits a single `{prefix}.h` describing the stable C ABI surface of a -//! [`ResolvedApi`](weaveffi_core::resolved::ResolvedApi), plus a companion +//! [`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. @@ -22,11 +22,11 @@ mod tests; use camino::Utf8Path; use serde::{Deserialize, Serialize}; -use weaveffi_core::resolved::ResolvedApi; 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}; diff --git a/crates/weaveffi-gen-c/src/tests.rs b/crates/weaveffi-gen-c/src/tests.rs index 437e9df5..b4356dbc 100644 --- a/crates/weaveffi-gen-c/src/tests.rs +++ b/crates/weaveffi-gen-c/src/tests.rs @@ -1,10 +1,10 @@ //! Tests for the C backend: rendered-header assertions over fixture APIs, //! keyword-escaping regressions, and packaging checks. -use weaveffi_ir::ir::Api; -use weaveffi_core::resolved::ResolvedApi; use camino::Utf8Path; use weaveffi_core::codegen::Generator; +use weaveffi_core::resolved::ResolvedApi; +use weaveffi_ir::ir::Api; use weaveffi_ir::ir::{ CallbackDef, EnumDef, EnumVariant, Function, ListenerDef, Module, Param, StructDef, StructField, TypeRef, diff --git a/crates/weaveffi-gen-cpp/src/lib.rs b/crates/weaveffi-gen-cpp/src/lib.rs index 98ff6bf7..6f4e7a50 100644 --- a/crates/weaveffi-gen-cpp/src/lib.rs +++ b/crates/weaveffi-gen-cpp/src/lib.rs @@ -52,13 +52,13 @@ mod types; use camino::Utf8Path; use serde::{Deserialize, Serialize}; -use weaveffi_core::resolved::ResolvedApi; use weaveffi_core::abi::is_buffered; use weaveffi_core::backend::{LanguageBackend, OutputFile}; use weaveffi_core::cabi; use weaveffi_core::capabilities::TargetCapabilities; use weaveffi_core::model::{BindingModel, CallShape, EnumBinding, InterfaceBinding, ModuleBinding}; use weaveffi_core::package::{PackageContext, PackagedFile}; +use weaveffi_core::resolved::ResolvedApi; use weaveffi_core::utils::{ render_abi_prefix_aliases, render_prelude, render_trailer, CommentStyle, }; diff --git a/crates/weaveffi-gen-cpp/src/tests.rs b/crates/weaveffi-gen-cpp/src/tests.rs index 92ebd246..3ccba77d 100644 --- a/crates/weaveffi-gen-cpp/src/tests.rs +++ b/crates/weaveffi-gen-cpp/src/tests.rs @@ -1,11 +1,11 @@ //! Tests for the C++ backend: rendered-header assertions over fixture APIs, //! plus config, packaging, and determinism checks. -use weaveffi_core::resolved::ResolvedApi; use camino::Utf8Path; use weaveffi_core::codegen::Generator; use weaveffi_core::lang::{self, CPP_KEYWORDS}; use weaveffi_core::model::BindingModel; +use weaveffi_core::resolved::ResolvedApi; use weaveffi_ir::ir::{ Api, CallbackDef, EnumDef, EnumVariant, ErrorCode, ErrorDomain, Function, InterfaceDef, ListenerDef, Module, Param, StructDef, StructField, TypeRef, diff --git a/crates/weaveffi-gen-dart/src/lib.rs b/crates/weaveffi-gen-dart/src/lib.rs index 979170f9..1b93e8cc 100644 --- a/crates/weaveffi-gen-dart/src/lib.rs +++ b/crates/weaveffi-gen-dart/src/lib.rs @@ -28,7 +28,6 @@ mod tests; use camino::Utf8Path; use serde::{Deserialize, Serialize}; -use weaveffi_core::resolved::ResolvedApi; use weaveffi_core::backend::{LanguageBackend, OutputFile}; use weaveffi_core::capabilities::TargetCapabilities; use weaveffi_core::model::{ @@ -37,6 +36,7 @@ use weaveffi_core::model::{ }; 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_callback_typedef, render_function, render_listener}; diff --git a/crates/weaveffi-gen-dart/src/tests.rs b/crates/weaveffi-gen-dart/src/tests.rs index 8a3a651f..d7b515ae 100644 --- a/crates/weaveffi-gen-dart/src/tests.rs +++ b/crates/weaveffi-gen-dart/src/tests.rs @@ -1,12 +1,12 @@ //! Unit tests for the Dart generator: rendered-source assertions over //! representative APIs, plus slot-mapping and packaging checks. -use weaveffi_core::resolved::ResolvedApi; use camino::Utf8Path; use weaveffi_core::abi::lower_param; use weaveffi_core::backend::LanguageBackend; use weaveffi_core::codegen::Generator; use weaveffi_core::model::{BindingModel, ParamBinding}; +use weaveffi_core::resolved::ResolvedApi; use weaveffi_ir::ir::{ Api, EnumDef, EnumVariant, Function, Module, Param, StructDef, StructField, TypeRef, }; diff --git a/crates/weaveffi-gen-dotnet/src/calls.rs b/crates/weaveffi-gen-dotnet/src/calls.rs index e548f47c..af51b02c 100644 --- a/crates/weaveffi-gen-dotnet/src/calls.rs +++ b/crates/weaveffi-gen-dotnet/src/calls.rs @@ -806,7 +806,9 @@ pub(crate) fn render_async_set_result(out: &mut String, ret: &Option, i 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);"); + w.line( + "if (result != IntPtr.Zero) NativeMethods.weaveffi_free_bytes(result, resultLen);", + ); emit_buffer_decode(&mut w, ty, "value", "resultBuf"); w.line("tcs.SetResult(value);"); out.push_str(&w.finish()); @@ -842,7 +844,9 @@ pub(crate) fn render_async_set_result(out: &mut String, ret: &Option, i 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( + "if (result != IntPtr.Zero) NativeMethods.weaveffi_free_bytes(result, resultLen);", + ); w.line("tcs.SetResult(arr);"); } // Only `Interface?` reaches here: a nullable owned object pointer. diff --git a/crates/weaveffi-gen-dotnet/src/lib.rs b/crates/weaveffi-gen-dotnet/src/lib.rs index 2dfd5125..6033551e 100644 --- a/crates/weaveffi-gen-dotnet/src/lib.rs +++ b/crates/weaveffi-gen-dotnet/src/lib.rs @@ -28,11 +28,11 @@ mod types; use camino::Utf8Path; use serde::{Deserialize, Serialize}; -use weaveffi_core::resolved::ResolvedApi; use weaveffi_core::backend::{LanguageBackend, OutputFile}; use weaveffi_core::capabilities::TargetCapabilities; use weaveffi_core::model::{BindingModel, CallShape, ErrorBinding}; use weaveffi_core::package::{PackageContext, PackagedFile}; +use weaveffi_core::resolved::ResolvedApi; use weaveffi_core::utils::{render_prelude, render_trailer, CommentStyle}; use crate::calls::render_wrapper_class; diff --git a/crates/weaveffi-gen-dotnet/src/tests.rs b/crates/weaveffi-gen-dotnet/src/tests.rs index b07bf691..0d0932c1 100644 --- a/crates/weaveffi-gen-dotnet/src/tests.rs +++ b/crates/weaveffi-gen-dotnet/src/tests.rs @@ -1,11 +1,11 @@ //! Output-content tests for the .NET generator: fixtures mirroring the //! sample IDLs plus assertions over the rendered C# source and manifests. -use weaveffi_ir::ir::Api; -use weaveffi_core::resolved::ResolvedApi; use super::*; use crate::types::{cs_type, pinvoke_type, safe_cs_name}; use weaveffi_core::codegen::Generator; +use weaveffi_core::resolved::ResolvedApi; +use weaveffi_ir::ir::Api; use weaveffi_ir::ir::{ EnumDef, EnumVariant, Function, Module, Param, StructDef, StructField, TypeRef, }; diff --git a/crates/weaveffi-gen-go/src/lib.rs b/crates/weaveffi-gen-go/src/lib.rs index 4b819897..4b2c3687 100644 --- a/crates/weaveffi-gen-go/src/lib.rs +++ b/crates/weaveffi-gen-go/src/lib.rs @@ -27,7 +27,6 @@ mod types; use camino::Utf8Path; use heck::ToUpperCamelCase; use serde::{Deserialize, Serialize}; -use weaveffi_core::resolved::ResolvedApi; use weaveffi_core::abi::is_buffered; use weaveffi_core::backend::{LanguageBackend, OutputFile}; use weaveffi_core::capabilities::TargetCapabilities; @@ -35,6 +34,7 @@ use weaveffi_core::model::{BindingModel, CallShape}; use weaveffi_core::package::{PackageContext, PackagedFile}; use weaveffi_core::pkg; use weaveffi_core::plan::{elem_free, ElemFree}; +use weaveffi_core::resolved::ResolvedApi; use weaveffi_core::utils::{render_prelude, render_trailer, wrapper_name, CommentStyle}; use weaveffi_ir::ir::TypeRef; diff --git a/crates/weaveffi-gen-go/src/package.rs b/crates/weaveffi-gen-go/src/package.rs index edb40fef..8da14686 100644 --- a/crates/weaveffi-gen-go/src/package.rs +++ b/crates/weaveffi-gen-go/src/package.rs @@ -2,11 +2,11 @@ //! bundling per-platform shared libraries with a rewritten cgo preamble. use camino::Utf8Path; -use weaveffi_core::resolved::ResolvedApi; 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 weaveffi_core::utils::{render_prelude, render_trailer, CommentStyle}; use crate::{render_go, GoConfig}; diff --git a/crates/weaveffi-gen-go/src/tests.rs b/crates/weaveffi-gen-go/src/tests.rs index 8be6c067..21841314 100644 --- a/crates/weaveffi-gen-go/src/tests.rs +++ b/crates/weaveffi-gen-go/src/tests.rs @@ -1,10 +1,10 @@ //! Output-content tests for the Go generator: fixtures mirroring the //! sample IDLs plus assertions over the rendered source. -use weaveffi_core::resolved::ResolvedApi; use camino::Utf8Path; use weaveffi_core::codegen::Generator; use weaveffi_core::errors::ERROR_BRAND; +use weaveffi_core::resolved::ResolvedApi; use weaveffi_ir::ir::{ Api, CallbackDef, EnumDef, EnumVariant, ErrorCode, ErrorDomain, Function, InterfaceDef, ListenerDef, Module, Param, StructDef, StructField, TypeRef, diff --git a/crates/weaveffi-gen-node/src/lib.rs b/crates/weaveffi-gen-node/src/lib.rs index fce4cb69..86651158 100644 --- a/crates/weaveffi-gen-node/src/lib.rs +++ b/crates/weaveffi-gen-node/src/lib.rs @@ -35,12 +35,12 @@ mod tests; use camino::Utf8Path; use serde::{Deserialize, Serialize}; -use weaveffi_core::resolved::ResolvedApi; 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::resolved::ResolvedApi; use crate::addon::render_addon_c; use crate::entities::render_node_index; diff --git a/crates/weaveffi-gen-node/src/tests.rs b/crates/weaveffi-gen-node/src/tests.rs index 4de1cb3f..7f9d3f9c 100644 --- a/crates/weaveffi-gen-node/src/tests.rs +++ b/crates/weaveffi-gen-node/src/tests.rs @@ -1,12 +1,12 @@ //! Unit tests: golden-content assertions over every emitted file. -use weaveffi_core::resolved::ResolvedApi; use super::*; use crate::addon::render_addon_c; use crate::entities::render_node_index; use crate::package::render_node_dts; use crate::types::ts_type_for; use weaveffi_core::codegen::Generator; +use weaveffi_core::resolved::ResolvedApi; use weaveffi_ir::ir::{ Api, EnumDef, EnumVariant, ErrorCode, ErrorDomain, Function, InterfaceDef, Module, Param, StructDef, StructField, TypeRef, diff --git a/crates/weaveffi-gen-python/src/calls.rs b/crates/weaveffi-gen-python/src/calls.rs index b82e7b23..285a7b16 100644 --- a/crates/weaveffi-gen-python/src/calls.rs +++ b/crates/weaveffi-gen-python/src/calls.rs @@ -635,9 +635,7 @@ fn render_async_ffi_call_body( "{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} _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 diff --git a/crates/weaveffi-gen-python/src/lib.rs b/crates/weaveffi-gen-python/src/lib.rs index 426e331f..94fe2147 100644 --- a/crates/weaveffi-gen-python/src/lib.rs +++ b/crates/weaveffi-gen-python/src/lib.rs @@ -23,7 +23,6 @@ mod tests; use camino::Utf8Path; use serde::{Deserialize, Serialize}; -use weaveffi_core::resolved::ResolvedApi; use weaveffi_core::backend::{LanguageBackend, OutputFile}; use weaveffi_core::capabilities::TargetCapabilities; use weaveffi_core::model::{ @@ -32,6 +31,7 @@ use weaveffi_core::model::{ }; 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_type, render_listener, FnScope}; diff --git a/crates/weaveffi-gen-python/src/tests.rs b/crates/weaveffi-gen-python/src/tests.rs index 21fc09e6..aedfc3a5 100644 --- a/crates/weaveffi-gen-python/src/tests.rs +++ b/crates/weaveffi-gen-python/src/tests.rs @@ -2,12 +2,12 @@ //! generated source text and are the byte-compatibility guard for the //! crate. -use weaveffi_core::resolved::ResolvedApi; use super::*; use crate::stubs::render_pyi_module; use crate::types::{py_ctypes_scalar, py_type_hint}; use camino::Utf8Path; use weaveffi_core::codegen::Generator; +use weaveffi_core::resolved::ResolvedApi; use weaveffi_ir::ir::{ Api, EnumDef, EnumVariant, Function, Module, Param, StructDef, StructField, TypeRef, }; diff --git a/crates/weaveffi-gen-ruby/src/lib.rs b/crates/weaveffi-gen-ruby/src/lib.rs index 3b14bbb1..a776bcd4 100644 --- a/crates/weaveffi-gen-ruby/src/lib.rs +++ b/crates/weaveffi-gen-ruby/src/lib.rs @@ -22,12 +22,12 @@ mod tests; use camino::Utf8Path; use serde::{Deserialize, Serialize}; -use weaveffi_core::resolved::ResolvedApi; 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::resolved::ResolvedApi; use weaveffi_core::utils::{render_prelude, render_trailer, CommentStyle}; use crate::calls::{ diff --git a/crates/weaveffi-gen-ruby/src/tests.rs b/crates/weaveffi-gen-ruby/src/tests.rs index 9b8e4056..b885282a 100644 --- a/crates/weaveffi-gen-ruby/src/tests.rs +++ b/crates/weaveffi-gen-ruby/src/tests.rs @@ -1,12 +1,12 @@ //! Tests for the Ruby generator: rendering coverage across every entity //! and call shape, error-domain semantics, packaging, and identifier policy. -use weaveffi_core::resolved::ResolvedApi; use super::*; use crate::types::{rb_abi_types, rb_ffi_type}; use camino::Utf8Path; use weaveffi_core::abi; use weaveffi_core::codegen::Generator; +use weaveffi_core::resolved::ResolvedApi; #[test] fn package_emits_platform_gems_and_swaps_loader() { diff --git a/crates/weaveffi-gen-swift/src/lib.rs b/crates/weaveffi-gen-swift/src/lib.rs index 1d755879..a6abdf49 100644 --- a/crates/weaveffi-gen-swift/src/lib.rs +++ b/crates/weaveffi-gen-swift/src/lib.rs @@ -31,12 +31,12 @@ use std::collections::{HashMap, HashSet}; use camino::Utf8Path; use heck::ToUpperCamelCase; use serde::{Deserialize, Serialize}; -use weaveffi_core::resolved::ResolvedApi; 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; diff --git a/crates/weaveffi-gen-swift/src/tests.rs b/crates/weaveffi-gen-swift/src/tests.rs index 7774afed..8343799e 100644 --- a/crates/weaveffi-gen-swift/src/tests.rs +++ b/crates/weaveffi-gen-swift/src/tests.rs @@ -1,7 +1,7 @@ -use weaveffi_core::resolved::ResolvedApi; use super::*; use crate::types::swift_type_for; use weaveffi_core::codegen::Generator; +use weaveffi_core::resolved::ResolvedApi; use weaveffi_ir::ir::{ Api, EnumDef, EnumVariant, ErrorCode, ErrorDomain, Function, Module, Param, StructDef, StructField, TypeRef, diff --git a/crates/weaveffi-gen-wasm/src/lib.rs b/crates/weaveffi-gen-wasm/src/lib.rs index ec354bdc..3e5d3435 100644 --- a/crates/weaveffi-gen-wasm/src/lib.rs +++ b/crates/weaveffi-gen-wasm/src/lib.rs @@ -23,11 +23,11 @@ mod tests; use camino::Utf8Path; use serde::{Deserialize, Serialize}; -use weaveffi_core::resolved::ResolvedApi; use weaveffi_core::backend::{LanguageBackend, OutputFile}; use weaveffi_core::capabilities::TargetCapabilities; use weaveffi_core::model::BindingModel; use weaveffi_core::pkg; +use weaveffi_core::resolved::ResolvedApi; use crate::dts::render_wasm_dts; use crate::entities::render_wasm_js_stub; diff --git a/crates/weaveffi-gen-wasm/src/tests.rs b/crates/weaveffi-gen-wasm/src/tests.rs index fa91df07..46c68edd 100644 --- a/crates/weaveffi-gen-wasm/src/tests.rs +++ b/crates/weaveffi-gen-wasm/src/tests.rs @@ -1,11 +1,11 @@ //! Unit tests: golden-content assertions over the JS loader, the //! TypeScript declarations, the README, and the package manifest. -use weaveffi_ir::ir::Api; -use weaveffi_core::resolved::ResolvedApi; use super::*; use camino::Utf8Path; use weaveffi_core::codegen::Generator; +use weaveffi_core::resolved::ResolvedApi; +use weaveffi_ir::ir::Api; use weaveffi_ir::ir::{ EnumDef, EnumVariant, Function, Module, Param, StructDef, StructField, TypeRef, }; From 038e25264decc3b876cf2cc0426ae15898692c5d Mon Sep 17 00:00:00 2001 From: Owen Carey <37121709+owenthcarey@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:16:17 -0700 Subject: [PATCH 3/3] fix: regenerate JSON Schema and drop unused serde_yaml dependency --- Cargo.lock | 1 - crates/weaveffi-gen-python/Cargo.toml | 2 -- weaveffi.schema.json | 7 ++----- 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c9638671..9d94432a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1944,7 +1944,6 @@ dependencies = [ "camino", "heck", "serde", - "serde_yaml", "tempfile", "weaveffi-core", "weaveffi-ir", diff --git a/crates/weaveffi-gen-python/Cargo.toml b/crates/weaveffi-gen-python/Cargo.toml index 870fc9f9..2c4b8dee 100644 --- a/crates/weaveffi-gen-python/Cargo.toml +++ b/crates/weaveffi-gen-python/Cargo.toml @@ -15,8 +15,6 @@ weaveffi-ir = { path = "../weaveffi-ir", version = "0.19.0" } camino = { workspace = true } heck = { workspace = true } serde = { workspace = true } -serde_yaml = { workspace = true } - [dev-dependencies] tempfile = { workspace = true } diff --git a/weaveffi.schema.json b/weaveffi.schema.json index dff5d4eb..a725ca8d 100644 --- a/weaveffi.schema.json +++ b/weaveffi.schema.json @@ -363,9 +363,6 @@ "string", "null" ] - }, - "default": { - "description": "Default value used when the field is omitted, kept as a raw YAML value so any literal the field's type accepts can be expressed. Ignored for algebraic [`EnumVariant`] payloads, which aren't defaultable. `None` when the field has no default." } } }, @@ -422,7 +419,7 @@ ] }, "fields": { - "description": "Associated data carried by this variant. Empty for a unit variant or a C-style enum; non-empty makes the owning enum a sum type (see [`EnumDef::is_rich`]). Variant fields reuse [`StructField`] but ignore the `default` slot (a sum-type payload has no defaultable fields).", + "description": "Associated data carried by this variant. Empty for a unit variant or a C-style enum; non-empty makes the owning enum a sum type (see [`EnumDef::is_rich`]).", "type": "array", "items": { "$ref": "#/definitions/StructField" @@ -534,7 +531,7 @@ ] }, "fields": { - "description": "Structured payload fields this error carries beyond its code and message, serialized across the ABI in the error's payload buffer. Empty for a plain code. Payload fields reuse [`StructField`] but ignore the `default` slot.", + "description": "Structured payload fields this error carries beyond its code and message, serialized across the ABI in the error's payload buffer. Empty for a plain code.", "type": "array", "items": { "$ref": "#/definitions/StructField"