Skip to main content

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        ) -> wasmtime::Result<()>,
99    ) -> wasmtime::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        ) -> wasmtime::Result<()>,
117    ) -> wasmtime::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        ) -> wasmtime::Result<()>,
128    ) -> wasmtime::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        ) -> wasmtime::Result<()>,
146    ) -> wasmtime::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        ) -> wasmtime::Result<()>,
164    ) -> wasmtime::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        ) -> wasmtime::Result<()>,
175    ) -> wasmtime::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        ) -> wasmtime::Result<()>,
185    ) -> wasmtime::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        ) -> wasmtime::Result<()>,
195    ) -> wasmtime::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(
244            p3::bindings::clocks::system_clock::add_to_linker::<_, WasiClocks>,
245        )?;
246        ctx.link_clocks_bindings(
247            p2::bindings::clocks::monotonic_clock::add_to_linker::<_, WasiClocks>,
248        )?;
249        ctx.link_clocks_bindings(
250            p3::bindings::clocks::monotonic_clock::add_to_linker::<_, WasiClocks>,
251        )?;
252        ctx.link_filesystem_bindings(
253            p2::bindings::filesystem::types::add_to_linker::<_, WasiFilesystem>,
254        )?;
255        ctx.link_filesystem_bindings(
256            p3::bindings::filesystem::types::add_to_linker::<_, WasiFilesystem>,
257        )?;
258        ctx.link_filesystem_bindings(
259            p2::bindings::filesystem::preopens::add_to_linker::<_, WasiFilesystem>,
260        )?;
261        ctx.link_filesystem_bindings(
262            p3::bindings::filesystem::preopens::add_to_linker::<_, WasiFilesystem>,
263        )?;
264        ctx.link_io_bindings(p2::bindings::io::error::add_to_linker::<_, HasIo>)?;
265        ctx.link_io_bindings(p2::bindings::io::poll::add_to_linker::<_, HasIo>)?;
266        ctx.link_io_bindings(p2::bindings::io::streams::add_to_linker::<_, HasIo>)?;
267        ctx.link_random_bindings(p2::bindings::random::random::add_to_linker::<_, WasiRandom>)?;
268        ctx.link_random_bindings(p3::bindings::random::random::add_to_linker::<_, WasiRandom>)?;
269        ctx.link_random_bindings(p2::bindings::random::insecure::add_to_linker::<_, WasiRandom>)?;
270        ctx.link_random_bindings(p3::bindings::random::insecure::add_to_linker::<_, WasiRandom>)?;
271        ctx.link_random_bindings(
272            p2::bindings::random::insecure_seed::add_to_linker::<_, WasiRandom>,
273        )?;
274        ctx.link_random_bindings(
275            p3::bindings::random::insecure_seed::add_to_linker::<_, WasiRandom>,
276        )?;
277        ctx.link_cli_default_bindings(p2::bindings::cli::exit::add_to_linker::<_, WasiCli>)?;
278        ctx.link_cli_default_bindings(p3::bindings::cli::exit::add_to_linker::<_, WasiCli>)?;
279        ctx.link_cli_bindings(p2::bindings::cli::environment::add_to_linker::<_, WasiCli>)?;
280        ctx.link_cli_bindings(p3::bindings::cli::environment::add_to_linker::<_, WasiCli>)?;
281        ctx.link_cli_bindings(p2::bindings::cli::stdin::add_to_linker::<_, WasiCli>)?;
282        ctx.link_cli_bindings(p3::bindings::cli::stdin::add_to_linker::<_, WasiCli>)?;
283        ctx.link_cli_bindings(p2::bindings::cli::stdout::add_to_linker::<_, WasiCli>)?;
284        ctx.link_cli_bindings(p3::bindings::cli::stdout::add_to_linker::<_, WasiCli>)?;
285        ctx.link_cli_bindings(p2::bindings::cli::stderr::add_to_linker::<_, WasiCli>)?;
286        ctx.link_cli_bindings(p3::bindings::cli::stderr::add_to_linker::<_, WasiCli>)?;
287        ctx.link_cli_bindings(p2::bindings::cli::terminal_input::add_to_linker::<_, WasiCli>)?;
288        ctx.link_cli_bindings(p3::bindings::cli::terminal_input::add_to_linker::<_, WasiCli>)?;
289        ctx.link_cli_bindings(p2::bindings::cli::terminal_output::add_to_linker::<_, WasiCli>)?;
290        ctx.link_cli_bindings(p3::bindings::cli::terminal_output::add_to_linker::<_, WasiCli>)?;
291        ctx.link_cli_bindings(p2::bindings::cli::terminal_stdin::add_to_linker::<_, WasiCli>)?;
292        ctx.link_cli_bindings(p3::bindings::cli::terminal_stdin::add_to_linker::<_, WasiCli>)?;
293        ctx.link_cli_bindings(p2::bindings::cli::terminal_stdout::add_to_linker::<_, WasiCli>)?;
294        ctx.link_cli_bindings(p3::bindings::cli::terminal_stdout::add_to_linker::<_, WasiCli>)?;
295        ctx.link_cli_bindings(p2::bindings::cli::terminal_stderr::add_to_linker::<_, WasiCli>)?;
296        ctx.link_cli_bindings(p3::bindings::cli::terminal_stderr::add_to_linker::<_, WasiCli>)?;
297        ctx.link_sockets_bindings(p2::bindings::sockets::tcp::add_to_linker::<_, WasiSockets>)?;
298        ctx.link_sockets_bindings(
299            p2::bindings::sockets::tcp_create_socket::add_to_linker::<_, WasiSockets>,
300        )?;
301        ctx.link_sockets_bindings(p2::bindings::sockets::udp::add_to_linker::<_, WasiSockets>)?;
302        ctx.link_sockets_bindings(
303            p2::bindings::sockets::udp_create_socket::add_to_linker::<_, WasiSockets>,
304        )?;
305        ctx.link_sockets_bindings(
306            p2::bindings::sockets::instance_network::add_to_linker::<_, WasiSockets>,
307        )?;
308        ctx.link_sockets_default_bindings(
309            p2::bindings::sockets::network::add_to_linker::<_, WasiSockets>,
310        )?;
311        ctx.link_sockets_bindings(
312            p2::bindings::sockets::ip_name_lookup::add_to_linker::<_, WasiSockets>,
313        )?;
314        ctx.link_sockets_bindings(
315            p3::bindings::sockets::ip_name_lookup::add_to_linker::<_, WasiSockets>,
316        )?;
317        ctx.link_sockets_bindings(p3::bindings::sockets::types::add_to_linker::<_, WasiSockets>)?;
318
319        ctx.link_all_bindings(wasi_2023_10_18::add_to_linker)?;
320        ctx.link_all_bindings(wasi_2023_11_10::add_to_linker)?;
321        Ok(())
322    }
323
324    fn configure_app<T: RuntimeFactors>(
325        &self,
326        _ctx: spin_factors::ConfigureAppContext<T, Self>,
327    ) -> anyhow::Result<Self::AppState> {
328        Ok(())
329    }
330
331    fn prepare<T: RuntimeFactors>(
332        &self,
333        ctx: PrepareContext<T, Self>,
334    ) -> anyhow::Result<InstanceBuilder> {
335        let mut wasi_ctx = WasiCtxBuilder::new();
336
337        // Mount files
338        let mount_ctx = MountFilesContext { ctx: &mut wasi_ctx };
339        self.files_mounter
340            .mount_files(ctx.app_component(), mount_ctx)?;
341
342        let mut builder = InstanceBuilder { ctx: wasi_ctx };
343
344        // Apply environment variables
345        builder.env(ctx.app_component().environment());
346
347        Ok(builder)
348    }
349}
350
351pub trait FilesMounter: Send + Sync {
352    fn mount_files(
353        &self,
354        app_component: &AppComponent,
355        ctx: MountFilesContext,
356    ) -> anyhow::Result<()>;
357}
358
359pub struct DummyFilesMounter;
360
361impl FilesMounter for DummyFilesMounter {
362    fn mount_files(
363        &self,
364        app_component: &AppComponent,
365        _ctx: MountFilesContext,
366    ) -> anyhow::Result<()> {
367        anyhow::ensure!(
368            app_component.files().next().is_none(),
369            "DummyFilesMounter can't actually mount files"
370        );
371        Ok(())
372    }
373}
374
375pub struct MountFilesContext<'a> {
376    ctx: &'a mut WasiCtxBuilder,
377}
378
379impl MountFilesContext<'_> {
380    pub fn preopened_dir(
381        &mut self,
382        host_path: impl AsRef<Path>,
383        guest_path: impl AsRef<str>,
384        writable: bool,
385    ) -> anyhow::Result<()> {
386        let (dir_perms, file_perms) = if writable {
387            (DirPerms::all(), FilePerms::all())
388        } else {
389            (DirPerms::READ, FilePerms::READ)
390        };
391        self.ctx
392            .preopened_dir(host_path, guest_path, dir_perms, file_perms)?;
393        Ok(())
394    }
395}
396
397pub struct InstanceBuilder {
398    ctx: WasiCtxBuilder,
399}
400
401impl InstanceBuilder {
402    /// Sets the WASI `stdin` descriptor to the given [`StdinStream`].
403    pub fn stdin(&mut self, stdin: impl StdinStream + 'static) {
404        self.ctx.stdin(stdin);
405    }
406
407    /// Sets the WASI `stdin` descriptor to the given [`Read`]er.
408    pub fn stdin_pipe(&mut self, r: impl Read + Send + Sync + Unpin + 'static) {
409        self.stdin(PipeReadStream::new(r));
410    }
411
412    /// Sets the WASI `stdout` descriptor to the given [`StdoutStream`].
413    pub fn stdout(&mut self, stdout: impl StdoutStream + 'static) {
414        self.ctx.stdout(stdout);
415    }
416
417    /// Sets the WASI `stdout` descriptor to the given [`Write`]r.
418    pub fn stdout_pipe(&mut self, w: impl Write + Send + Sync + Unpin + 'static) {
419        self.stdout(PipedWriteStream::new(w));
420    }
421
422    /// Sets the WASI `stderr` descriptor to the given [`StdoutStream`].
423    pub fn stderr(&mut self, stderr: impl StdoutStream + 'static) {
424        self.ctx.stderr(stderr);
425    }
426
427    /// Sets the WASI `stderr` descriptor to the given [`Write`]r.
428    pub fn stderr_pipe(&mut self, w: impl Write + Send + Sync + Unpin + 'static) {
429        self.stderr(PipedWriteStream::new(w));
430    }
431
432    /// Appends the given strings to the WASI 'args'.
433    pub fn args(&mut self, args: impl IntoIterator<Item = impl AsRef<str>>) {
434        for arg in args {
435            self.ctx.arg(arg);
436        }
437    }
438
439    /// Sets the given key/value string entries on the WASI 'env'.
440    pub fn env(&mut self, vars: impl IntoIterator<Item = (impl AsRef<str>, impl AsRef<str>)>) {
441        for (k, v) in vars {
442            self.ctx.env(k, v);
443        }
444    }
445
446    /// "Mounts" the given `host_path` into the WASI filesystem at the given
447    /// `guest_path`.
448    pub fn preopened_dir(
449        &mut self,
450        host_path: impl AsRef<Path>,
451        guest_path: impl AsRef<str>,
452        writable: bool,
453    ) -> anyhow::Result<()> {
454        let (dir_perms, file_perms) = if writable {
455            (DirPerms::all(), FilePerms::all())
456        } else {
457            (DirPerms::READ, FilePerms::READ)
458        };
459        self.ctx
460            .preopened_dir(host_path, guest_path, dir_perms, file_perms)?;
461        Ok(())
462    }
463}
464
465impl FactorInstanceBuilder for InstanceBuilder {
466    type InstanceState = InstanceState;
467
468    fn build(self) -> anyhow::Result<Self::InstanceState> {
469        let InstanceBuilder { ctx: mut wasi_ctx } = self;
470        Ok(InstanceState {
471            ctx: wasi_ctx.build(),
472        })
473    }
474}
475
476impl InstanceBuilder {
477    pub fn outbound_socket_addr_check<F, Fut>(&mut self, check: F)
478    where
479        F: Fn(SocketAddr, SocketAddrUse) -> Fut + Send + Sync + Clone + 'static,
480        Fut: Future<Output = bool> + Send + Sync,
481    {
482        self.ctx.socket_addr_check(move |addr, addr_use| {
483            let check = check.clone();
484            Box::pin(async move {
485                match addr_use {
486                    SocketAddrUse::TcpBind => false,
487                    SocketAddrUse::TcpConnect
488                    | SocketAddrUse::UdpBind
489                    | SocketAddrUse::UdpConnect
490                    | SocketAddrUse::UdpOutgoingDatagram => check(addr, addr_use).await,
491                }
492            })
493        });
494    }
495}
496
497pub struct InstanceState {
498    ctx: WasiCtx,
499}