Coverage for wasmtime/_wasi.py: 92%
106 statements
« prev ^ index » next coverage.py v7.11.3, created at 2025-12-01 19:40 +0000
« prev ^ index » next coverage.py v7.11.3, created at 2025-12-01 19:40 +0000
1import ctypes
2import errno
3from ctypes import POINTER, c_char, c_char_p, cast, CFUNCTYPE, c_void_p
4from enum import Enum
5from os import PathLike
6from typing import Iterable, List, Union, Callable
8from wasmtime import Managed, WasmtimeError
10from . import _ffi as ffi
11from ._config import setter_property
12from ._slab import Slab
15def _encode_path(path: Union[str, bytes, PathLike]) -> bytes:
16 if isinstance(path, (bytes, str)):
17 path2 = path
18 else:
19 path2 = path.__fspath__()
20 if isinstance(path2, bytes):
21 return path2
22 return path2.encode('utf8')
24class DirPerms(Enum):
25 READ_ONLY = ffi.wasi_dir_perms_flags.WASMTIME_WASI_DIR_PERMS_READ.value
26 WRITE_ONLY = ffi.wasi_dir_perms_flags.WASMTIME_WASI_DIR_PERMS_WRITE.value
27 READ_WRITE = ffi.wasi_dir_perms_flags.WASMTIME_WASI_DIR_PERMS_READ.value | ffi.wasi_dir_perms_flags.WASMTIME_WASI_DIR_PERMS_WRITE.value
29class FilePerms(Enum):
30 READ_ONLY = ffi.wasi_file_perms_flags.WASMTIME_WASI_FILE_PERMS_READ.value
31 WRITE_ONLY = ffi.wasi_file_perms_flags.WASMTIME_WASI_FILE_PERMS_WRITE.value
32 READ_WRITE = ffi.wasi_file_perms_flags.WASMTIME_WASI_FILE_PERMS_READ.value | ffi.wasi_file_perms_flags.WASMTIME_WASI_FILE_PERMS_WRITE.value
35CustomOutput = Callable[[bytes], Union[int, None]]
36CUSTOM_OUTPUTS: Slab[CustomOutput] = Slab()
39class WasiConfig(Managed["ctypes._Pointer[ffi.wasi_config_t]"]):
41 def __init__(self) -> None:
42 self._set_ptr(ffi.wasi_config_new())
44 def _delete(self, ptr: "ctypes._Pointer[ffi.wasi_config_t]") -> None:
45 ffi.wasi_config_delete(ptr)
47 @setter_property
48 def argv(self, argv: List[str]) -> None:
49 """
50 Explicitly configure the `argv` for this WASI configuration
51 """
52 ptrs = to_char_array(argv)
53 if not ffi.wasi_config_set_argv(self.ptr(), len(argv), ptrs):
54 raise WasmtimeError("failed to configure argv")
56 def inherit_argv(self) -> None:
57 ffi.wasi_config_inherit_argv(self.ptr())
59 @setter_property
60 def env(self, pairs: Iterable[Iterable]) -> None:
61 """
62 Configure environment variables to be returned for this WASI
63 configuration.
65 The `pairs` provided must be an iterable list of key/value pairs of
66 environment variables.
67 """
68 names = []
69 values = []
70 for name, value in pairs:
71 names.append(name)
72 values.append(value)
73 name_ptrs = to_char_array(names)
74 value_ptrs = to_char_array(values)
75 if not ffi.wasi_config_set_env(self.ptr(), len(names), name_ptrs, value_ptrs):
76 raise WasmtimeError("failed to configure environment")
78 def inherit_env(self) -> None:
79 """
80 Configures the environment variables available within WASI to be those
81 in this own process's environment. All environment variables are
82 inherited.
83 """
84 ffi.wasi_config_inherit_env(self.ptr())
86 @setter_property
87 def stdin_file(self, path: Union[str, bytes, PathLike]) -> None:
88 """
89 Configures a file to be used as the stdin stream of this WASI
90 configuration.
92 Reads of the stdin stream will read the path specified.
94 The file must already exist on the filesystem. If it cannot be
95 opened then `WasmtimeError` is raised.
96 """
98 res = ffi.wasi_config_set_stdin_file(
99 self.ptr(), c_char_p(_encode_path(path)))
100 if not res:
101 raise WasmtimeError("failed to set stdin file")
103 def inherit_stdin(self) -> None:
104 """
105 Configures this own process's stdin to be used as the WASI program's
106 stdin.
108 Reads of the stdin stream will read this process's stdin.
109 """
110 ffi.wasi_config_inherit_stdin(self.ptr())
112 @setter_property
113 def stdout_file(self, path: str) -> None:
114 """
115 Configures a file to be used as the stdout stream of this WASI
116 configuration.
118 Writes to stdout will be written to the file specified.
120 The file specified will be created if it doesn't exist, or truncated if
121 it already exists. It must be available to open for writing. If it
122 cannot be opened for writing then `WasmtimeError` is raised.
123 """
124 res = ffi.wasi_config_set_stdout_file(
125 self.ptr(), c_char_p(_encode_path(path)))
126 if not res:
127 raise WasmtimeError("failed to set stdout file")
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)
138 def inherit_stdout(self) -> None:
139 """
140 Configures this own process's stdout to be used as the WASI program's
141 stdout.
143 Writes to stdout stream will write to this process's stdout.
144 """
145 ffi.wasi_config_inherit_stdout(self.ptr())
147 @setter_property
148 def stderr_file(self, path: str) -> None:
149 """
150 Configures a file to be used as the stderr stream of this WASI
151 configuration.
153 Writes to stderr will be written to the file specified.
155 The file specified will be created if it doesn't exist, or truncated if
156 it already exists. It must be available to open for writing. If it
157 cannot be opened for writing then `WasmtimeError` is raised.
158 """
159 res = ffi.wasi_config_set_stderr_file(
160 self.ptr(), c_char_p(_encode_path(path)))
161 if not res:
162 raise WasmtimeError("failed to set stderr file")
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)
173 def inherit_stderr(self) -> None:
174 """
175 Configures this own process's stderr to be used as the WASI program's
176 stderr.
178 Writes to stderr stream will write to this process's stderr.
179 """
180 ffi.wasi_config_inherit_stderr(self.ptr())
182 def preopen_dir(self, path: str, guest_path: str, dir_perms: DirPerms = DirPerms.READ_WRITE, file_perms: FilePerms = FilePerms.READ_WRITE) -> None:
183 """
184 Allows the WASI program to access the directory at `path` using the
185 path `guest_path` within the WASI program.
187 `dir_perms` specifies the permissions that wasm will have to operate on
188 `guest_path`. This can be used, for example, to provide readonly access to a
189 directory.
191 `file_perms` specifies the maximum set of permissions that can be used for
192 any file in this directory.
193 """
194 path_ptr = c_char_p(path.encode('utf-8'))
195 guest_path_ptr = c_char_p(guest_path.encode('utf-8'))
196 if not ffi.wasi_config_preopen_dir(self.ptr(), path_ptr, guest_path_ptr, dir_perms.value, file_perms.value):
197 raise WasmtimeError('failed to add preopen dir')
200def to_char_array(strings: List[str]) -> "ctypes._Pointer[ctypes._Pointer[c_char]]":
201 ptrs = (c_char_p * len(strings))()
202 for i, s in enumerate(strings):
203 ptrs[i] = c_char_p(s.encode('utf-8'))
204 return cast(ptrs, POINTER(POINTER(c_char)))
207@CFUNCTYPE(ctypes.c_ssize_t, c_void_p, POINTER(ctypes.c_ubyte), ctypes.c_size_t)
208def 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
221@CFUNCTYPE(None, c_void_p)
222def custom_finalize(idx): # type: ignore
223 if CUSTOM_OUTPUTS:
224 CUSTOM_OUTPUTS.deallocate(idx or 0)
225 return None