Coverage for wasmtime/_wasi.py: 92%

98 statements  

« prev     ^ index     » next       coverage.py v7.11.3, created at 2026-08-20 19:29 +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 

7 

8from wasmtime import Managed, WasmtimeError 

9 

10from . import _ffi as ffi 

11from ._config import setter_property 

12from ._slab import Slab 

13 

14 

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') 

23 

24 

25CustomOutput = Callable[[bytes], Union[int, None]] 

26CUSTOM_OUTPUTS: Slab[CustomOutput] = Slab() 

27 

28 

29class WasiConfig(Managed["ctypes._Pointer[ffi.wasi_config_t]"]): 

30 

31 def __init__(self) -> None: 

32 self._set_ptr(ffi.wasi_config_new()) 

33 

34 def _delete(self, ptr: "ctypes._Pointer[ffi.wasi_config_t]") -> None: 

35 ffi.wasi_config_delete(ptr) 

36 

37 @setter_property 

38 def argv(self, argv: List[str]) -> None: 

39 """ 

40 Explicitly configure the `argv` for this WASI configuration 

41 """ 

42 ptrs = to_char_array(argv) 

43 if not ffi.wasi_config_set_argv(self.ptr(), len(argv), ptrs): 

44 raise WasmtimeError("failed to configure argv") 

45 

46 def inherit_argv(self) -> None: 

47 ffi.wasi_config_inherit_argv(self.ptr()) 

48 

49 @setter_property 

50 def env(self, pairs: Iterable[Iterable]) -> None: 

51 """ 

52 Configure environment variables to be returned for this WASI 

53 configuration. 

54 

55 The `pairs` provided must be an iterable list of key/value pairs of 

56 environment variables. 

57 """ 

58 names = [] 

59 values = [] 

60 for name, value in pairs: 

61 names.append(name) 

62 values.append(value) 

63 name_ptrs = to_char_array(names) 

64 value_ptrs = to_char_array(values) 

65 if not ffi.wasi_config_set_env(self.ptr(), len(names), name_ptrs, value_ptrs): 

66 raise WasmtimeError("failed to configure environment") 

67 

68 def inherit_env(self) -> None: 

69 """ 

70 Configures the environment variables available within WASI to be those 

71 in this own process's environment. All environment variables are 

72 inherited. 

73 """ 

74 ffi.wasi_config_inherit_env(self.ptr()) 

75 

76 @setter_property 

77 def stdin_file(self, path: Union[str, bytes, PathLike]) -> None: 

78 """ 

79 Configures a file to be used as the stdin stream of this WASI 

80 configuration. 

81 

82 Reads of the stdin stream will read the path specified. 

83 

84 The file must already exist on the filesystem. If it cannot be 

85 opened then `WasmtimeError` is raised. 

86 """ 

87 

88 res = ffi.wasi_config_set_stdin_file( 

89 self.ptr(), c_char_p(_encode_path(path))) 

90 if not res: 

91 raise WasmtimeError("failed to set stdin file") 

92 

93 def inherit_stdin(self) -> None: 

94 """ 

95 Configures this own process's stdin to be used as the WASI program's 

96 stdin. 

97 

98 Reads of the stdin stream will read this process's stdin. 

99 """ 

100 ffi.wasi_config_inherit_stdin(self.ptr()) 

101 

102 @setter_property 

103 def stdout_file(self, path: str) -> None: 

104 """ 

105 Configures a file to be used as the stdout stream of this WASI 

106 configuration. 

107 

108 Writes to stdout will be written to the file specified. 

109 

110 The file specified will be created if it doesn't exist, or truncated if 

111 it already exists. It must be available to open for writing. If it 

112 cannot be opened for writing then `WasmtimeError` is raised. 

113 """ 

114 res = ffi.wasi_config_set_stdout_file( 

115 self.ptr(), c_char_p(_encode_path(path))) 

116 if not res: 

117 raise WasmtimeError("failed to set stdout file") 

118 

119 @setter_property 

120 def stdout_custom(self, callback: CustomOutput) -> None: 

