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_wasi::{
19    DirPerms, FilePerms, IoImpl, IoView, ResourceTable, StdinStream, StdoutStream, WasiCtx,
20    WasiCtxBuilder, WasiImpl, WasiView,
21};
22
23pub use wasmtime_wasi::SocketAddrUse;
24
25pub struct WasiFactor {
26    files_mounter: Box<dyn FilesMounter>,
27}
28
29impl WasiFactor {
30    pub fn new(files_mounter: impl FilesMounter + 'static) -> Self {
31        Self {
32            files_mounter: Box::new(files_mounter),
33        }
34    }
35
36    pub fn get_wasi_impl(
37        runtime_instance_state: &mut impl RuntimeFactorsInstanceState,
38    ) -> Option<WasiImpl<impl WasiView + '_>> {
39        let (state, table) = runtime_instance_state.get_with_table::<WasiFactor>()?;
40        Some(WasiImpl(IoImpl(WasiImplInner {
41            ctx: &mut state.ctx,
42            table,
43        })))
44    }
45}
46
47impl Factor for WasiFactor {
48    type RuntimeConfig = ();
49    type AppState = ();
50    type InstanceBuilder = InstanceBuilder;
51
52    fn init<T: Send + 'static>(&mut self, mut ctx: InitContext<T, Self>) -> anyhow::Result<()> {
53        fn type_annotate_wasi<T, F>(f: F) -> F
54        where
55            F: Fn(&mut T) -> WasiImpl<WasiImplInner>,
56        {
57            f
58        }
59        fn type_annotate_io<T, F>(f: F) -> F
60        where
61            F: Fn(&mut T) -> IoImpl<WasiImplInner>,
62        {
63            f
64        }
65        let get_data_with_table = ctx.get_data_with_table_fn();
66        let io_closure = type_annotate_io(move |data| {
67            let (state, table) = get_data_with_table(data);
68            IoImpl(WasiImplInner {
69                ctx: &mut state.ctx,
70                table,
71            })
72        });
73        let wasi_closure = type_annotate_wasi(move |data| WasiImpl(io_closure(data)));
74        let linker = ctx.linker();
75        use wasmtime_wasi::bindings;
76        bindings::clocks::wall_clock::add_to_linker_get_host(linker, wasi_closure)?;
77        bindings::clocks::monotonic_clock::add_to_linker_get_host(linker, wasi_closure)?;
78        bindings::filesystem::types::add_to_linker_get_host(linker, wasi_closure)?;
79        bindings::filesystem::preopens::add_to_linker_get_host(linker, wasi_closure)?;
80        bindings::io::error::add_to_linker_get_host(linker, io_closure)?;
81        bindings::io::poll::add_to_linker_get_host(linker, io_closure)?;
82        bindings::io::streams::add_to_linker_get_host(linker, io_closure)?;
83        bindings::random::random::add_to_linker_get_host(linker, wasi_closure)?;
84        bindings::random::insecure::add_to_linker_get_host(linker, wasi_closure)?;
85        bindings::random::insecure_seed::add_to_linker_get_host(linker, wasi_closure)?;
86        bindings::cli::exit::add_to_linker_get_host(linker, &Default::default(), wasi_closure)?;
87        bindings::cli::environment::add_to_linker_get_host(linker, wasi_closure)?;
88        bindings::cli::stdin::add_to_linker_get_host(linker, wasi_closure)?;
89        bindings::cli::stdout::add_to_linker_get_host(linker, wasi_closure)?;
90        bindings::cli::stderr::add_to_linker_get_host(linker, wasi_closure)?;
91        bindings::cli::terminal_input::add_to_linker_get_host(linker, wasi_closure)?;
92        bindings::cli::terminal_output::add_to_linker_get_host(linker, wasi_closure)?;
93        bindings::cli::terminal_stdin::add_to_linker_get_host(linker, wasi_closure)?;
94        bindings::cli::terminal_stdout::add_to_linker_get_host(linker, wasi_closure)?;
95        bindings::cli::terminal_stderr::add_to_linker_get_host(linker, wasi_closure)?;
96        bindings::sockets::tcp::add_to_linker_get_host(linker, wasi_closure)?;
97        bindings::sockets::tcp_create_socket::add_to_linker_get_host(linker, wasi_closure)?;
98        bindings::sockets::udp::add_to_linker_get_host(linker, wasi_closure)?;
99        bindings::sockets::udp_create_socket::add_to_linker_get_host(linker, wasi_closure)?;
100        bindings::sockets::instance_network::add_to_linker_get_host(linker, wasi_closure)?;
101        bindings::sockets::network::add_to_linker_get_host(
102            linker,
103            &Default::default(),
104            wasi_closure,
105        )?;
106        bindings::sockets::ip_name_lookup::add_to_linker_get_host(linker, wasi_closure)?;
107
108        wasi_2023_10_18::add_to_linker(linker, wasi_closure)?;
109        wasi_2023_11_10::add_to_linker(linker, wasi_closure)?;
110
111        Ok(())
112    }
113
114    fn configure_app<T: RuntimeFactors>(
115        &self,
116        _ctx: spin_factors::ConfigureAppContext<T, Self>,
117    ) -> anyhow::Result<Self::AppState> {
118        Ok(())
119    }
120
121    fn prepare<T: RuntimeFactors>(
122        &self,
123        ctx: PrepareContext<T, Self>,
124    ) -> anyhow::Result<InstanceBuilder> {
125        let mut wasi_ctx = WasiCtxBuilder::new();
126
127        // Mount files
128        let mount_ctx = MountFilesContext { ctx: &mut wasi_ctx };
129        self.files_mounter
130            .mount_files(ctx.app_component(), mount_ctx)?;
131
132        let mut builder = InstanceBuilder { ctx: wasi_ctx };
133
134        // Apply environment variables
135        builder.env(ctx.app_component().environment());
136
137        Ok(builder)
138    }
139}
140
141pub trait FilesMounter: Send + Sync {
142    fn mount_files(
143        &self,
144        app_component: &AppComponent,
145        ctx: MountFilesContext,
146    ) -> anyhow::Result<()>;
147}
148
149pub struct DummyFilesMounter;
150
151impl FilesMounter for DummyFilesMounter {
152    fn mount_files(
153        &self,
154        app_component: &AppComponent,
155        _ctx: MountFilesContext,
156    ) -> anyhow::Result<()> {
157        anyhow::ensure!(
158            app_component.files().next().is_none(),
159            "DummyFilesMounter can't actually mount files"
160        );
161        Ok(())
162    }
163}
164
165pub struct MountFilesContext<'a> {
166    ctx: &'a mut WasiCtxBuilder,
167}
168
169impl MountFilesContext<'_> {
170    pub fn preopened_dir(
171        &mut self,
172        host_path: impl AsRef<Path>,
173        guest_path: impl AsRef<str>,
174        writable: bool,
175    ) -> anyhow::Result<()> {
176        let (dir_perms, file_perms) = if writable {
177            (DirPerms::all(), FilePerms::all())
178        } else {
179            (DirPerms::READ, FilePerms::READ)
180        };
181        self.ctx
182            .preopened_dir(host_path, guest_path, dir_perms, file_perms)?;
183        Ok(())
184    }
185}
186
187pub struct InstanceBuilder {
188    ctx: WasiCtxBuilder,
189}
190
191impl InstanceBuilder {
192    /// Sets the WASI `stdin` descriptor to the given [`StdinStream`].
193    pub fn stdin(&mut self, stdin: impl StdinStream + 'static) {
194        self.ctx.stdin(stdin);
195    }
196
197    /// Sets the WASI `stdin` descriptor to the given [`Read`]er.
198    pub fn stdin_pipe(&mut self, r: impl Read + Send + Sync + Unpin + 'static) {
199        self.stdin(PipeReadStream::new(r));
200    }
201
202    /// Sets the WASI `stdout` descriptor to the given [`StdoutStream`].
203    pub fn stdout(&mut self, stdout: impl StdoutStream + 'static) {
204        self.ctx.stdout(stdout);
205    }
206
207    /// Sets the WASI `stdout` descriptor to the given [`Write`]r.
208    pub fn stdout_pipe(&mut self, w: impl Write + Send + Sync + Unpin + 'static) {
209        self.stdout(PipedWriteStream::new(w));
210    }
211
212    /// Sets the WASI `stderr` descriptor to the given [`StdoutStream`].
213    pub fn stderr(&mut self, stderr: impl StdoutStream + 'static) {
214        self.ctx.stderr(stderr);
215    }
216
217    /// Sets the WASI `stderr` descriptor to the given [`Write`]r.
218    pub fn stderr_pipe(&mut self, w: impl Write + Send + Sync + Unpin + 'static) {
219        self.stderr(PipedWriteStream::new(w));
220    }
221
222    /// Appends the given strings to the WASI 'args'.
223    pub fn args(&mut self, args: impl IntoIterator<Item = impl AsRef<str>>) {
224        for arg in args {
225            self.ctx.arg(arg);
226        }
227    }
228
229    /// Sets the given key/value string entries on the WASI 'env'.
230    pub fn env(&mut self, vars: impl IntoIterator<Item = (impl AsRef<str>, impl AsRef<str>)>) {
231        for (k, v) in vars {
232            self.ctx.env(k, v);
233        }
234    }
235
236    /// "Mounts" the given `host_path` into the WASI filesystem at the given
237    /// `guest_path`.
238    pub fn preopened_dir(
239        &mut self,
240        host_path: impl AsRef<Path>,
241        guest_path: impl AsRef<str>,
242        writable: bool,
243    ) -> anyhow::Result<()> {
244        let (dir_perms, file_perms) = if writable {
245            (DirPerms::all(), FilePerms::all())
246        } else {
247            (DirPerms::READ, FilePerms::READ)
248        };
249        self.ctx
250            .preopened_dir(host_path, guest_path, dir_perms, file_perms)?;
251        Ok(())
252    }
253}
254
255impl FactorInstanceBuilder for InstanceBuilder {
256    type InstanceState = InstanceState;
257
258    fn build(self) -> anyhow::Result<Self::InstanceState> {
259        let InstanceBuilder { ctx: mut wasi_ctx } = self;
260        Ok(InstanceState {
261            ctx: wasi_ctx.build(),
262        })
263    }
264}
265
266impl InstanceBuilder {
267    pub fn outbound_socket_addr_check<F, Fut>(&mut self, check: F)
268    where
269        F: Fn(SocketAddr, SocketAddrUse) -> Fut + Send + Sync + Clone + 'static,
270        Fut: Future<Output = bool> + Send + Sync,
271    {
272        self.ctx.socket_addr_check(move |addr, addr_use| {
273            let check = check.clone();
274            Box::pin(async move {
275                match addr_use {
276                    wasmtime_wasi::SocketAddrUse::TcpBind => false,
277                    wasmtime_wasi::SocketAddrUse::TcpConnect
278                    | wasmtime_wasi::SocketAddrUse::UdpBind
279                    | wasmtime_wasi::SocketAddrUse::UdpConnect
280                    | wasmtime_wasi::SocketAddrUse::UdpOutgoingDatagram => {
281                        check(addr, addr_use).await
282                    }
283                }
284            })
285        });
286    }
287}
288
289pub struct InstanceState {
290    ctx: WasiCtx,
291}
292
293struct WasiImplInner<'a> {
294    ctx: &'a mut WasiCtx,
295    table: &'a mut ResourceTable,
296}
297
298impl WasiView for WasiImplInner<'_> {
299    fn ctx(&mut self) -> &mut WasiCtx {
300        self.ctx
301    }
302}
303
304impl IoView for WasiImplInner<'_> {
305    fn table(&mut self) -> &mut ResourceTable {
306        self.table
307    }
308}