Skip to content

Commit 498542c

Browse files
c-api: wasi: Add custom callbacks for stdout/stderr (#11965)
* c-api: wasi: Add custom callbacks for stdout/stderr Fixes #11963 * Move everything into c-api crate * Return `ptrdiff_t` * Smoke test * Use `io::Error::from_raw_os_error()` * Fixes * Fix * documentation prtest:full
1 parent 3a1ecd8 commit 498542c

File tree

6 files changed

+182
-2
lines changed

6 files changed

+182
-2
lines changed

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/c-api/Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ wat = { workspace = true, optional = true }
3434
cap-std = { workspace = true, optional = true }
3535
tokio = { workspace = true, optional = true, features = ["fs"] }
3636
wasmtime-wasi = { workspace = true, optional = true, features = ["p1"] }
37+
wasmtime-wasi-io = { workspace = true, optional = true, features = ["std"] }
38+
async-trait = { workspace = true, optional = true }
39+
bytes = { workspace = true, optional = true }
3740

3841
# Optional dependencies for the `async` feature
3942
futures = { workspace = true, optional = true }
@@ -44,7 +47,7 @@ async = ['wasmtime/async', 'futures']
4447
profiling = ["wasmtime/profiling"]
4548
cache = ["wasmtime/cache"]
4649
parallel-compilation = ['wasmtime/parallel-compilation']
47-
wasi = ['cap-std', 'wasmtime-wasi', 'tokio']
50+
wasi = ['cap-std', 'wasmtime-wasi', 'wasmtime-wasi-io', 'tokio', 'async-trait', 'bytes']
4851
logging = ['dep:env_logger']
4952
disable-logging = ["log/max_level_off", "tracing/max_level_off"]
5053
coredump = ["wasmtime/coredump"]

crates/c-api/include/wasi.h

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,21 @@ 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 config The config to operate on
152+
* \param callback A non-null callback must be provided, that will get called
153+
* for each write with the buffer. A positive return value indicates the amount
154+
* of bytes written. Negative return values are treated as OS error codes.
155+
* \param data An optional user provided data that will be passed to \p callback
156+
* \param finalizer An optional callback to be called to destroy \p data
157+
*/
158+
WASI_API_EXTERN void wasi_config_set_stdout_custom(
159+
wasi_config_t *config,
160+
ptrdiff_t (*callback)(void *, const unsigned char *, size_t), void *data,
161+
void (*finalizer)(void *));
162+
148163
/**
149164
* \brief Configures standard output to be written to the specified file.
150165
*
@@ -163,6 +178,21 @@ WASI_API_EXTERN bool wasi_config_set_stderr_file(wasi_config_t *config,
163178
*/
164179
WASI_API_EXTERN void wasi_config_inherit_stderr(wasi_config_t *config);
165180

181+
/**
182+
* \brief Configures standard error output to be directed to \p callback
183+
*
184+
* \param config The config to operate on
185+
* \param callback A non-null callback must be provided, that will get called
186+
* for each write with the buffer. A positive return value indicates the amount
187+
* of bytes written. Negative return values are treated as OS error codes.
188+
* \param data An optional user provided data that will be passed to \p callback
189+
* \param finalizer An optional callback to be called to destroy \p data
190+
*/
191+
WASI_API_EXTERN void wasi_config_set_stderr_custom(
192+
wasi_config_t *config,
193+
ptrdiff_t (*callback)(void *, const unsigned char *, size_t), void *data,
194+
void (*finalizer)(void *));
195+
166196
/**
167197
* \brief The permissions granted for a directory when preopening it.
168198
*/

crates/c-api/include/wasmtime/wasi.hh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@ public:
102102
return wasi_config_preopen_dir(ptr.get(), path.c_str(), guest_path.c_str(),
103103
dir_perms, file_perms);
104104
}
105+
106+
/// \brief Returns the underlying C API pointer.
107+
[[nodiscard]] wasi_config_t *capi() { return ptr.get(); }
105108
};
106109

107110
} // namespace wasmtime

crates/c-api/src/wasi.rs

Lines changed: 125 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,17 @@
22
33
use crate::wasm_byte_vec_t;
44
use anyhow::Result;
5-
use std::ffi::{CStr, c_char};
5+
use bytes::Bytes;
6+
use std::ffi::{CStr, c_char, c_void};
67
use std::fs::File;
78
use std::path::Path;
9+
use std::pin::Pin;
810
use std::slice;
11+
use std::task::{Context, Poll};
12+
use tokio::io::{self, AsyncWrite};
913
use wasmtime_wasi::WasiCtxBuilder;
1014
use wasmtime_wasi::p1::WasiP1Ctx;
15+
use wasmtime_wasi_io::streams::StreamError;
1116