121 """ 

122 Sets a custom `callback` that is invoked whenever stdout is written to. 

123 """ 

124 ffi.wasi_config_set_stdout_custom( 

125 self.ptr(), custom_call, 

126 CUSTOM_OUTPUTS.allocate(callback), custom_finalize) 

127 

128 def inherit_stdout(self) -> None: 

129 """ 

130 Configures this own process's stdout to be used as the WASI program's 

131 stdout. 

132 

133 Writes to stdout stream will write to this process's stdout. 

134 """ 

135 ffi.wasi_config_inherit_stdout(self.ptr()) 

136 

137 @setter_property 

138 def stderr_file(self, path: str) -> None: 

139 """ 

140 Configures a file to be used as the stderr stream of this WASI 

141 configuration. 

142 

143 Writes to stderr will be written to the file specified. 

144 

145 The file specified will be created if it doesn't exist, or truncated if 

146 it already exists. It must be available to open for writing. If it 

147 cannot be opened for writing then `WasmtimeError` is raised. 

148 """ 

149 res = ffi.wasi_config_set_stderr_file( 

150 self.ptr(), c_char_p(_encode_path(path))) 

151 if not res: 

152 raise WasmtimeError("failed to set stderr file") 

153 

154 @setter_property 

155 def stderr_custom(self, callback: CustomOutput) -> None: 

156 """ 

157 Sets a custom `callback` that is invoked whenever stderr is written to. 

158 """ 

159 ffi.wasi_config_set_stderr_custom( 

160 self.ptr(), custom_call, 

161 CUSTOM_OUTPUTS.allocate(callback), custom_finalize) 

162 

163 def inherit_stderr(self) -> None: 

164 """ 

165 Configures this own process's stderr to be used as the WASI program's 

166 stderr. 

167 

168 Writes to stderr stream will write to this process's stderr. 

169 """ 

170 ffi.wasi_config_inherit_stderr(self.ptr()) 

171 

172 def preopen_dir(self, path: str, guest_path: str, fs_mutable: bool = True) -> None: 

173 """ 

174 Allows the WASI program to access the directory at `path` using the 

175 path `guest_path` within the WASI program. 

176 

177 `dir_perms` specifies the permissions that wasm will have to operate on 

178 `guest_path`. This can be used, for example, to provide readonly access to a 

179 directory. 

180 

181 `file_perms` specifies the maximum set of permissions that can be used for 

182 any file in this directory. 

183 """ 

184 path_ptr = c_char_p(path.encode('utf-8')) 

185 guest_path_ptr = c_char_p(guest_path.encode('utf-8')) 

186 if not ffi.wasi_config_preopen_dir(self.ptr(), path_ptr, guest_path_ptr, fs_mutable): 

187 raise WasmtimeError('failed to add preopen dir') 

188 

189 

190def to_char_array(strings: List[str]) -> "ctypes._Pointer[ctypes._Pointer[c_char]]": 

191 ptrs = (c_char_p * len(strings))() 

192 for i, s in enumerate(strings): 

193 ptrs[i] = c_char_p(s.encode('utf-8')) 

194 return cast(ptrs, POINTER(POINTER(c_char))) 

195 

196 

197@CFUNCTYPE(ctypes.c_ssize_t, c_void_p, POINTER(ctypes.c_ubyte), ctypes.c_size_t) 

198def custom_call(idx, ptr, size): # type: ignore 

199 try: 

200 ty = ctypes.c_uint8 * size 

201 arg = bytes(ty.from_address(ctypes.addressof(ptr.contents))) 

202 ret = CUSTOM_OUTPUTS.get(idx or 0)(arg) 

203 if ret is None: 

204 return size 

205 return ret 

206 except Exception as e: 

207 print('failed custom output, required to catch exception:', e) 

208 return -errno.EIO 

209 

210 

211@CFUNCTYPE(None, c_void_p) 

212def custom_finalize(idx): # type: ignore 

213 if CUSTOM_OUTPUTS: 

214 CUSTOM_OUTPUTS.deallocate(idx or 0) 

215 return None