Skip to content

Commit 8807ce8

Browse files
c-api: wasi: Add custom callbacks for stdout/stderr
Fixes bytecodealliance#11963
1 parent d280865 commit 8807ce8

4 files changed

Lines changed: 147 additions & 1 deletion

File tree

crates/c-api/include/wasi.h

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,19 @@ WASI_API_EXTERN bool wasi_config_set_stdout_file(wasi_config_t *config,
145145
*/
146146
WASI_API_EXTERN void wasi_config_inherit_stdout(wasi_config_t *config);
147147

148+
/**
149+
* \brief Configures standard output to be directed to \p callback
150+
*
151+
* \param callback The callback that will get called for each write with the
152+
* buffer
153+
* \param data An optional user provided data that will be passed to \p callback
154+
* \param finalizer An optional callback to be called to destroy \p data
155+
*/
156+
WASI_API_EXTERN void wasi_config_set_stdout_custom(
157+
wasi_config_t *config,
158+
void (*callback)(void *, const unsigned char *, size_t), void *data,
159+
void (*finalizer)(void *));
160+
148161
/**
149162
* \brief Configures standard output to be written to the specified file.
150163
*
@@ -163,6 +176,19 @@ WASI_API_EXTERN bool wasi_config_set_stderr_file(wasi_config_t *config,
163176
*/
164177
WASI_API_EXTERN void wasi_config_inherit_stderr(wasi_config_t *config);
165178

179+
/**
180+
* \brief Configures standard error output to be directed to \p callback
181+
*
182+
* \param callback The callback that will get called for each write with the
183+
* buffer
184+
* \param data An optional user provided data that will be passed to \p callback
185+
* \param finalizer An optional callback to be called to destroy \p data
186+
*/
187+
WASI_API_EXTERN void wasi_config_set_stderr_custom(
188+
wasi_config_t *config,
189+
void (*callback)(void *, const unsigned char *, size_t), void *data,
190+
void (*finalizer)(void *));
191+
166192
/**
167193
* \brief The permissions granted for a directory when preopening it.
168194
*/

crates/c-api/src/wasi.rs

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
33
use crate::wasm_byte_vec_t;
44
use anyhow::Result;
5-
use std::ffi::{CStr, c_char};
5+
use std::ffi::{CStr, c_char, c_void};
66
use std::fs::File;
77
use std::path::Path;
88
use std::slice;
@@ -149,6 +149,34 @@ pub extern "C" fn wasi_config_inherit_stdout(config: &mut wasi_config_t) {
149149
config.builder.inherit_stdout();
150150
}
151151

152+
struct CustomOutputStream {
153+
foreign_data: crate::ForeignData,
154+
callback: extern "C" fn(*mut c_void, *const u8, usize),
155+
}
156+
157+
impl wasmtime_wasi::p2::pipe::SimpleCustomOutputWriter for CustomOutputStream {
158+
fn write(&self, buf: &[u8]) {
159+
(self.callback)(self.foreign_data.data, buf.as_ptr(), buf.len());
160+
}
161+
}
162+
163+
#[unsafe(no_mangle)]
164+
pub extern "C" fn wasi_config_set_stdout_custom(
165+
config: &mut wasi_config_t,
166+
callback: Option<extern "C" fn(*mut c_void, *const u8, usize)>,
167+
data: *mut c_void,
168+
finalizer: Option<extern "C" fn(*mut c_void)>,
169+
) {
170+
config
171+
.builder
172+
.stdout(wasmtime_wasi::p2::pipe::SimpleCustomOutputStream::new(
173+
CustomOutputStream {
174+
foreign_data: crate::ForeignData { data, finalizer },
175+
callback: callback.expect("Callback must be provided"),
176+
},
177+
));
178+
}
179+
152180
#[unsafe(no_mangle)]
153181
pub unsafe extern "C" fn wasi_config_set_stderr_file(
154182
config: &mut wasi_config_t,
@@ -171,6 +199,23 @@ pub extern "C" fn wasi_config_inherit_stderr(config: &mut wasi_config_t) {
171199
config.builder.inherit_stderr();
172200
}
173201

202+
#[unsafe(no_mangle)]
203+
pub extern "C" fn wasi_config_set_stderr_custom(
204+
config: &mut wasi_config_t,
205+
callback: Option<extern "C" fn(*mut c_void, *const u8, usize)>,
206+
data: *mut c_void,
207+
finalizer: Option<extern "C" fn(*mut c_void)>,
208+
) {
209+
config
210+
.builder
211+
.stderr(wasmtime_wasi::p2::pipe::SimpleCustomOutputStream::new(
212+
CustomOutputStream {
213+
foreign_data: crate::ForeignData { data, finalizer },
214+
callback: callback.expect("Callback must be provided"),
215+
},
216+
));
217+
}
218+
174219
#[unsafe(no_mangle)]
175220
pub unsafe extern "C" fn wasi_config_preopen_dir(
176221
config: &mut wasi_config_t,

crates/wasi/src/cli/mem.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,16 @@ impl StdoutStream for p2::pipe::MemoryOutputPipe {
3232
Box::new(self.clone())
3333
}
3434
}
35+
36+
// Implementation for p2::pipe::SimpleCustomOutputStream
37+
impl<T: p2::pipe::SimpleCustomOutputWriter> IsTerminal for p2::pipe::SimpleCustomOutputStream<T> {
38+
fn is_terminal(&self) -> bool {
39+
false
40+
}
41+
}
42+
43+
impl<T: p2::pipe::SimpleCustomOutputWriter> StdoutStream for p2::pipe::SimpleCustomOutputStream<T> {
44+
fn async_stream(&self) -> Box<dyn AsyncWrite + Send + Sync> {
45+
Box::new(Clone::clone(self))
46+
}
47+
}

crates/wasi/src/p2/pipe.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,68 @@ impl AsyncWrite for MemoryOutputPipe {
146146
}
147147
}
148148

149+
pub trait SimpleCustomOutputWriter: Send + Sync + 'static {
150+
fn write(&self, buf: &[u8]);
151+
}
152+
153+
pub struct SimpleCustomOutputStream<T: SimpleCustomOutputWriter> {
154+
writer: Arc<Mutex<T>>,
155+
}
156+
157+
impl<T: SimpleCustomOutputWriter> SimpleCustomOutputStream<T> {
158+
pub fn new(x: T) -> Self {
159+
Self {
160+
writer: Arc::new(Mutex::new(x)),
161+
}
162+
}
163+
}
164+
165+
impl<T: SimpleCustomOutputWriter> Clone for SimpleCustomOutputStream<T> {
166+
fn clone(&self) -> Self {
167+
Self {
168+
writer: self.writer.clone(),
169+
}
170+
}
171+
}
172+
173+
#[async_trait::async_trait]
174+
impl<T: SimpleCustomOutputWriter> Pollable for SimpleCustomOutputStream<T> {
175+
async fn ready(&mut self) {}
176+
}
177+
178+
#[async_trait::async_trait]
179+
impl<T: SimpleCustomOutputWriter> OutputStream for SimpleCustomOutputStream<T> {
180+
fn write(&mut self, bytes: Bytes) -> Result<(), StreamError> {
181+
self.writer.lock().unwrap().write(&bytes);
182+
// Always ready for writing
183+
Ok(())
184+
}
185+
fn flush(&mut self) -> Result<(), StreamError> {
186+
// This stream is always flushed
187+
Ok(())
188+
}
189+
fn check_write(&mut self) -> Result<usize, StreamError> {
190+
Ok(8192)
191+
}
192+
}
193+
194+
impl<T: SimpleCustomOutputWriter> AsyncWrite for SimpleCustomOutputStream<T> {
195+
fn poll_write(
196+
self: Pin<&mut Self>,
197+
_cx: &mut Context<'_>,
198+
buf: &[u8],
199+
) -> Poll<io::Result<usize>> {
200+
self.writer.lock().unwrap().write(buf);
201+
Poll::Ready(Ok(buf.len()))
202+
}
203+
fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
204+
Poll::Ready(Ok(()))
205+
}
206+
fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
207+
Poll::Ready(Ok(()))
208+
}
209+
}
210+
149211
/// Provides a [`InputStream`] impl from a [`tokio::io::AsyncRead`] impl
150212
pub struct AsyncReadStream {
151213
closed: bool,

0 commit comments

Comments
 (0)