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