Skip to content
Open
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
34 changes: 25 additions & 9 deletions crates/core/src/abi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1069,10 +1069,8 @@ impl<'a, B: Bindgen> Generator<'a, B> {
let ptr = match variant {
// When a wasm module calls an import it will provide
// space that isn't explicitly deallocated.
AbiVariant::GuestImport => self.bindgen.return_pointer(size, align),

AbiVariant::GuestImportAsync => {
todo!("direct param lowering for async guest import not implemented")
AbiVariant::GuestImport | AbiVariant::GuestImportAsync => {
self.bindgen.return_pointer(size, align)
}

// When calling a wasm module from the outside, though,
Expand Down Expand Up @@ -1105,7 +1103,11 @@ impl<'a, B: Bindgen> Generator<'a, B> {
self.realloc = None;

// If necessary we may need to prepare a return pointer for this ABI.
if variant == AbiVariant::GuestImport && sig.retptr {
if matches!(
variant,
AbiVariant::GuestImport | AbiVariant::GuestImportAsync
) && sig.retptr
{
let info = self.bindgen.sizes().params(&func.result);
let ptr = self.bindgen.return_pointer(info.size, info.align);
self.return_pointer = Some(ptr.clone());
Expand Down Expand Up @@ -1135,14 +1137,17 @@ impl<'a, B: Bindgen> Generator<'a, B> {
self.return_pointer.take().unwrap()
}

AbiVariant::GuestImportAsync => {
assert_eq!(sig.results.len(), 1);
self.return_pointer.take().unwrap()
}

// guest exports means that this is a host
// calling wasm so wasm returned a pointer to where
// the result is stored
AbiVariant::GuestExport => self.stack.pop().unwrap(),

AbiVariant::GuestImportAsync
| AbiVariant::GuestExportAsync
| AbiVariant::GuestExportAsyncStackful => {
AbiVariant::GuestExportAsync | AbiVariant::GuestExportAsyncStackful => {
unreachable!()
}
};
Expand Down Expand Up @@ -1177,9 +1182,20 @@ impl<'a, B: Bindgen> Generator<'a, B> {

// Emit the function return
if async_ {
if sig.retptr {
// In an async guest import with retptr, CallWasm pushed the status result `ret`,
// and `read_results_from_memory` pushed the lifted result. Pop `ret` so only the
// lifted result remains on the stack for AsyncTaskReturn.
let status_idx = self.stack.len() - 1 - usize::from(func.result.is_some());
self.stack.remove(status_idx);
}
self.emit(&Instruction::AsyncTaskReturn {
name: &func.name,
params: &sig.results,
params: if sig.retptr && func.result.is_some() {
&[WasmType::Pointer]
} else {
&sig.results
},
});
} else {
self.emit(&Instruction::Return {
Expand Down
13 changes: 13 additions & 0 deletions crates/cpp/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,16 @@ For now for functions the guest import convention is used in both directions:
with one modification:

Resource IDs become usize, so you can optimize the resource table away.

## C++20 Coroutine & Async Support

The C++ backend targets C++20 (`-std=c++20`).

Asynchronous functions in WIT are mapped to idiomatic C++20 coroutines:
- Functions defined as `async func(...) -> T` or filtered via `--async`/`--sync` return `wit::Task<T>` (or `wit::Task<void>` for functions returning void).
- Callers can `co_await` imported async functions or inspect completion with `.done()`, `.get()`.
- Export implementations use `co_return` (or `co_return value;`) within a coroutine body returning `wit::Task<T>`.
- WIT `future<T>` is mapped to `wit::FutureReader<T>`.
- WIT `stream<T>` is mapped to `wit::StreamReader<T>`.
- WIT `error-context` is mapped to `wit::ErrorContext`.

167 changes: 167 additions & 0 deletions crates/cpp/helper-types/wit.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include <new>
#include <span>
#include <utility> // pair
#include <coroutine>

namespace wit {
/// @brief Helper class to map between IDs and resources
Expand Down Expand Up @@ -331,4 +332,170 @@ class ResourceImportBase {
}
ResourceImportBase &operator=(ResourceImportBase const &r) = delete;
};

/// @brief Represents an ongoing asynchronous computation in C++20 coroutines.
template <typename T = void>
class Task {
public:
struct promise_type;
using handle_type = std::coroutine_handle<promise_type>;

struct promise_type {
std::optional<T> value;
std::coroutine_handle<> continuation = nullptr;

Task get_return_object() {
return Task(handle_type::from_promise(*this));
}
std::suspend_never initial_suspend() noexcept { return {}; }
auto final_suspend() noexcept {
struct FinalAwaiter {
bool await_ready() noexcept { return false; }
std::coroutine_handle<> await_suspend(handle_type h) noexcept {
auto cont = h.promise().continuation;
if (cont) return cont;
return std::noop_coroutine();
}
void await_resume() noexcept {}
};
return FinalAwaiter{};
}
void return_value(T v) {
value = std::move(v);
}
void unhandled_exception() {
assert(false && "unhandled exception in wit::Task");
}
};

handle_type handle = nullptr;

Task() = default;
explicit Task(handle_type h) : handle(h) {}
Task(Task &&other) noexcept : handle(other.handle) { other.handle = nullptr; }
Task &operator=(Task &&other) noexcept {
if (this != &other) {
if (handle) handle.destroy();
handle = other.handle;
other.handle = nullptr;
}
return *this;
}
~Task() {
if (handle) handle.destroy();
}
Task(const Task &) = delete;
Task &operator=(const Task &) = delete;

bool await_ready() const noexcept {
return !handle || handle.done();
}
std::coroutine_handle<> await_suspend(std::coroutine_handle<> awaiting) noexcept {
handle.promise().continuation = awaiting;
return handle;
}
T await_resume() {
assert(handle && handle.promise().value.has_value());
return std::move(*handle.promise().value);
}

bool done() const noexcept {
return !handle || handle.done();
}

T get() {
assert(handle && handle.promise().value.has_value());
return std::move(*handle.promise().value);
}
};

template <>
class Task<void> {
public:
struct promise_type;
using handle_type = std::coroutine_handle<promise_type>;

struct promise_type {
std::coroutine_handle<> continuation = nullptr;

Task get_return_object() {
return Task(handle_type::from_promise(*this));
}
std::suspend_never initial_suspend() noexcept { return {}; }
auto final_suspend() noexcept {
struct FinalAwaiter {
bool await_ready() noexcept { return false; }
std::coroutine_handle<> await_suspend(handle_type h) noexcept {
auto cont = h.promise().continuation;
if (cont) return cont;
return std::noop_coroutine();
}
void await_resume() noexcept {}
};
return FinalAwaiter{};
}
void return_void() {}
void unhandled_exception() {
assert(false && "unhandled exception in wit::Task");
}
};

handle_type handle = nullptr;

Task() = default;
explicit Task(handle_type h) : handle(h) {}
Task(Task &&other) noexcept : handle(other.handle) { other.handle = nullptr; }
Task &operator=(Task &&other) noexcept {
if (this != &other) {
if (handle) handle.destroy();
handle = other.handle;
other.handle = nullptr;
}
return *this;
}
~Task() {
if (handle) handle.destroy();
}
Task(const Task &) = delete;
Task &operator=(const Task &) = delete;

bool await_ready() const noexcept {
return !handle || handle.done();
}
std::coroutine_handle<> await_suspend(std::coroutine_handle<> awaiting) noexcept {
handle.promise().continuation = awaiting;
return handle;
}
void await_resume() {}

bool done() const noexcept {
return !handle || handle.done();
}
};

template <typename T = void>
class FutureReader {
public:
int32_t handle = -1;
FutureReader() = default;
explicit FutureReader(int32_t h) : handle(h) {}
int32_t get_handle() const { return handle; }
};

template <typename T = void>
class StreamReader {
public:
int32_t handle = -1;
StreamReader() = default;
explicit StreamReader(int32_t h) : handle(h) {}
int32_t get_handle() const { return handle; }
};

class ErrorContext {
public:
int32_t handle = -1;
ErrorContext() = default;
explicit ErrorContext(int32_t h) : handle(h) {}
int32_t get_handle() const { return handle; }
};
} // namespace wit
Loading
Loading