Skip to content

Commit 4942bae

Browse files
authored
Implement capturing stdout/stderr in Python (#311)
Bind a recent addition to the C API Closes #34
1 parent ea51991 commit 4942bae

File tree

2 files changed

+110
-2
lines changed

2 files changed

+110
-2
lines changed

tests/test_wasi.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,65 @@ def preopen_nonexistent(self):
5252
config = WasiConfig()
5353
with self.assertRaises(WasmtimeError):
5454
config.preopen_dir('/path/to/nowhere', '/', DirPerms.READ_ONLY, FilePerms.READ_ONLY)
55+
56+
def test_custom_print(self):
57+
linker = Linker(Engine())
58+
linker.define_wasi()
59+
60+
stderr = ''
61+
stdout = ''
62+
63+
def on_stdout(data: bytes) -> None:
64+
nonlocal stdout
65+
stdout += data.decode('utf8')
66+
67+
def on_stderr(data: bytes) -> None:
68+
nonlocal stderr
69+
stderr += data.decode('utf8')
70+
71+
module = Module(linker.engine, """
72+
(module
73+
(import "wasi_snapshot_preview1" "fd_write"
74+
(func $write (param i32 i32 i32 i32) (result i32)))
75+
76+
(memory (export "memory") 1)
77+
78+
(func $print
79+
(i32.store (i32.const 300) (i32.const 100)) ;; iov base
80+
(i32.store (i32.const 304) (i32.const 14)) ;; iov len
81+
82+
(call $write
83+
(i32.const 1) ;; fd 1 is stdout
84+
(i32.const 300) ;; iovecs ptr
85+
(i32.const 1) ;; iovecs len
86+
(i32.const 400)) ;; nwritten ptr
87+
if unreachable end ;; verify no error
88+
89+
(i32.store (i32.const 300) (i32.const 200)) ;; iov base
90+
(i32.store (i32.const 304) (i32.const 14)) ;; iov len
91+
92+
(call $write
93+
(i32.const 2) ;; fd 2 is stderr
94+
(i32.const 300) ;; iovecs ptr
95+
(i32.const 1) ;; iovecs len
96+
(i32.const 400)) ;; nwritten ptr
97+
if unreachable end ;; verify no error
98+
)
99+
100+
(start $print)
101+
102+
(data (i32.const 100) "Hello, stdout!")
103+
(data (i32.const 200) "Hello, stderr!")
104+
)
105+
""")
106+
107+
wasi = WasiConfig()
108+
wasi.stdout_custom = on_stdout
109+
wasi.stderr_custom = on_stderr
110+
111+
store = Store(linker.engine)
112+
store.set_wasi(wasi)
113+
linker.instantiate(store, module)
114+
115+
self.assertEqual('Hello, stdout!', stdout)
116+
self.assertEqual('Hello, stderr!', stderr)

wasmtime/_wasi.py

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import ctypes
2-
from ctypes import POINTER, c_char, c_char_p, cast
2+
import errno
3+
from ctypes import POINTER, c_char, c_char_p, cast, CFUNCTYPE, c_void_p
34
from enum import Enum
45
from os import PathLike
5-
from typing import Iterable, List, Union
6+
from typing import Iterable, List, Union, Callable
67

78
from wasmtime import Managed, WasmtimeError
89

910
from . import _ffi as ffi
1011
from ._config import setter_property
12+
from ._func import Slab
1113

1214

1315
def _encode_path(path: Union[str, bytes, PathLike]) -> bytes:
@@ -29,6 +31,11 @@ class FilePerms(Enum):
2931
WRITE_ONLY = ffi.wasi_file_perms_flags.WASMTIME_WASI_FILE_PERMS_WRITE.value
3032
READ_WRITE = ffi.wasi_file_perms_flags.WASMTIME_WASI_FILE_PERMS_READ.value | ffi.wasi_file_perms_flags.WASMTIME_WASI_FILE_PERMS_WRITE.value
3133

34+
35+
CustomOutput = Callable[[bytes], Union[int, None]]
36+
CUSTOM_OUTPUTS: Slab[CustomOutput] = Slab()
37+
38+
3239
class WasiConfig(Managed["ctypes._Pointer[ffi.wasi_config_t]"]):
3340

3441
def __init__(self) -> None:
@@ -119,6 +126,15 @@ def stdout_file(self, path: str) -> None:
119126
if not res:
120127
raise WasmtimeError("failed to set stdout file")
121128

129+
@setter_property
130+
def stdout_custom(self, callback: CustomOutput) -> None:
131+
"""
132+
Sets a custom `callback` that is invoked whenever stdout is written to.
133+
"""
134+
ffi.wasi_config_set_stdout_custom(
135+
self.ptr(), custom_call,
136+
CUSTOM_OUTPUTS.allocate(callback), custom_finalize)
137+
122138
def inherit_stdout(self) -> None:
123139
"""
124140
Configures this own process's stdout to be used as the WASI program's
@@ -145,6 +161,15 @@ def stderr_file(self, path: str) -> None:
145161
if not res:
146162
raise WasmtimeError("failed to set stderr file")
147163

164+
@setter_property
165+
def stderr_custom(self, callback: CustomOutput) -> None:
166+
"""
167+
Sets a custom `callback` that is invoked whenever stderr is written to.
168+
"""
169+
ffi.wasi_config_set_stderr_custom(
170+
self.ptr(), custom_call,
171+
CUSTOM_OUTPUTS.allocate(callback), custom_finalize)
172+
148173
def inherit_stderr(self) -> None:
149174
"""
150175
Configures this own process's stderr to be used as the WASI program's
@@ -177,3 +202,24 @@ def to_char_array(strings: List[str]) -> "ctypes._Pointer[ctypes._Pointer[c_char
177202
for i, s in enumerate(strings):
178203
ptrs[i] = c_char_p(s.encode('utf-8'))
179204
return cast(ptrs, POINTER(POINTER(c_char)))
205+
206+
207+
@CFUNCTYPE(ctypes.c_ssize_t, c_void_p, POINTER(ctypes.c_ubyte), ctypes.c_size_t)
208+
def custom_call(idx, ptr, size): # type: ignore
209+
try:
210+
ty = ctypes.c_uint8 * size
211+
arg = bytes(ty.from_address(ctypes.addressof(ptr.contents)))
212+
ret = CUSTOM_OUTPUTS.get(idx or 0)(arg)
213+
if ret is None:
214+
return size
215+
return ret
216+
except Exception as e:
217+
print('failed custom output, required to catch exception:', e)
218+
return -errno.EIO
219+
220+
221+
@CFUNCTYPE(None, c_void_p)
222+
def custom_finalize(idx): # type: ignore
223+
if CUSTOM_OUTPUTS:
224+
CUSTOM_OUTPUTS.deallocate(idx or 0)
225+
return None

0 commit comments

Comments
 (0)