Skip to content

Commit 7dd7ebc

Browse files
committed
fix: final consolidation
Signed-off-by: Gordon Smith <[email protected]>
1 parent 102acc6 commit 7dd7ebc

File tree

3 files changed

+394
-35
lines changed

3 files changed

+394
-35
lines changed

README.md

Lines changed: 123 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ This repository contains a C++ ABI implementation of the WebAssembly Component M
1616

1717
### OS
1818
- [x] Ubuntu 24.04
19-
- [x] MacOS 13
20-
- [x] MacOS 14 (Arm)
21-
- [x] Windows 2019
19+
- [ ] MacOS 13
20+
- [ ] MacOS 14 (Arm)
21+
- [ ] Windows 2019
2222
- [x] Windows 2022
2323

2424
### Host Data Types
@@ -45,8 +45,8 @@ This repository contains a C++ ABI implementation of the WebAssembly Component M
4545
- [x] Flags
4646
- [x] Streams (readable/writable)
4747
- [x] Futures (readable/writable)
48-
- [ ] Own
49-
- [ ] Borrow
48+
- [x] Own
49+
- [x] Borrow
5050

5151
### Host Functions
5252
- [x] lower_flat_values
@@ -162,61 +162,149 @@ ctest -VV # Run tests
162162

163163
### Build Options
164164

