Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
2 changes: 0 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 13 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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?
Expand Down Expand Up @@ -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
Expand All @@ -277,15 +277,15 @@ weaveffi schema-version # prints 0.6.0
| `weaveffi format <file>` | Rewrite an IDL file in canonical form (sorted keys); `--check` for CI |
| `weaveffi watch <file>` | 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 <shell>` | Print shell completion scripts (`bash`, `zsh`, `fish`, `powershell`, `elvish`) |

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: ...
```

Expand Down Expand Up @@ -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`.
Expand Down
161 changes: 161 additions & 0 deletions conformance/c/async_demo.c
Original file line number Diff line number Diff line change
@@ -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 <assert.h>
#include <stdatomic.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#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;
}
4 changes: 4 additions & 0 deletions conformance/c/producer.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
63 changes: 63 additions & 0 deletions conformance/cpp/async_demo.cpp
Original file line number Diff line number Diff line change
@@ -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 <cassert>
#include <cstdio>
#include <cstring>
#include <string>
#include <vector>

#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<const weaveffi::TaskError*>(&e) != nullptr);
assert(dynamic_cast<const weaveffi::WeaveFFIError*>(&e) != nullptr);
}
assert(threw && "expected InvalidNameError for empty name");

// Buffered list-of-records both ways.
std::vector<weaveffi::TaskResult> 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;
}
54 changes: 54 additions & 0 deletions conformance/dart/async_demo.dart
Original file line number Diff line number Diff line change
@@ -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<void> 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');
}
Loading
Loading