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#[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 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 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 pub fn stdin(&mut self, stdin: impl StdinStream + 'static) {
222 self.ctx.stdin(stdin);
223 }
224
225 pub fn stdin_pipe(&mut self, r: impl Read + Send + Sync + Unpin + 'static) {
227 self.stdin(PipeReadStream::new(r));
228 }
229
230 pub fn stdout(&mut self, stdout: impl StdoutStream + 'static) {
232 self.ctx.stdout(stdout);
233 }
234
235 pub fn stdout_pipe(&mut self, w: impl Write + Send + Sync + Unpin + 'static) {
237 self.stdout(PipedWriteStream::new(w));
238 }
239
240 pub fn stderr(&mut self, stderr: impl StdoutStream + 'static) {
242 self.ctx.stderr(stderr);
243 }
244
245 pub fn stderr_pipe(&mut self, w: impl Write + Send + Sync + Unpin + 'static) {
247 self.stderr(PipedWriteStream::new(w));
248 }
249
250 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 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 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}