1217
unsafe fn cstr_to_path<'a>(path: *const c_char) -> Option<&'a Path> {
1318
CStr::from_ptr(path).to_str().map(Path::new).ok()
@@ -149,6 +154,112 @@ pub extern "C" fn wasi_config_inherit_stdout(config: &mut wasi_config_t) {
149154
config.builder.inherit_stdout();
150155
}
151156

157+
struct CustomOutputStreamInner {
158+
foreign_data: crate::ForeignData,
159+
callback: extern "C" fn(*mut c_void, *const u8, usize) -> isize,
160+
}
161+
162+
impl CustomOutputStreamInner {
163+
pub fn raw_write(&self, buf: &[u8]) -> io::Result<usize> {
164+
let wrote = (self.callback)(self.foreign_data.data, buf.as_ptr(), buf.len());
165+
166+
if wrote >= 0 {
167+
Ok(wrote as _)
168+
} else {
169+
Err(io::Error::from_raw_os_error(wrote.abs() as _))
170+
}
171+
}
172+
}
173+
174+
#[derive(Clone)]
175+
pub struct CustomOutputStream {
176+
inner: std::sync::Arc<CustomOutputStreamInner>,
177+
}
178+
179+
impl CustomOutputStream {
180+
pub fn new(
181+
foreign_data: crate::ForeignData,
182+
callback: extern "C" fn(*mut c_void, *const u8, usize) -> isize,
183+
) -> Self {
184+
Self {
185+
inner: std::sync::Arc::new(CustomOutputStreamInner {
186+
foreign_data,
187+
callback,
188+
}),
189+
}
190+
}
191+
}
192+
193+
#[async_trait::async_trait]
194+
impl wasmtime_wasi::p2::Pollable for CustomOutputStream {
195+
async fn ready(&mut self) {}
196+
}
197+
198+
#[async_trait::async_trait]
199+
impl wasmtime_wasi::p2::OutputStream for CustomOutputStream {
200+
fn write(&mut self, bytes: Bytes) -> Result<(), StreamError> {
201+
let wrote = self
202+
.inner
203+
.raw_write(&bytes)
204+
.map_err(|e| StreamError::LastOperationFailed(e.into()))?;
205+
206+
if wrote != bytes.len() {
207+
return Err(StreamError::LastOperationFailed(anyhow::anyhow!(
208+
"Partial writes in wasip2 implementation are not allowed"
209+
)));
210+
}
211+
212+
Ok(())
213+
}
214+
fn flush(&mut self) -> Result<(), StreamError> {
215+
Ok(())
216+
}
217+
fn check_write(&mut self) -> Result<usize, StreamError> {
218+
Ok(usize::MAX)
219+
}
220+
}
221+
222+
impl AsyncWrite for CustomOutputStream {
223+
fn poll_write(
224+
self: Pin<&mut Self>,
225+
_cx: &mut Context<'_>,
226+
buf: &[u8],
227+
) -> Poll<io::Result<usize>> {
228+
Poll::Ready(self.inner.raw_write(buf))
229+
}
230+
fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
231+
Poll::Ready(Ok(()))
232+
}
233+
fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
234+
Poll::Ready(Ok(()))
235+
}
236+
}
237+
238+
impl wasmtime_wasi::cli::IsTerminal for CustomOutputStream {
239+
fn is_terminal(&self) -> bool {
240+
false
241+
}
242+
}
243+
244+
impl wasmtime_wasi::cli::StdoutStream for CustomOutputStream {
245+
fn async_stream(&self) -> Box<dyn AsyncWrite + Send + Sync> {
246+
Box::new(self.clone())
247+
}
248+
}
249+
250+
#[unsafe(no_mangle)]
251+
pub extern "C" fn wasi_config_set_stdout_custom(
252+
config: &mut wasi_config_t,
253+
callback: extern "C" fn(*mut c_void, *const u8, usize) -> isize,
254+
data: *mut c_void,
255+
finalizer: Option<extern "C" fn(*mut c_void)>,
256+
) {
257+
config.builder.stdout(CustomOutputStream::new(
258+
crate::ForeignData { data, finalizer },
259+
callback,
260+
));
261+
}
262+
152263
#[unsafe(no_mangle)]
153264
pub unsafe extern "C" fn wasi_config_set_stderr_file(
154265
config: &mut wasi_config_t,
@@ -171,6 +282,19 @@ pub extern "C" fn wasi_config_inherit_stderr(config: &mut wasi_config_t) {
171282
config.builder.inherit_stderr();
172283
}
173284

285+
#[unsafe(no_mangle)]
286+
pub extern "C" fn wasi_config_set_stderr_custom(
287+
config: &mut wasi_config_t,
288+
callback: extern "C" fn(*mut c_void, *const u8, usize) -> isize,
289+
data: *mut c_void,
290+
finalizer: Option<extern "C" fn(*mut c_void)>,
291+
) {
292+
config.builder.stderr(CustomOutputStream::new(
293+
crate::ForeignData { data, finalizer },
294+
callback,
295+
));
296+
}
297+
174298
#[unsafe(no_mangle)]
175299
pub unsafe extern "C" fn wasi_config_preopen_dir(
176300
config: &mut wasi_config_t,

crates/c-api/tests/wasip2.cc

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#include <gtest/gtest.h>
2+
#include <string_view>
23
#include <wasmtime/component.hh>
34
#include <wasmtime/store.hh>
45

@@ -20,6 +21,22 @@ TEST(wasip2, smoke) {
2021
auto context = store.context();
2122

2223
wasmtime::WasiConfig config;
24+
25+
wasi_config_set_stdout_custom(
26+
config.capi(),
27+
[](void *, const unsigned char *buf, size_t len) -> ptrdiff_t {
28+
std::cout << std::string_view{(const char *)(buf), len};
29+
return len;
30+
},
31+
nullptr, nullptr);
32+
wasi_config_set_stderr_custom(
33+
config.capi(),
34+
[](void *, const unsigned char *buf, size_t len) -> ptrdiff_t {
35+
std::cerr << std::string_view{(const char *)(buf), len};
36+
return len;
37+
},
38+
nullptr, nullptr);
39+
2340
context.set_wasi(std::move(config)).unwrap();
2441
Component component = Component::compile(engine, component_text).unwrap();
2542

0 commit comments

Comments
 (0)