Skip to content

Commit b38aa77

Browse files
committed
Implement a C ABI for async import/export communication
This commit is an implementation of a solution for WebAssembly/component-model#485 for Rust. This should enable releasing multiple versions of `wit-bindgen` into the wild and have them all work together for now. Integration with `wasi-libc` will come in the future in theory.
1 parent 74f5bdc commit b38aa77

14 files changed

Lines changed: 321 additions & 86 deletions
Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -41,45 +41,65 @@ set -ex
4141

4242
version=$(./ci/print-current-version.sh | sed 's/\./_/g')
4343

44-
sym=cabi_realloc_wit_bindgen_$version
44+
realloc=cabi_realloc_wit_bindgen_$version
45+
wasip3_task_set=wasip3_task_set_wit_bindgen_$version
46+
wasip3_task_get=wasip3_task_get_wit_bindgen_$version
4547

46-
cat >./crates/guest-rust/rt/src/cabi_realloc.rs <<-EOF
48+
cat >./crates/guest-rust/rt/src/wit_bindgen_cabi.rs <<-EOF
4749
// This file is generated by $0
4850
4951
#[unsafe(no_mangle)]
50-
pub unsafe extern "C" fn $sym(
52+
pub unsafe extern "C" fn $realloc(
5153
old_ptr: *mut u8,
5254
old_len: usize,
5355
align: usize,
5456
new_len: usize,
5557
) -> *mut u8 {
5658
crate::cabi_realloc(old_ptr, old_len, align, new_len)
5759
}
60+
61+
static mut WASIP3_TASK: *mut u8 = core::ptr::null_mut();
62+
63+
#[unsafe(no_mangle)]
64+
pub unsafe extern "C" fn $wasip3_task_set(ptr: *mut u8) -> *mut u8 {
65+
unsafe {
66+
let ret = WASIP3_TASK;
67+
WASIP3_TASK = ptr;
68+
ret
69+
}
70+
}
5871
EOF
5972

60-
cat >./crates/guest-rust/rt/src/cabi_realloc.c <<-EOF
73+
cat >./crates/guest-rust/rt/src/wit_bindgen_cabi.c <<-EOF
6174
// This file is generated by $0
6275
6376
#include <stdint.h>
6477
65-
extern void *$sym(void *ptr, size_t old_size, size_t align, size_t new_size);
78+
extern void *$realloc(void *ptr, size_t old_size, size_t align, size_t new_size);
6679
6780
__attribute__((__weak__, __export_name__("cabi_realloc")))
6881
void *cabi_realloc(void *ptr, size_t old_size, size_t align, size_t new_size) {
69-
return $sym(ptr, old_size, align, new_size);
82+
return $realloc(ptr, old_size, align, new_size);
83+
}
84+
85+
extern void *$wasip3_task_set(void *ptr);
86+
87+
__attribute__((__weak__))
88+
void *wasip3_task_set(void *ptr) {
89+
return $wasip3_task_set(ptr);
7090
}
7191
EOF
7292

73-
rm -f crates/guest-rust/rt/src/cabi_realloc.o
74-
$WASI_SDK_PATH/bin/clang crates/guest-rust/rt/src/cabi_realloc.c \
75-
-O -c -o crates/guest-rust/rt/src/cabi_realloc.o
93+
rm -f crates/guest-rust/rt/src/wit_bindgen_cabi.o
94+
$WASI_SDK_PATH/bin/clang crates/guest-rust/rt/src/wit_bindgen_cabi.c \
95+
-O -c -o crates/guest-rust/rt/src/wit_bindgen_cabi.o
7696

7797
# Remove the `producers` section. This appears to differ whether the host for
7898
# clang is either macOS or Linux. Not needed here anyway, so discard it to help
7999
# either host produce the same object.
80-
wasm-tools strip -d producers ./crates/guest-rust/rt/src/cabi_realloc.o \
81-
-o ./crates/guest-rust/rt/src/cabi_realloc.o
100+
wasm-tools strip -d producers ./crates/guest-rust/rt/src/wit_bindgen_cabi.o \
101+
-o ./crates/guest-rust/rt/src/wit_bindgen_cabi.o
82102

83-
rm -f crates/guest-rust/rt/src/libwit_bindgen_cabi_realloc.a
84-
$WASI_SDK_PATH/bin/llvm-ar crus crates/guest-rust/rt/src/libwit_bindgen_cabi_realloc.a \
85-
crates/guest-rust/rt/src/cabi_realloc.o
103+
rm -f crates/guest-rust/rt/src/libwit_bindgen_cabi.a
104+
$WASI_SDK_PATH/bin/llvm-ar crus crates/guest-rust/rt/src/libwit_bindgen_cabi.a \
105+
crates/guest-rust/rt/src/wit_bindgen_cabi.o

crates/guest-rust/rt/README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@ crate is to contain "runtime" code related to the macro-expansion of the
88
be removed in some situations.
99

1010
This crate contains a precompiled object file and archive at
11-
`src/cabi_realloc.o` and `src/libwit_bindgen_cabi_realloc.a`. This is compiled
12-
from the source `src/cabi_realloc.c` and is checked in as precompiled to avoid
13-
needing a C compiler at compile-time which isn't always available. This object
14-
file is only used on wasm targets.
11+
`src/wit_bindgen_cabi.o` and `src/libwit_bindgen_cabi.a`. This is compiled
12+
from the source `src/wit_bindgen_cabi.c` and is checked in as precompiled to
13+
avoid needing a C compiler at compile-time which isn't always available. This
14+
object file is only used on wasm targets.
1515

1616
The object file is compiled by
17-
[this script]https://github.com/bytecodealliance/wit-bindgen/blob/main/ci/rebuild-libcabi-realloc.sh)
17+
[this script]https://github.com/bytecodealliance/wit-bindgen/blob/main/ci/rebuild-libwit-bindgen-cabi.sh)
1818
and is verified in repository continuous integration that the checked-in
1919
versions match what CI produces.
2020

crates/guest-rust/rt/build.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ fn main() {
1616

1717
let mut src = env::current_dir().unwrap();
1818
src.push("src");
19-
src.push("libwit_bindgen_cabi_realloc.a");
19+
src.push("libwit_bindgen_cabi.a");
2020

2121
let dst_name = format!(
22-
"wit_bindgen_cabi_realloc{}",
22+
"wit_bindgen_cabi{}",
2323
env!("CARGO_PKG_VERSION").replace(".", "_")
2424
);
2525
let dst = out_dir.join(format!("lib{dst_name}.a"));

crates/guest-rust/rt/src/async_support.rs

Lines changed: 74 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@ extern crate std;
88
use core::sync::atomic::{AtomicBool, Ordering};
99
use std::boxed::Box;
1010
use std::collections::HashMap;
11+
use std::ffi::c_void;
1112
use std::fmt::{self, Debug, Display};
1213
use std::future::Future;
1314
use std::mem;
1415
use std::pin::Pin;
1516
use std::ptr;
1617
use std::string::String;
1718
use std::sync::Arc;
18-
use std::task::{Context, Poll, Wake, Waker};
19+
use std::task::{Context, Poll, Wake};
1920
use std::vec::Vec;
2021

2122
use futures::channel::oneshot;
@@ -36,6 +37,7 @@ macro_rules! rtdebug {
3637
}
3738

3839
mod abi_buffer;
40+
mod cabi;
3941
mod future_support;
4042
mod stream_support;
4143
mod waitable;
@@ -60,21 +62,32 @@ struct FutureState {
6062
tasks: Option<FuturesUnordered<BoxFuture>>,
6163
/// The waitable set containing waitables created by this task, if any.
6264
waitable_set: Option<u32>,
63-
/// A map of waitables to the corresponding waker and completion code.
64-
///
65-
/// This is primarily filled in and managed by `WaitableOperation<S>`. The
66-
/// waker here comes straight from `std::task::Context` and the pointer is
67-
/// otherwise stored within the `WaitableOperation<S>` The raw pointer here
68-
/// has a disconnected lifetime with each future but the management of the
69-
/// internal states with respect to drop should always ensure that this is
70-
/// only ever pointing to active waitable operations.
71-
///
72-
/// When a waitable notification is received the corresponding entry in this
73-
/// map is removed, the status code is filled in, and the waker is notified.
74-
wakers: HashMap<u32, (Waker, *mut Option<u32>)>,
65+
66+
/// State of all waitables in `waitable_set`, and the ptr/callback they're
67+
/// associated with.
68+
waitables: HashMap<u32, (*mut c_void, unsafe extern "C" fn(*mut c_void, u32))>,
69+
70+
/// Raw structure used to pass to `cabi::wasip3_task_set`
71+
wasip3_task: cabi::wasip3_task,
7572
}
7673

7774
impl FutureState {
75+
fn new(future: BoxFuture) -> FutureState {
76+
FutureState {
77+
todo: 0,
78+
tasks: Some([future].into_iter().collect()),
79+
waitable_set: None,
80+
waitables: HashMap::new(),
81+
wasip3_task: cabi::wasip3_task {
82+
// This pointer is filled in before calling `wasip3_task_set`.
83+
ptr: ptr::null_mut(),
84+
version: cabi::WASIP3_TASK_V1,
85+
waitable_register,
86+
waitable_unregister,
87+
},
88+
}
89+
}
90+
7891
fn get_or_create_waitable_set(&mut self) -> u32 {
7992
*self.waitable_set.get_or_insert_with(waitable_set_new)
8093
}
@@ -88,7 +101,32 @@ impl FutureState {
88101
}
89102

90103
fn remaining_work(&self) -> bool {
91-
self.todo > 0 || !self.wakers.is_empty()
104+
self.todo > 0 || !self.waitables.is_empty()
105+
}
106+
}
107+
108+
unsafe extern "C" fn waitable_register(
109+
ptr: *mut c_void,
110+
waitable: u32,
111+
callback: unsafe extern "C" fn(*mut c_void, u32),
112+
callback_ptr: *mut c_void,
113+
) -> *mut c_void {
114+
let ptr = ptr.cast::<FutureState>();
115+
assert!(!ptr.is_null());
116+
(*ptr).add_waitable(waitable);
117+
match (*ptr).waitables.insert(waitable, (callback_ptr, callback)) {
118+
Some((prev, _)) => prev,
119+
None => ptr::null_mut(),
120+
}
121+
}
122+
123+
unsafe extern "C" fn waitable_unregister(ptr: *mut c_void, waitable: u32) -> *mut c_void {
124+
let ptr = ptr.cast::<FutureState>();
125+
assert!(!ptr.is_null());
126+
(*ptr).remove_waitable(waitable);
127+
match (*ptr).waitables.remove(&waitable) {
128+
Some((prev, _)) => prev,
129+
None => ptr::null_mut(),
92130
}
93131
}
94132

@@ -145,6 +183,22 @@ unsafe fn poll(state: *mut FutureState) -> Poll<()> {
145183
}
146184
}
147185

186+
// Finish our `wasip3_task` by initializing its self-referential pointer,
187+
// and then register it for the duration of this function with
188+
// `wasip3_task_set`. The previous value of `wasip3_task_set` will get
189+
// restored when this function returns.
190+
struct ResetTask(*mut cabi::wasip3_task);
191+
impl Drop for ResetTask {
192+
fn drop(&mut self) {
193+
unsafe {
194+
cabi::wasip3_task_set(self.0);
195+
}
196+
}
197+
}
198+
(*state).wasip3_task.ptr = state.cast();
199+
let prev = cabi::wasip3_task_set(&mut (*state).wasip3_task);
200+
let _reset = ResetTask(prev);
201+
148202
loop {
149203
if let Some(futures) = (*state).tasks.as_mut() {
150204
let old = CURRENT;
@@ -191,16 +245,9 @@ pub fn first_poll<T: 'static>(
191245
future: impl Future<Output = T> + 'static,
192246
fun: impl FnOnce(&T) + 'static,
193247
) -> i32 {
194-
let state = Box::into_raw(Box::new(FutureState {
195-
todo: 0,
196-
tasks: Some(
197-
[Box::pin(future.map(|v| fun(&v))) as BoxFuture]
198-
.into_iter()
199-
.collect(),
200-
),
201-
waitable_set: None,
202-
wakers: HashMap::new(),
203-
}));
248+
let state = Box::into_raw(Box::new(FutureState::new(Box::pin(
249+
future.map(|v| fun(&v)),
250+
))));
204251
let done = unsafe { poll(state).is_ready() };
205252
unsafe { callback_code(state, done) }
206253
}
@@ -339,9 +386,8 @@ unsafe fn callback_with_state(
339386
"EVENT_{{STREAM,FUTURE}}_{{READ,WRITE}}({event0:#x}, {event1:#x}, {event2:#x})"
340387
);
341388
(*state).remove_waitable(event1 as u32);
342-
let (waker, code) = (*state).wakers.remove(&(event1 as u32)).unwrap();
343-
*code = Some(event2 as u32);
344-
waker.wake();
389+
let (ptr, callback) = (*state).waitables.remove(&(event1 as u32)).unwrap();
390+
callback(ptr, event2 as u32);
345391

346392
let done = poll(state).is_ready();
347393
callback_code(state, done)
@@ -469,16 +515,7 @@ pub fn spawn(future: impl Future<Output = ()> + 'static) {
469515
// TODO: refactor so `'static` bounds aren't necessary
470516
pub fn block_on<T: 'static>(future: impl Future<Output = T> + 'static) -> T {
471517
let (tx, mut rx) = oneshot::channel();
472-
let state = &mut FutureState {
473-
todo: 0,
474-
tasks: Some(
475-
[Box::pin(future.map(move |v| drop(tx.send(v)))) as BoxFuture]
476-
.into_iter()
477-
.collect(),
478-
),
479-
waitable_set: None,
480-
wakers: HashMap::new(),
481-
};
518+
let state = &mut FutureState::new(Box::pin(future.map(move |v| drop(tx.send(v)))) as BoxFuture);
482519
loop {
483520
match unsafe { poll(state) } {
484521
Poll::Ready(()) => break rx.try_recv().unwrap().unwrap(),
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
//! Definition of the "C ABI" of how imported functions interact with exported
2+
//! tasks.
3+
//!
4+
//! Ok this crate is written in Rust, why in the world does this exist? This
5+
//! comment is intended to explain this rationale but the tl;dr; is we want
6+
//! this to work:
7+
//!
8+
//! * Within a single component ...
9+
//! * One rust crate uses `wit-bindgen 0.A.0` to generate an exported function.
10+
//! * One rust crate uses `wit-bindgen 0.B.0` to bind an imported function.
11+
//! * The two crates are connected in the application with
12+
//! `std::future::Future`.
13+
//!
14+
//! Without this module this situation won't work because 0.A.0 has no
15+
//! knowledge of 0.B.0 meaning that when 0.B.0 decides to block it won't know
16+
//! where to register its `waitable` within a `waitable-set`.
17+
//!
18+
//! To solve this problem the long-term intention is that something will live
19+
//! in `wasi-libc` itself, but in the meantime it's living "somewhere" within
20+
//! `wit-bindgen 0.*.0`. Specifically all `wit-bindgen` versions will all
21+
//! reference, via C linkage, a single function which is used to manipulate a
22+
//! single pointer in linear memory. This pointer is a `wasip3_task` structure
23+
//! which has all the various fields to use it.
24+
//!
25+
//! The `wasip3_task_set` symbol is itself defined in C inside of the
26+
//! `src/wit_bindgen_cabi.c` file at this time, specifically because it's
27+
//! annotated with `__weak__` meaning that any definition of it suffices. This
28+
//! isn't possible to define in stable Rust (specifically `__weak__`).
29+
//!
30+
//! Once `wasip3_task_set` is defined everything then operates via indirection,
31+
//! aka based off the returned pointer. The intention is that exported functions
32+
//! will set this (it's sort of like an executor) and then imported functions
33+
//! will all use this as the source of registering waitables. In the end that
34+
//! means that it's possible to share types with `std::future::Future` that
35+
//! are backed at the ABI level with this "channel".
36+
//!
37+
//! In the future it's hoped that this can move into `wasi-libc` itself, or if
38+
//! `wasi-libc` provides something else that would be prioritized over this.
39+
//! For now this is basically an affordance that we're going to be frequently
40+
//! releaseing new major versions of `wit-bindgen` and we don't want to force
41+
//! applications to all be using the exact same version of the bindings
42+
//! generator and async bindings.
43+
//!
44+
//! Additionally for now this file is serving as documentation of this
45+
//! interface.
46+
47+
use core::ffi::c_void;
48+
49+
extern "C" {
50+
/// Sets the global task pointer to `ptr` provided. Returns the previous
51+
/// value.
52+
///
53+
/// This function acts as both a dual getter and a setter. To get the
54+
/// current task pointer a dummy `ptr` can be provided (e.g. NULL) and then
55+
/// it's passed back when you're done working with it. When setting the
56+
/// current task pointer it's recommended to call this and then call it
57+
/// again with the previous value when the tasks's work is done.
58+
///
59+
/// For executors they need to ensure that the `ptr` passed in lives for
60+
/// the entire lifetime of the component model task.
61+
pub fn wasip3_task_set(ptr: *mut wasip3_task) -> *mut wasip3_task;
62+
}
63+
64+
/// The first version of `wasip3_task` which implies the existence of the
65+
/// fields `ptr`, `waitable_register`, and `waitable_unregister`.
66+
pub const WASIP3_TASK_V1: u32 = 1;
67+
68+
/// Indirect "vtable" used to connect imported functions and exported tasks.
69+
/// Executors (e.g. exported functions) define and manage this while imports
70+
/// use it.
71+
#[repr(C)]
72+
pub struct wasip3_task {
73+
/// Currently `WASIP3_TASK_V1`. Indicates what fields are present next
74+
/// depending on the version here.
75+
pub version: u32,
76+
77+
/// Private pointer owned by the `wasip3_task` itself, passed to callbacks
78+
/// below as the first argument.
79+
pub ptr: *mut c_void,
80+
81+
/// Register a new `waitable` for this exported task.
82+
///
83+
/// This exported task will add `waitable` to its `waitable-set`. When it
84+
/// becomes ready then `callback` will be invoked with the ready code as
85+
/// well as the `callback_ptr` provided.
86+
///
87+
/// If `waitable` was previously registered with this task then the
88+
/// previuos `callback_ptr` is returned. Otherwise `NULL` is returned.
89+
///
90+
/// It's the caller's responsibility to ensure that `callback_ptr` is valid
91+
/// until `callback` is invoked, `waitable_unregister` is invoked, or
92+
/// `waitable_register` is called again to overwrite the value.
93+
pub waitable_register: unsafe extern "C" fn(
94+
ptr: *mut c_void,
95+
waitable: u32,
96+
callback: unsafe extern "C" fn(callback_ptr: *mut c_void, code: u32),
97+
callback_ptr: *mut c_void,
98+
) -> *mut c_void,
99+
100+
/// Removes the `waitable` from this task's `waitable-set`.
101+
///
102+
/// Returns the `callback_ptr` passed to `waitable_register` if present, or
103+
/// `NULL` if it's not present.
104+
pub waitable_unregister: unsafe extern "C" fn(ptr: *mut c_void, waitable: u32) -> *mut c_void,
105+
}

0 commit comments

Comments
 (0)