wasi.rs

  1use std::{
  2    collections::HashMap, fs::File, future::Future, marker::PhantomData, path::Path, pin::Pin,
  3};
  4
  5use anyhow::{anyhow, Error};
  6use serde::{de::DeserializeOwned, Serialize};
  7
  8use wasi_common::{dir, file};
  9use wasmtime::{
 10    AsContext, AsContextMut, Caller, Config, Engine, Extern, Instance, Linker, Module, Store,
 11    StoreContext, StoreContextMut, Trap, TypedFunc, WasmParams,
 12};
 13use wasmtime::{IntoFunc, Memory};
 14use wasmtime_wasi::{Dir, WasiCtx, WasiCtxBuilder};
 15
 16pub struct WasiResource(u32);
 17
 18#[repr(C)]
 19struct WasiBuffer {
 20    ptr: u32,
 21    len: u32,
 22}
 23
 24impl WasiBuffer {
 25    pub fn into_u64(self) -> u64 {
 26        ((self.ptr as u64) << 32) | (self.len as u64)
 27    }
 28
 29    pub fn from_u64(packed: u64) -> Self {
 30        WasiBuffer {
 31            ptr: (packed >> 32) as u32,
 32            len: packed as u32,
 33        }
 34    }
 35}
 36
 37pub struct WasiFn<A: Serialize, R: DeserializeOwned> {
 38    function: TypedFunc<u64, u64>,
 39    _function_type: PhantomData<fn(A) -> R>,
 40}
 41
 42impl<A: Serialize, R: DeserializeOwned> Copy for WasiFn<A, R> {}
 43
 44impl<A: Serialize, R: DeserializeOwned> Clone for WasiFn<A, R> {
 45    fn clone(&self) -> Self {
 46        Self {
 47            function: self.function,
 48            _function_type: PhantomData,
 49        }
 50    }
 51}
 52
 53pub struct WasiPluginBuilder {
 54    wasi_ctx: WasiCtx,
 55    engine: Engine,
 56    linker: Linker<WasiCtxAlloc>,
 57}
 58
 59impl WasiPluginBuilder {
 60    pub fn new(wasi_ctx: WasiCtx) -> Result<Self, Error> {
 61        let mut config = Config::default();
 62        config.async_support(true);
 63        let engine = Engine::new(&config)?;
 64        let linker = Linker::new(&engine);
 65
 66        Ok(WasiPluginBuilder {
 67            // host_functions: HashMap::new(),
 68            wasi_ctx,
 69            engine,
 70            linker,
 71        })
 72    }
 73
 74    pub fn new_with_default_ctx() -> Result<Self, Error> {
 75        let wasi_ctx = WasiCtxBuilder::new()
 76            .inherit_stdin()
 77            .inherit_stderr()
 78            .build();
 79        Self::new(wasi_ctx)
 80    }
 81
 82    pub fn host_function<A: DeserializeOwned + Send, R: Serialize + Send + Sync + Clone>(
 83        mut self,
 84        name: &str,
 85        function: impl Fn(A) -> R + Send + Sync + 'static,
 86    ) -> Result<Self, Error> {
 87        self.linker.func_wrap1_async(
 88            "env",
 89            &format!("__{}", name),
 90            move |mut caller: Caller<'_, WasiCtxAlloc>, packed_buffer: u64| {
 91                // TODO: use try block once avaliable
 92                let result: Result<(Memory, Vec<u8>), Trap> = (|| {
 93                    // grab a handle to the memory
 94                    let mut plugin_memory = match caller.get_export("memory") {
 95                        Some(Extern::Memory(mem)) => mem,
 96                        _ => return Err(Trap::new("Could not grab slice of plugin memory"))?,
 97                    };
 98
 99                    // get the args passed from Guest
100                    let args = Wasi::deserialize_from_buffer(
101                        &mut plugin_memory,
102                        &caller,
103                        WasiBuffer::from_u64(packed_buffer),
104                    )?;
105
106                    // Call the Host-side function
107                    let result: R = function(args);
108
109                    // Serialize the result back to guest
110                    let result = Wasi::serialize(result).map_err(|_| {
111                        Trap::new("Could not serialize value returned from function")
112                    })?;
113                    Ok((plugin_memory, result))
114                })();
115
116                Box::new(async move {
117                    let (mut plugin_memory, result) = result?;
118
119                    let buffer = Wasi::serialize_to_buffer(
120                        caller.data().alloc_buffer(),
121                        &mut plugin_memory,
122                        &mut caller,
123                        result,
124                    )
125                    .await?;
126
127                    Ok(buffer.into_u64())
128                })
129            },
130        )?;
131        Ok(self)
132    }
133
134    pub async fn init<T: AsRef<[u8]>>(self, module: T) -> Result<Wasi, Error> {
135        Wasi::init(module.as_ref().to_vec(), self).await
136    }
137}
138
139// // TODO: remove
140// /// Represents a to-be-initialized plugin.
141// /// Please use [`WasiPluginBuilder`], don't use this directly.
142// pub struct WasiPlugin {
143//     pub module: Vec<u8>,
144//     pub wasi_ctx: WasiCtx,
145//     pub host_functions:
146//         HashMap<String, Box<dyn Fn(&str, &mut Linker<WasiCtx>) -> Result<(), Error>>>,
147// }
148
149#[derive(Copy, Clone)]
150struct WasiAlloc {
151    alloc_buffer: TypedFunc<u32, u32>,
152    free_buffer: TypedFunc<u64, ()>,
153}
154
155struct WasiCtxAlloc {
156    wasi_ctx: WasiCtx,
157    alloc: Option<WasiAlloc>,
158}
159
160impl WasiCtxAlloc {
161    fn alloc_buffer(&self) -> TypedFunc<u32, u32> {
162        self.alloc
163            .expect("allocator has been not initialized, cannot allocate buffer!")
164            .alloc_buffer
165    }
166
167    fn free_buffer(&self) -> TypedFunc<u64, ()> {
168        self.alloc
169            .expect("allocator has been not initialized, cannot free buffer!")
170            .free_buffer
171    }
172
173    fn init_alloc(&mut self, alloc: WasiAlloc) {
174        self.alloc = Some(alloc)
175    }
176}
177
178pub struct Wasi {
179    engine: Engine,
180    module: Module,
181    store: Store<WasiCtxAlloc>,
182    instance: Instance,
183}
184
185impl Wasi {
186    pub fn dump_memory(data: &[u8]) {
187        for (i, byte) in data.iter().enumerate() {
188            if i % 32 == 0 {
189                println!();
190            }
191            if i % 4 == 0 {
192                print!("|");
193            }
194            if *byte == 0 {
195                print!("__")
196            } else {
197                print!("{:02x}", byte);
198            }
199        }
200        println!();
201    }
202}
203
204impl Wasi {
205    async fn init(module: Vec<u8>, plugin: WasiPluginBuilder) -> Result<Self, Error> {
206        // initialize the WebAssembly System Interface context
207        let engine = plugin.engine;
208        let mut linker = plugin.linker;
209        wasmtime_wasi::add_to_linker(&mut linker, |s| &mut s.wasi_ctx)?;
210
211        // create a store, note that we can't initialize the allocator,
212        // because we can't grab the functions until initialized.
213        let mut store: Store<WasiCtxAlloc> = Store::new(
214            &engine,
215            WasiCtxAlloc {
216                wasi_ctx: plugin.wasi_ctx,
217                alloc: None,
218            },
219        );
220        let module = Module::new(&engine, module)?;
221
222        // load the provided module into the asynchronous runtime
223        linker.module_async(&mut store, "", &module).await?;
224        let instance = linker.instantiate_async(&mut store, &module).await?;
225
226        // now that the module is initialized,
227        // we can initialize the store's allocator
228        let alloc_buffer = instance.get_typed_func(&mut store, "__alloc_buffer")?;
229        let free_buffer = instance.get_typed_func(&mut store, "__free_buffer")?;
230        store.data_mut().init_alloc(WasiAlloc {
231            alloc_buffer,
232            free_buffer,
233        });
234
235        Ok(Wasi {
236            engine,
237            module,
238            store,
239            instance,
240        })
241    }
242
243    /// Attaches a file or directory the the given system path to the runtime.
244    /// Note that the resource must be freed by calling `remove_resource` afterwards.
245    pub fn attach_path<T: AsRef<Path>>(&mut self, path: T) -> Result<WasiResource, Error> {
246        // grab the WASI context
247        let ctx = self.store.data_mut();
248
249        // open the file we want, and convert it into the right type
250        // this is a footgun and a half
251        let file = File::open(&path).unwrap();
252        let dir = Dir::from_std_file(file);
253        let dir = Box::new(wasmtime_wasi::dir::Dir::from_cap_std(dir));
254
255        // grab an empty file descriptor, specify capabilities
256        let fd = ctx.wasi_ctx.table().push(Box::new(()))?;
257        let caps = dir::DirCaps::all();
258        let file_caps = file::FileCaps::all();
259
260        // insert the directory at the given fd,
261        // return a handle to the resource
262        ctx.wasi_ctx
263            .insert_dir(fd, dir, caps, file_caps, path.as_ref().to_path_buf());
264        Ok(WasiResource(fd))
265    }
266
267    /// Returns `true` if the resource existed and was removed.
268    pub fn remove_resource(&mut self, resource: WasiResource) -> Result<(), Error> {
269        self.store
270            .data_mut()
271            .wasi_ctx
272            .table()
273            .delete(resource.0)
274            .ok_or_else(|| anyhow!("Resource did not exist, but a valid handle was passed in"))?;
275        Ok(())
276    }
277
278    // pub fn with_resource<T>(
279    //     &mut self,
280    //     resource: WasiResource,
281    //     callback: fn(&mut Self) -> Result<T, Error>,
282    // ) -> Result<T, Error> {
283    //     let result = callback(self);
284    //     self.remove_resource(resource)?;
285    //     return result;
286    // }
287
288    // So this call function is kinda a dance, I figured it'd be a good idea to document it.
289    // the high level is we take a serde type, serialize it to a byte array,
290    // (we're doing this using bincode for now)
291    // then toss that byte array into webassembly.
292    // webassembly grabs that byte array, does some magic,
293    // and serializes the result into yet another byte array.
294    // we then grab *that* result byte array and deserialize it into a result.
295    //
296    // phew...
297    //
298    // now the problem is, webassambly doesn't support buffers.
299    // only really like i32s, that's it (yeah, it's sad. Not even unsigned!)
300    // (ok, I'm exaggerating a bit).
301    //
302    // the Wasm function that this calls must have a very specific signature:
303    //
304    // fn(pointer to byte array: i32, length of byte array: i32)
305    //     -> pointer to (
306    //            pointer to byte_array: i32,
307    //            length of byte array: i32,
308    //     ): i32
309    //
310    // This pair `(pointer to byte array, length of byte array)` is called a `Buffer`
311    // and can be found in the cargo_test plugin.
312    //
313    // so on the wasm side, we grab the two parameters to the function,
314    // stuff them into a `Buffer`,
315    // and then pray to the `unsafe` Rust gods above that a valid byte array pops out.
316    //
317    // On the flip side, when returning from a wasm function,
318    // we convert whatever serialized result we get into byte array,
319    // which we stuff into a Buffer and allocate on the heap,
320    // which pointer to we then return.
321    // Note the double indirection!
322    //
323    // So when returning from a function, we actually leak memory *twice*:
324    //
325    // 1) once when we leak the byte array
326    // 2) again when we leak the allocated `Buffer`
327    //
328    // This isn't a problem because Wasm stops executing after the function returns,
329    // so the heap is still valid for our inspection when we want to pull things out.
330
331    fn serialize<A: Serialize>(item: A) -> Result<Vec<u8>, Error> {
332        // serialize the argument using bincode
333        let item = bincode::serialize(&item)?;
334        Ok(item)
335    }
336
337    /// Takes an item, allocates a buffer, serializes the argument to that buffer,
338    /// and returns a (ptr, len) pair to that buffer.
339    async fn serialize_to_buffer(
340        alloc_buffer: TypedFunc<u32, u32>,
341        plugin_memory: &mut Memory,
342        mut store: impl AsContextMut<Data = WasiCtxAlloc>,
343        item: Vec<u8>,
344    ) -> Result<WasiBuffer, Error> {
345        // allocate a buffer and write the argument to that buffer
346        let len = item.len() as u32;
347        let ptr = alloc_buffer.call_async(&mut store, len).await?;
348        plugin_memory.write(&mut store, ptr as usize, &item)?;
349        Ok(WasiBuffer { ptr, len })
350    }
351
352    // /// Takes `ptr to a `(ptr, len)` pair, and returns `(ptr, len)`.
353    // fn deref_buffer(
354    //     plugin_memory: &mut Memory,
355    //     store: impl AsContext<Data = WasiCtxAlloc>,
356    //     buffer: u32,
357    // ) -> Result<(u32, u32), Error> {
358    //     // create a buffer to read the (ptr, length) pair into
359    //     // this is a total of 4 + 4 = 8 bytes.
360    //     let raw_buffer = &mut [0; 8];
361    //     plugin_memory.read(store, buffer as usize, raw_buffer)?;
362
363    //     // use these bytes (wasm stores things little-endian)
364    //     // to get a pointer to the buffer and its length
365    //     let b = raw_buffer;
366    //     let buffer_ptr = u32::from_le_bytes([b[0], b[1], b[2], b[3]]);
367    //     let buffer_len = u32::from_le_bytes([b[4], b[5], b[6], b[7]]);
368
369    //     return Ok((buffer_ptr, buffer_len));
370    // }
371
372    /// Takes a `(ptr, len)` pair and returns the corresponding deserialized buffer.
373    fn deserialize_from_buffer<R: DeserializeOwned>(
374        plugin_memory: &mut Memory,
375        store: impl AsContext<Data = WasiCtxAlloc>,
376        buffer: WasiBuffer,
377    ) -> Result<R, Error> {
378        let buffer_start = buffer.ptr as usize;
379        let buffer_end = buffer_start + buffer.len as usize;
380
381        // read the buffer at this point into a byte array
382        // deserialize the byte array into the provided serde type
383        let result = &plugin_memory.data(store.as_context())[buffer_start..buffer_end];
384        let result = bincode::deserialize(result)?;
385
386        // TODO: this is handled wasm-side
387        // // deallocate the argument buffer
388        // self.free_buffer.call(&mut self.store, arg_buffer);
389
390        Ok(result)
391    }
392
393    /// Retrieves the handle to a function of a given type.
394    pub fn function<A: Serialize, R: DeserializeOwned, T: AsRef<str>>(
395        &mut self,
396        name: T,
397    ) -> Result<WasiFn<A, R>, Error> {
398        let fun_name = format!("__{}", name.as_ref());
399        let fun = self
400            .instance
401            .get_typed_func::<u64, u64, _>(&mut self.store, &fun_name)?;
402        Ok(WasiFn {
403            function: fun,
404            _function_type: PhantomData,
405        })
406    }
407
408    // TODO: dont' use as for conversions
409    /// Asynchronously calls a function defined Guest-side.
410    pub async fn call<A: Serialize, R: DeserializeOwned>(
411        &mut self,
412        handle: &WasiFn<A, R>,
413        arg: A,
414    ) -> Result<R, Error> {
415        // dbg!(&handle.name);
416        // dbg!(serde_json::to_string(&arg)).unwrap();
417
418        let mut plugin_memory = self
419            .instance
420            .get_memory(&mut self.store, "memory")
421            .ok_or_else(|| anyhow!("Could not grab slice of plugin memory"))?;
422
423        // write the argument to linear memory
424        // this returns a (ptr, lentgh) pair
425        let arg_buffer = Self::serialize_to_buffer(
426            self.store.data().alloc_buffer(),
427            &mut plugin_memory,
428            &mut self.store,
429            Self::serialize(arg)?,
430        )
431        .await?;
432
433        // call the function, passing in the buffer and its length
434        // this returns a ptr to a (ptr, lentgh) pair
435        let result_buffer = handle
436            .function
437            .call_async(&mut self.store, arg_buffer.into_u64())
438            .await?;
439
440        Self::deserialize_from_buffer(
441            &mut plugin_memory,
442            &mut self.store,
443            WasiBuffer::from_u64(result_buffer),
444        )
445    }
446}