spin_factor_wasi/
lib.rs

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/// Helper trait to extend `InitContext` with some more `link_*_bindings`
71/// methods related to `wasmtime-wasi` and `wasmtime-wasi-io`-specific
72/// signatures.
73#[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        // Mount files
336        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        // Apply environment variables
343        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    /// Sets the WASI `stdin` descriptor to the given [`StdinStream`].
401    pub fn stdin(&mut self, stdin: impl StdinStream + 'static) {
402        self.ctx.stdin(stdin);
403    }
404
405    /// Sets the WASI `stdin` descriptor to the given [`Read`]er.
406    pub fn stdin_pipe(&mut self, r: impl Read + Send + Sync + Unpin + 'static) {
407        self.stdin(PipeReadStream::new(r));
408    }
409
410    /// Sets the WASI `stdout` descriptor to the given [`StdoutStream`].
411    pub fn stdout(&mut self, stdout: impl StdoutStream + 'static) {
412        self.ctx.stdout(stdout);
413    }
414
415    /// Sets the WASI `stdout` descriptor to the given [`Write`]r.
416    pub fn stdout_pipe(&mut self, w: impl Write + Send + Sync + Unpin + 'static) {
417        self.stdout(PipedWriteStream::new(w));
418    }
419
420    /// Sets the WASI `stderr` descriptor to the given [`StdoutStream`].
421    pub fn stderr(&mut self, stderr: impl StdoutStream + 'static) {
422        self.ctx.stderr(stderr);
423    }
424
425    /// Sets the WASI `stderr` descriptor to the given [`Write`]r.
426    pub fn stderr_pipe(&mut self, w: impl Write + Send + Sync + Unpin + 'static) {
427        self.stderr(PipedWriteStream::new(w));
428    }
429
430    /// Appends the given strings to the WASI 'args'.
431    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    /// Sets the given key/value string entries on the WASI 'env'.
438    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    /// "Mounts" the given `host_path` into the WASI filesystem at the given
445    /// `guest_path`.
446    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}