1mod io;
2pub mod spin;
3mod wasi_2023_10_18;
4mod wasi_2023_11_10;
5
6use std::{
7 future::Future,
8 io::{Read, Write},
9 net::SocketAddr,
10 path::Path,
11};
12
13use io::{PipeReadStream, PipedWriteStream};
14use spin_factors::{
15 anyhow, AppComponent, Factor, FactorInstanceBuilder, InitContext, PrepareContext,
16 RuntimeFactors, RuntimeFactorsInstanceState,
17};
18use wasmtime::component::HasData;
19use wasmtime_wasi::cli::{StdinStream, StdoutStream, WasiCli, WasiCliCtxView};
20use wasmtime_wasi::clocks::{WasiClocks, WasiClocksCtxView};
21use wasmtime_wasi::filesystem::{WasiFilesystem, WasiFilesystemCtxView};
22use wasmtime_wasi::random::{WasiRandom, WasiRandomCtx};
23use wasmtime_wasi::sockets::{WasiSockets, WasiSocketsCtxView};
24use wasmtime_wasi::{DirPerms, FilePerms, ResourceTable, WasiCtx, WasiCtxBuilder, WasiCtxView};
25
26pub use wasmtime_wasi::sockets::SocketAddrUse;
27
28pub struct WasiFactor {
29 files_mounter: Box<dyn FilesMounter>,
30}
31
32impl WasiFactor {
33 pub fn new(files_mounter: impl FilesMounter + 'static) -> Self {
34 Self {
35 files_mounter: Box::new(files_mounter),
36 }
37 }
38
39 pub fn get_wasi_impl(
40 runtime_instance_state: &mut impl RuntimeFactorsInstanceState,
41 ) -> Option<WasiCtxView<'_>> {
42 let (state, table) = runtime_instance_state.get_with_table::<WasiFactor>()?;
43 Some(WasiCtxView {
44 ctx: &mut state.ctx,
45 table,
46 })
47 }
48
49 pub fn get_cli_impl(
50 runtime_instance_state: &mut impl RuntimeFactorsInstanceState,
51 ) -> Option<WasiCliCtxView<'_>> {
52 let (state, table) = runtime_instance_state.get_with_table::<WasiFactor>()?;
53 Some(WasiCliCtxView {
54 ctx: state.ctx.cli(),
55 table,
56 })
57 }
58
59 pub fn get_sockets_impl(
60 runtime_instance_state: &mut impl RuntimeFactorsInstanceState,
61 ) -> Option<WasiSocketsCtxView<'_>> {
62 let (state, table) = runtime_instance_state.get_with_table::<WasiFactor>()?;
63 Some(WasiSocketsCtxView {
64 ctx: state.ctx.sockets(),
65 table,
66 })
67 }
68}
69
70#[allow(clippy::type_complexity, reason = "sorry, blame alex")]
74trait InitContextExt: InitContext<WasiFactor> {
75 fn get_table(data: &mut Self::StoreData) -> &mut ResourceTable {
76 let (_state, table) = Self::get_data_with_table(data);
77 table
78 }
79
80 fn get_clocks(data: &mut Self::StoreData) -> WasiClocksCtxView<'_> {
81 let (state, table) = Self::get_data_with_table(data);
82 WasiClocksCtxView {
83 ctx: state.ctx.clocks(),
84 table,
85 }
86 }
87
88 fn get_random(data: &mut Self::StoreData) -> &mut WasiRandomCtx {
89 let (state, _) = Self::get_data_with_table(data);
90 state.ctx.random()
91 }
92
93 fn link_clocks_bindings(
94 &mut self,
95 add_to_linker: fn(
96 &mut wasmtime::component::Linker<Self::StoreData>,
97 fn(&mut Self::StoreData) -> WasiClocksCtxView<'_>,
98 ) -> anyhow::Result<()>,
99 ) -> anyhow::Result<()> {
100 add_to_linker(self.linker(), Self::get_clocks)
101 }
102
103 fn get_cli(data: &mut Self::StoreData) -> WasiCliCtxView<'_> {
104 let (state, table) = Self::get_data_with_table(data);
105 WasiCliCtxView {
106 ctx: state.ctx.cli(),
107 table,
108 }
109 }
110
111 fn link_cli_bindings(
112 &mut self,
113 add_to_linker: fn(
114 &mut wasmtime::component::Linker<Self::StoreData>,
115 fn(&mut Self::StoreData) -> WasiCliCtxView<'_>,
116 ) -> anyhow::Result<()>,
117 ) -> anyhow::Result<()> {
118 add_to_linker(self.linker(), Self::get_cli)
119 }
120
121 fn link_cli_default_bindings<O: Default>(
122 &mut self,
123 add_to_linker: fn(
124 &mut wasmtime::component::Linker<Self::StoreData>,
125 &O,
126 fn(&mut Self::StoreData) -> WasiCliCtxView<'_>,
127 ) -> anyhow::Result<()>,
128 ) -> anyhow::Result<()> {
129 add_to_linker(self.linker(), &O::default(), Self::get_cli)
130 }
131
132 fn get_filesystem(data: &mut Self::StoreData) -> WasiFilesystemCtxView<'_> {
133 let (state, table) = Self::get_data_with_table(data);
134 WasiFilesystemCtxView {
135 ctx: state.ctx.filesystem(),
136 table,
137 }
138 }
139
140 fn link_filesystem_bindings(
141 &mut self,
142 add_to_linker: fn(
143 &mut wasmtime::component::Linker<Self::StoreData>,
144 fn(&mut Self::StoreData) -> WasiFilesystemCtxView<'_>,
145 ) -> anyhow::Result<()>,
146 ) -> anyhow::Result<()> {
147 add_to_linker(self.linker(), Self::get_filesystem)
148 }
149
150 fn get_sockets(data: &mut Self::StoreData) -> WasiSocketsCtxView<'_> {
151 let (state, table) = Self::get_data_with_table(data);
152 WasiSocketsCtxView {
153 ctx: state.ctx.sockets(),
154 table,
155 }
156 }
157
158 fn link_sockets_bindings(
159 &mut self,
160 add_to_linker: fn(
161 &mut wasmtime::component::Linker<Self::StoreData>,
162 fn(&mut Self::StoreData) -> WasiSocketsCtxView<'_>,
163 ) -> anyhow::Result<()>,
164 ) -> anyhow::Result<()> {
165 add_to_linker(self.linker(), Self::get_sockets)
166 }
167
168 fn link_sockets_default_bindings<O: Default>(
169 &mut self,
170 add_to_linker: fn(
171 &mut wasmtime::component::Linker<Self::StoreData>,
172 &O,
173 fn(&mut Self::StoreData) -> WasiSocketsCtxView<'_>,
174 ) -> anyhow::Result<()>,
175 ) -> anyhow::Result<()> {
176 add_to_linker(self.linker(), &O::default(), Self::get_sockets)
177 }
178
179 fn link_io_bindings(
180 &mut self,
181 add_to_linker: fn(
182 &mut wasmtime::component::Linker<Self::StoreData>,
183 fn(&mut Self::StoreData) -> &mut ResourceTable,
184 ) -> anyhow::Result<()>,
185 ) -> anyhow::Result<()> {
186 add_to_linker(self.linker(), Self::get_table)
187 }
188
189 fn link_random_bindings(
190 &mut self,
191 add_to_linker: fn(
192 &mut wasmtime::component::Linker<Self::StoreData>,
193 fn(&mut Self::StoreData) -> &mut WasiRandomCtx,
194 ) -> anyhow::Result<()>,
195 ) -> anyhow::Result<()> {
196 add_to_linker(self.linker(), |data| {
197 let (state, _table) = Self::get_data_with_table(data);
198 state.ctx.random()
199 })
200 }
201
202 fn link_all_bindings(
203 &mut self,
204 add_to_linker: fn(
205 &mut wasmtime::component::Linker<Self::StoreData>,
206 fn(&mut Self::StoreData) -> &mut ResourceTable,
207 fn(&mut Self::StoreData) -> &mut WasiRandomCtx,
208 fn(&mut Self::StoreData) -> WasiClocksCtxView<'_>,
209 fn(&mut Self::StoreData) -> WasiCliCtxView<'_>,
210 fn(&mut Self::StoreData) -> WasiFilesystemCtxView<'_>,
211 fn(&mut Self::StoreData) -> WasiSocketsCtxView<'_>,
212 ) -> anyhow::Result<()>,
213 ) -> anyhow::Result<()> {
214 add_to_linker(
215 self.linker(),
216 Self::get_table,
217 Self::get_random,
218 Self::get_clocks,
219 Self::get_cli,
220 Self::get_filesystem,
221 Self::get_sockets,
222 )
223 }
224}
225
226impl<T> InitContextExt for T where T: InitContext<WasiFactor> {}
227
228struct HasIo;
229
230impl HasData for HasIo {
231 type Data<'a> = &'a mut ResourceTable;
232}
233
234impl Factor for WasiFactor {
235 type RuntimeConfig = ();
236 type AppState = ();
237 type InstanceBuilder = InstanceBuilder;
238
239 fn init(&mut self, ctx: &mut impl InitContext<Self>) -> anyhow::Result<()> {
240 use wasmtime_wasi::{p2, p3};
241
242 ctx.link_clocks_bindings(p2::bindings::clocks::wall_clock::add_to_linker::<_, WasiClocks>)?;
243 ctx.link_clocks_bindings(p3::bindings::clocks::wall_clock::add_to_linker::<_, WasiClocks>)?;
244 ctx.link_clocks_bindings(
245 p2::bindings::clocks::monotonic_clock::add_to_linker::<_, WasiClocks>,
246 )?;
247 ctx.link_clocks_bindings(
248 p3::bindings::clocks::monotonic_clock::add_to_linker::<_, WasiClocks>,
249 )?;
250 ctx.link_filesystem_bindings(
251 p2::bindings::filesystem::types::add_to_linker::<_, WasiFilesystem>,
252 )?;
253 ctx.link_filesystem_bindings(
254 p3::bindings::filesystem::types::add_to_linker::<_, WasiFilesystem>,
255 )?;
256 ctx.link_filesystem_bindings(
257 p2::bindings::filesystem::preopens::add_to_linker::<_, WasiFilesystem>,
258 )?;
259 ctx.link_filesystem_bindings(
260 p3::bindings::filesystem::preopens::add_to_linker::<_, WasiFilesystem>,
261 )?;
262 ctx.link_io_bindings(p2::bindings::io::error::add_to_linker::<_, HasIo>)?;
263 ctx.link_io_bindings(p2::bindings::io::poll::add_to_linker::<_, HasIo>)?;
264 ctx.link_io_bindings(p2::bindings::io::streams::add_to_linker::<_, HasIo>)?;
265 ctx.link_random_bindings(p2::bindings::random::random::add_to_linker::<_, WasiRandom>)?;
266 ctx.link_random_bindings(p3::bindings::random::random::add_to_linker::<_, WasiRandom>)?;
267 ctx.link_random_bindings(p2::bindings::random::insecure::add_to_linker::<_, WasiRandom>)?;
268 ctx.link_random_bindings(p3::bindings::random::insecure::add_to_linker::<_, WasiRandom>)?;
269 ctx.link_random_bindings(
270 p2::bindings::random::insecure_seed::add_to_linker::<_, WasiRandom>,
271 )?;
272 ctx.link_random_bindings(
273 p3::bindings::random::insecure_seed::add_to_linker::<_, WasiRandom>,
274 )?;
275 ctx.link_cli_default_bindings(p2::bindings::cli::exit::add_to_linker::<_, WasiCli>)?;
276 ctx.link_cli_default_bindings(p3::bindings::cli::exit::add_to_linker::<_, WasiCli>)?;
277 ctx.link_cli_bindings(p2::bindings::cli::environment::add_to_linker::<_, WasiCli>)?;
278 ctx.link_cli_bindings(p3::bindings::cli::environment::add_to_linker::<_, WasiCli>)?;
279 ctx.link_cli_bindings(p2::bindings::cli::stdin::add_to_linker::<_, WasiCli>)?;
280 ctx.link_cli_bindings(p3::bindings::cli::stdin::add_to_linker::<_, WasiCli>)?;
281 ctx.link_cli_bindings(p2::bindings::cli::stdout::add_to_linker::<_, WasiCli>)?;
282 ctx.link_cli_bindings(p3::bindings::cli::stdout::add_to_linker::<_, WasiCli>)?;
283 ctx.link_cli_bindings(p2::bindings::cli::stderr::add_to_linker::<_, WasiCli>)?;
284 ctx.link_cli_bindings(p3::bindings::cli::stderr::add_to_linker::<_, WasiCli>)?;
285 ctx.link_cli_bindings(p2::bindings::cli::terminal_input::add_to_linker::<_, WasiCli>)?;
286 ctx.link_cli_bindings(p3::bindings::cli::terminal_input::add_to_linker::<_, WasiCli>)?;
287 ctx.link_cli_bindings(p2::bindings::cli::terminal_output::add_to_linker::<_, WasiCli>)?;
288 ctx.link_cli_bindings(p3::bindings::cli::terminal_output::add_to_linker::<_, WasiCli>)?;
289 ctx.link_cli_bindings(p2::bindings::cli::terminal_stdin::add_to_linker::<_, WasiCli>)?;
290 ctx.link_cli_bindings(p3::bindings::cli::terminal_stdin::add_to_linker::<_, WasiCli>)?;
291 ctx.link_cli_bindings(p2::bindings::cli::terminal_stdout::add_to_linker::<_, WasiCli>)?;
292 ctx.link_cli_bindings(p3::bindings::cli::terminal_stdout::add_to_linker::<_, WasiCli>)?;
293 ctx.link_cli_bindings(p2::bindings::cli::terminal_stderr::add_to_linker::<_, WasiCli>)?;
294 ctx.link_cli_bindings(p3::bindings::cli::terminal_stderr::add_to_linker::<_, WasiCli>)?;
295 ctx.link_sockets_bindings(p2::bindings::sockets::tcp::add_to_linker::<_, WasiSockets>)?;
296 ctx.link_sockets_bindings(
297 p2::bindings::sockets::tcp_create_socket::add_to_linker::<_, WasiSockets>,
298 )?;
299 ctx.link_sockets_bindings(p2::bindings::sockets::udp::add_to_linker::<_, WasiSockets>)?;
300 ctx.link_sockets_bindings(
301 p2::bindings::sockets::udp_create_socket::add_to_linker::<_, WasiSockets>,
302 )?;
303 ctx.link_sockets_bindings(
304 p2::bindings::sockets::instance_network::add_to_linker::<_, WasiSockets>,
305 )?;
306 ctx.link_sockets_default_bindings(
307 p2::bindings::sockets::network::add_to_linker::<_, WasiSockets>,
308 )?;
309 ctx.link_sockets_bindings(
310 p2::bindings::sockets::ip_name_lookup::add_to_linker::<_, WasiSockets>,
311 )?;
312 ctx.link_sockets_bindings(
313 p3::bindings::sockets::ip_name_lookup::add_to_linker::<_, WasiSockets>,
314 )?;
315 ctx.link_sockets_bindings(p3::bindings::sockets::types::add_to_linker::<_, WasiSockets>)?;
316
317 ctx.link_all_bindings(wasi_2023_10_18::add_to_linker)?;
318 ctx.link_all_bindings(wasi_2023_11_10::add_to_linker)?;
319 Ok(())
320 }
321
322 fn configure_app<T: RuntimeFactors>(
323 &self,
324 _ctx: spin_factors::ConfigureAppContext<T, Self>,
325 ) -> anyhow::Result<Self::AppState> {
326 Ok(())
327 }
328
329 fn prepare<T: RuntimeFactors>(
330 &self,
331 ctx: PrepareContext<T, Self>,
332 ) -> anyhow::Result<InstanceBuilder> {
333 let mut wasi_ctx = WasiCtxBuilder::new();
334
335 let mount_ctx = MountFilesContext { ctx: &mut wasi_ctx };
337 self.files_mounter
338 .mount_files(ctx.app_component(), mount_ctx)?;
339
340 let mut builder = InstanceBuilder { ctx: wasi_ctx };
341
342 builder.env(ctx.app_component().environment());
344
345 Ok(builder)
346 }
347}
348
349pub trait FilesMounter: Send + Sync {
350 fn mount_files(
351 &self,
352 app_component: &AppComponent,
353 ctx: MountFilesContext,
354 ) -> anyhow::Result<()>;
355}
356
357pub struct DummyFilesMounter;
358
359impl FilesMounter for DummyFilesMounter {
360 fn mount_files(
361 &self,
362 app_component: &AppComponent,
363 _ctx: MountFilesContext,
364 ) -> anyhow::Result<()> {
365 anyhow::ensure!(
366 app_component.files().next().is_none(),
367 "DummyFilesMounter can't actually mount files"
368 );
369 Ok(())
370 }
371}
372
373pub struct MountFilesContext<'a> {
374 ctx: &'a mut WasiCtxBuilder,
375}
376
377impl MountFilesContext<'_> {
378 pub fn preopened_dir(
379 &mut self,
380 host_path: impl AsRef<Path>,
381 guest_path: impl AsRef<str>,
382 writable: bool,
383 ) -> anyhow::Result<()> {
384 let (dir_perms, file_perms) = if writable {
385 (DirPerms::all(), FilePerms::all())
386 } else {
387 (DirPerms::READ, FilePerms::READ)
388 };
389 self.ctx
390 .preopened_dir(host_path, guest_path, dir_perms, file_perms)?;
391 Ok(())
392 }
393}
394
395pub struct InstanceBuilder {
396 ctx: WasiCtxBuilder,
397}
398
399impl InstanceBuilder {
400 pub fn stdin(&mut self, stdin: impl StdinStream + 'static) {
402 self.ctx.stdin(stdin);
403 }
404
405 pub fn stdin_pipe(&mut self, r: impl Read + Send + Sync + Unpin + 'static) {
407 self.stdin(PipeReadStream::new(r));
408 }
409
410 pub fn stdout(&mut self, stdout: impl StdoutStream + 'static) {
412 self.ctx.stdout(stdout);
413 }
414
415 pub fn stdout_pipe(&mut self, w: impl Write + Send + Sync + Unpin + 'static) {
417 self.stdout(PipedWriteStream::new(w));
418 }
419
420 pub fn stderr(&mut self, stderr: impl StdoutStream + 'static) {
422 self.ctx.stderr(stderr);
423 }
424
425 pub fn stderr_pipe(&mut self, w: impl Write + Send + Sync + Unpin + 'static) {
427 self.stderr(PipedWriteStream::new(w));
428 }
429
430 pub fn args(&mut self, args: impl IntoIterator<Item = impl AsRef<str>>) {
432 for arg in args {
433 self.ctx.arg(arg);
434 }
435 }
436
437 pub fn env(&mut self, vars: impl IntoIterator<Item = (impl AsRef<str>, impl AsRef<str>)>) {
439 for (k, v) in vars {
440 self.ctx.env(k, v);
441 }
442 }
443
444 pub fn preopened_dir(
447 &mut self,
448 host_path: impl AsRef<Path>,
449 guest_path: impl AsRef<str>,
450 writable: bool,
451 ) -> anyhow::Result<()> {
452 let (dir_perms, file_perms) = if writable {
453 (DirPerms::all(), FilePerms::all())
454 } else {
455 (DirPerms::READ, FilePerms::READ)
456 };
457 self.ctx
458 .preopened_dir(host_path, guest_path, dir_perms, file_perms)?;
459 Ok(())
460 }
461}
462
463impl FactorInstanceBuilder for InstanceBuilder {
464 type InstanceState = InstanceState;
465
466 fn build(self) -> anyhow::Result<Self::InstanceState> {
467 let InstanceBuilder { ctx: mut wasi_ctx } = self;
468 Ok(InstanceState {
469 ctx: wasi_ctx.build(),
470 })
471 }
472}
473
474impl InstanceBuilder {
475 pub fn outbound_socket_addr_check<F, Fut>(&mut self, check: F)
476 where
477 F: Fn(SocketAddr, SocketAddrUse) -> Fut + Send + Sync + Clone + 'static,
478 Fut: Future<Output = bool> + Send + Sync,
479 {
480 self.ctx.socket_addr_check(move |addr, addr_use| {
481 let check = check.clone();
482 Box::pin(async move {
483 match addr_use {
484 SocketAddrUse::TcpBind => false,
485 SocketAddrUse::TcpConnect
486 | SocketAddrUse::UdpBind
487 | SocketAddrUse::UdpConnect
488 | SocketAddrUse::UdpOutgoingDatagram => check(addr, addr_use).await,
489 }
490 })
491 });
492 }
493}
494
495pub struct InstanceState {
496 ctx: WasiCtx,
497}