165-
- `-DBUILD_TESTING=ON/OFF` - Enable/disable building tests (requires doctest, ICU)
166-
- `-DBUILD_SAMPLES=ON/OFF` - Enable/disable building samples (requires wasi-sdk)
167-
- `-DCMAKE_BUILD_TYPE=Debug/Release/RelWithDebInfo/MinSizeRel` - Build configuration
165+
166+
### Coverage
167+
168+
The presets build tests with GCC/Clang coverage instrumentation enabled, so generating a report is mostly a matter of running the suite and capturing the counters. On Ubuntu the full workflow looks like:
169+
170+
```bash
171+
# 1. Install tooling (once per machine)
172+
sudo apt-get update
173+
sudo apt-get install -y lcov
174+
175+
# 2. Rebuild and rerun tests to refresh .gcda files
176+
cmake --build --preset linux-ninja-Debug
177+
cd build
178+
ctest --output-on-failure
179+
180+
# 3. Capture raw coverage data
181+
lcov --capture --directory . --output-file coverage.info
182+
183+
# 4. Filter out system headers, vcpkg packages, and tests (optional but recommended)
184+
lcov --remove coverage.info '/usr/include/*' '*/vcpkg/*' '*/test/*' \
185+
--output-file coverage.filtered.info --ignore-errors unused
186+
187+
# 5. Inspect the summary or render HTML
188+
lcov --list coverage.filtered.info
189+
genhtml coverage.filtered.info --output-directory coverage-html # optional
190+
```
191+
192+
Generated artifacts live in the `build/` directory (`coverage.info`, `coverage.filtered.info`, and optionally `coverage-html/`). The same commands work on other platforms once the equivalent of `lcov` (or LLVM's `llvm-cov`) is installed.
168193

169194
## Usage
170195

171196
This library is a header only library. To use it in your project, you can:
172197
- [x] Copy the contents of the `include` directory to your project.
173198
- [ ] Use `vcpkg` to install the library and its dependencies.
174199

175-
### Async runtime helpers
200+
### Configuring `InstanceContext` and canonical options
201+
202+
Most host interactions begin by materialising an `InstanceContext`. This container wires together the host trap callback, string conversion routine, and the guest `realloc` export. Use `createInstanceContext` to capture those dependencies once:
203+
204+
```cpp
205+
cmcpp::HostTrap trap = [](const char *msg) {
206+
throw std::runtime_error(msg ? msg : "trap");
207+
};
208+
cmcpp::HostUnicodeConversion convert = {}; // see test/host-util.cpp for an ICU-backed example
209+
cmcpp::GuestRealloc realloc = [&](int ptr, int old_size, int align, int new_size) {
210+
return guest_realloc(ptr, old_size, align, new_size);
211+
};
212+
213+
auto icx = cmcpp::createInstanceContext(trap, convert, realloc);
214+
```
215+
216+
When preparing to lift or lower values, create a `LiftLowerContext` from the instance. Pass the guest memory span and any canonical options you need:
217+
218+
```cpp
219+
cmcpp::Heap heap(4096);
220+
cmcpp::CanonicalOptions options;
221+
options.memory = cmcpp::GuestMemory(heap.memory.data(), heap.memory.size());
222+
options.string_encoding = cmcpp::Encoding::Utf8;
223+
options.realloc = icx->realloc;
224+
options.post_return = [] { /* guest cleanup */ };
225+
options.callback = [](cmcpp::EventCode code, uint32_t index, uint32_t payload) {
226+
std::printf("async event %u for handle %u (0x%x)\n",
227+
static_cast<unsigned>(code), index, payload);
228+
};
229+
options.sync = false; // allow async continuations
230+
231+
auto cx = icx->createLiftLowerContext(std::move(options));
232+
cx->inst = &component_instance;
233+
```
234+
235+
The canonical options determine whether async continuations are allowed (`sync`), which hook to run after a successful lowering (`post_return`), and how async notifications surface back to the embedder (`callback`). Every guest call that moves data across the ABI should use the same context until `LiftLowerContext::exit_call()` is invoked.
236+
237+
### Driving async flows with the runtime harness
176238
177-
The canonical Component Model runtime is cooperative: hosts must drive pending work by scheduling tasks explicitly. `cmcpp` now provides a minimal async harness in `cmcpp/runtime.hpp`:
239+
The Component Model runtime is cooperative: hosts advance work by draining a pending queue. `cmcpp/runtime.hpp` provides the same primitives as the canonical Python reference:
178240
179-
- `Store` owns the pending task queue and exposes `invoke` plus `tick()`.
241+
- `Store` owns the queue of `Thread` objects and exposes `invoke` plus `tick()`.
180242
- `FuncInst` is the callable signature hosts use to wrap guest functions.
181-
- `Thread::create` builds a pending task with user-supplied readiness/resume callbacks.
182-
- `Call::from_thread` returns a cancellation-capable handle to the caller.
183-
- `Task` coordinates canonical backpressure, `canon_task.{return,cancel}`, and `canon_yield` helpers exposed through `context.hpp`.
184-
- `canon_backpressure_{set,inc,dec}` update in-flight counters; most canonical entry points now guard `ComponentInstance::may_leave` before touching guest state.
243+
- `Thread::create` builds resumable work with readiness and resume callbacks.
244+
- `Call::from_thread` returns a handle that supports cancellation and completion queries.
245+
- `Task` bridges canonical backpressure (`canon_task.{return,cancel}`) and ensures `ComponentInstance::may_leave` rules are enforced.
185246
186-
Typical usage:
247+
A minimal async call looks like this:
187248
188249
```cpp
189250
cmcpp::Store store;
190-
cmcpp::FuncInst func = [](cmcpp::Store &store,
191-
cmcpp::SupertaskPtr,
192-
cmcpp::OnStart on_start,
193-
cmcpp::OnResolve on_resolve) {
251+
252+
cmcpp::FuncInst guest = [](cmcpp::Store &store,
253+
cmcpp::SupertaskPtr,
254+
cmcpp::OnStart on_start,
255+
cmcpp::OnResolve on_resolve) {
194256
auto args = std::make_shared<std::vector<std::any>>(on_start());
195-
auto gate = std::make_shared<std::atomic<bool>>(false);
257+
auto ready = std::make_shared<std::atomic<bool>>(false);
196258
197259
auto thread = cmcpp::Thread::create(
198-
store,
199-
[gate]() { return gate->load(); },
200-
[args, on_resolve](bool cancelled) {
201-
on_resolve(cancelled ? std::nullopt : std::optional{*args});
202-
return false; // finished
203-
},
204-
true,
205-
[gate]() { gate->store(true); });
260+
store,
261+
[ready] { return ready->load(); },
262+
[args, on_resolve](bool cancelled) {
263+
on_resolve(cancelled ? std::nullopt : std::optional{*args});
264+
return false; // one-shot
265+
},
266+
/*notify_on_cancel=*/true,
267+
[ready] { ready->store(true); });
206268
207269
return cmcpp::Call::from_thread(thread);
208270
};
209271
210-
auto call = store.invoke(func, nullptr, [] { return std::vector<std::any>{}; }, [](auto) {});
211-
// Drive progress
212-
store.tick();
272+
auto call = store.invoke(
273+
guest,
274+
nullptr,
275+
[] { return std::vector<std::any>{int32_t{7}}; },
276+
[](std::optional<std::vector<std::any>> values) {
277+
if (!values) { std::puts("cancelled"); return; }
278+
std::printf("resolved with %d\n", std::any_cast<int32_t>((*values)[0]));
279+
});
280+
281+
while (!call.completed()) {
282+
store.tick();
283+
}
213284
```
214285

215-
### Waitables, streams, and futures
286+
`Call::request_cancellation()` cooperatively aborts work before the next `tick()`, mirroring the canonical `cancel` semantics.
287+
288+
### Waitables, streams, futures, and other resources
289+
290+
`ComponentInstance` manages resource tables that back the canonical `canon_waitable_*`, `canon_stream_*`, and `canon_future_*` entry points. Hosts typically:
291+
292+
1. Instantiate a descriptor (`make_stream_descriptor<T>()`, `make_future_descriptor<T>()`, etc.).
293+
2. Create handles via `canon_stream_new`/`canon_future_new`, which return packed readable/writable indices.
294+
3. Join readable ends to a waitable set with `canon_waitable_join`.
295+
4. Poll readiness using `canon_waitable_set_poll`, decoding the `EventCode` and payload stored in guest memory.
296+
5. Drop resources with the corresponding `canon_*_drop_*` helpers once the guest is finished.
297+
298+
Streams and futures honour the canonical copy result payload layout, so the values copied into guest memory exactly match the spec. Cancellation helpers (`canon_stream_cancel_*`, `canon_future_cancel_*`) post events when the embedder requests termination, and the async callback registered in `CanonicalOptions` receives the same event triplet that the waitable set reports.
299+
300+
For a complete walkthrough, see the doctest suites in `test/main.cpp`:
216301

217-
The canonical async ABI surfaces are implemented via `canon_waitable_*`, `canon_stream_*`, and `canon_future_*` helpers on `ComponentInstance`. Waitable sets can be joined to readable/writable stream ends or futures, and `canon_waitable_set_poll` reports readiness using the same event payload layout defined by the spec. See the doctests in `test/main.cpp` for end-to-end examples.
302+
- "Async runtime schedules threads" demonstrates `Store`, `Thread`, `Call`, and cancellation.
303+
- "Waitable set surfaces stream readiness" polls a waitable set tied to a stream.
304+
- "Future lifecycle completes" verifies readable/writable futures.
305+
- "Task yield, cancel, and return" exercises backpressure and async task APIs.
218306

219-
Call `tick()` in your host loop until all pending work completes. Cancellation is cooperative: calling `Call::request_cancellation()` marks the associated thread as cancelled before the next `tick()`.
307+
Those tests are ICU-enabled and run automatically via `ctest`.
220308

221309

222310
## Related projects

docs/issue-backlog.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Draft issues ready to be copy-pasted into GitHub. Each entry lists a suggested t
66

77
## Issue: Implement canonical async runtime scaffolding
88
- **Labels:** `enhancement`, `async`
9+
- **Status:** Done
910

1011
### Background
1112
The canonical ABI reference (`design/mvp/canonical-abi/definitions.py`) includes a `Store`, `Thread`, and scheduling loop that enable cooperative async execution via `tick()`. The current C++ headers lack any runtime to drive asynchronous component calls.
@@ -26,6 +27,7 @@ The canonical ABI reference (`design/mvp/canonical-abi/definitions.py`) includes
2627

2728
## Issue: Complete resource handle lifecycle
2829
- **Labels:** `enhancement`, `abi`
30+
- **Status:** Done
2931

3032
### Background
3133
`ResourceHandle`, `ResourceType`, and `ComponentInstance.resources` are currently empty shells. The canonical implementation tracks ownership, borrow counts, destructors, and exposes `canon resource.{new,drop,rep}`.
@@ -49,6 +51,7 @@ The canonical ABI reference (`design/mvp/canonical-abi/definitions.py`) includes
4951

5052
### Background
5153
Canonical ABI defines waitables, waitable-sets, buffers, streams, and futures plus their cancellation behaviors. Our headers only contain empty structs.
54+
- **Status:** Done
5255

5356
### Scope
5457
- Model waitable and waitable-set state, including registration with the component store.
@@ -66,6 +69,7 @@ Canonical ABI defines waitables, waitable-sets, buffers, streams, and futures pl
6669

6770
## Issue: Implement backpressure and task lifecycle management
6871
- **Labels:** `enhancement`, `async`
72+
- **Status:** Done
6973

7074
### Background
7175
`ComponentInstance` holds flags for `may_leave`, `backpressure`, and call-state tracking but they are unused. Canonical ABI specifies `canon task.{return,cancel}`, `canon yield`, and backpressure counters governing concurrent entry.
@@ -86,6 +90,7 @@ Canonical ABI defines waitables, waitable-sets, buffers, streams, and futures pl
8690

8791
## Issue: Support context locals and error-context APIs
8892
- **Labels:** `enhancement`, `abi`
93+
- **Status:** Done
8994

9095
### Background
9196
`LiftLowerContext` currently omits instance references and borrow scopes, and `ContextLocalStorage`/`ErrorContext` types are unused. The canonical ABI exposes `canon context.{get,set}` and `canon error-context.{new,debug-message,drop}`.
@@ -106,6 +111,7 @@ Canonical ABI defines waitables, waitable-sets, buffers, streams, and futures pl
106111

107112
## Issue: Finish function flattening utilities
108113
- **Labels:** `enhancement`, `abi`
114+
- **Status:** Done
109115

110116
### Background
111117
`include/cmcpp/func.hpp` contains commented-out flattening helpers. Canonical ABI requires flattening functions to honor `MAX_FLAT_PARAMS/RESULTS` and spill to heap memory via the provided `realloc`.
@@ -146,6 +152,7 @@ Canonical ABI defines waitables, waitable-sets, buffers, streams, and futures pl
146152

147153
## Issue: Expand docs and tests for canonical runtime features
148154
- **Labels:** `documentation`, `testing`
155+
- **Status:** Done
149156

150157
### Background
151158
New runtime pieces require supporting documentation and tests. Currently, README lacks guidance and test coverage mirrors only existing functionality.

0 commit comments

Comments
 (0)