Skip to content

Commit

Permalink
Multiple changes required to implement the wasmer terminal on the bro…
Browse files Browse the repository at this point in the history
…wser

- Split functionality out of WasiEnv so that it can support multi-threading
- Added methods to the VFS File Trait that supporting polling
- Implemented basic time functionality for WASI
- Incorported a yield callback for when WASI processes idle
- Improved the error handling on WASI IO calls
- Reduce the verbose logging on some critical WASI calls (write/read)
- Implemented the missing poll functionality for WASI processes
- Moved the syspoll functionality behind a feature flag to default to WASI method
- Refactored the thread sleeping functionality for WASI processes
- Fixed the files system benchmark which was not compiling
- Modified the file system trait so that it is SYNC and thus can handle multiple threads
- Removed the large mutex around filesystem state and implemented granular locks instead
  (this is needed to fix a deadlock scenario on the terminal)
- Split the inodes object apart from the state to fix the deadlock scenario.
- Few minor fixes to some warnings when not using certain features
- Sleeping will now call a callback that can be used by the runtime operator when
  a WASI thread goes to sleep (for instance to do other work)
- Fixed a bug where paths that exist on the real file system are leaking into VFS
- Timing functions now properly return a time precision on WASI
- Some improved macros for error handling within syscalls (wasi_try_ok!)
- Refactored the remove_directory WASI function which was not working properly
- Refactored the unlink WASI function which was not working properly
- Refactored the poll WASI function which was not working properly
- Updates some of the tests to make them compile again
- Rewrote the OutputCapturer so that it does leak into the internals
  • Loading branch information
john-sharratt committed May 14, 2022
1 parent 09d5d6d commit 891b1c7
Show file tree
Hide file tree
Showing 18 changed files with 1,804 additions and 1,077 deletions.
22 changes: 12 additions & 10 deletions lib/api/src/js/externals/memory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -237,10 +237,8 @@ impl Memory {
Ok(Pages(new_pages))
}

/// Used by tests
#[doc(hidden)]
pub fn uint8view(&self) -> js_sys::Uint8Array {
self.view.clone()
pub(crate) fn uint8view(&self) -> js_sys::Uint8Array {
js_sys::Uint8Array::new(&self.vm_memory.memory.buffer())
}

pub(crate) fn from_vm_export(store: &Store, vm_memory: VMMemory) -> Self {
Expand Down Expand Up @@ -276,16 +274,17 @@ impl Memory {
/// This method is guaranteed to be safe (from the host side) in the face of
/// concurrent writes.
pub fn read(&self, offset: u64, buf: &mut [u8]) -> Result<(), MemoryAccessError> {
let view = self.uint8view();
let offset: u32 = offset.try_into().map_err(|_| MemoryAccessError::Overflow)?;
let len: u32 = buf
.len()
.try_into()
.map_err(|_| MemoryAccessError::Overflow)?;
let end = offset.checked_add(len).ok_or(MemoryAccessError::Overflow)?;
if end > self.view.length() {
if end > view.length() {
Err(MemoryAccessError::HeapOutOfBounds)?;
}
self.view.subarray(offset, end).copy_to(buf);
view.subarray(offset, end).copy_to(buf);
Ok(())
}

Expand All @@ -304,13 +303,14 @@ impl Memory {
offset: u64,
buf: &'a mut [MaybeUninit<u8>],
) -> Result<&'a mut [u8], MemoryAccessError> {
let view = self.uint8view();
let offset: u32 = offset.try_into().map_err(|_| MemoryAccessError::Overflow)?;
let len: u32 = buf
.len()
.try_into()
.map_err(|_| MemoryAccessError::Overflow)?;
let end = offset.checked_add(len).ok_or(MemoryAccessError::Overflow)?;
if end > self.view.length() {
if end > view.length() {
Err(MemoryAccessError::HeapOutOfBounds)?;
}

Expand All @@ -321,7 +321,7 @@ impl Memory {
}
let buf = unsafe { slice::from_raw_parts_mut(buf.as_mut_ptr() as *mut u8, buf.len()) };

self.view.subarray(offset, end).copy_to(buf);
view.subarray(offset, end).copy_to(buf);
Ok(buf)
}

Expand All @@ -333,16 +333,18 @@ impl Memory {
/// This method is guaranteed to be safe (from the host side) in the face of
/// concurrent reads/writes.
pub fn write(&self, offset: u64, data: &[u8]) -> Result<(), MemoryAccessError> {
let view = self.uint8view();
let offset: u32 = offset.try_into().map_err(|_| MemoryAccessError::Overflow)?;
let len: u32 = data
.len()
.try_into()
.map_err(|_| MemoryAccessError::Overflow)?;
let view = self.uint8view();
let end = offset.checked_add(len).ok_or(MemoryAccessError::Overflow)?;
if end > self.view.length() {
if end > view.length() {
Err(MemoryAccessError::HeapOutOfBounds)?;
}
self.view.subarray(offset, end).copy_from(data);
view.subarray(offset, end).copy_from(data);
Ok(())
}
}
Expand Down
4 changes: 2 additions & 2 deletions lib/cache/benches/bench_filesystem_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ pub fn store_cache_native(c: &mut Criterion) {
let tmp_dir = TempDir::new().unwrap();
let mut fs_cache = FileSystemCache::new(tmp_dir.path()).unwrap();
let compiler = Singlepass::default();
let store = Store::new(&Native::new(compiler).engine());
let store = Store::new(&Universal::new(compiler).engine());
let module = Module::new(
&store,
std::fs::read("../../lib/c-api/examples/assets/qjs.wasm").unwrap(),
Expand All @@ -73,7 +73,7 @@ pub fn load_cache_native(c: &mut Criterion) {
let tmp_dir = TempDir::new().unwrap();
let mut fs_cache = FileSystemCache::new(tmp_dir.path()).unwrap();
let compiler = Singlepass::default();
let store = Store::new(&Native::new(compiler).engine());
let store = Store::new(&Universal::new(compiler).engine());
let module = Module::new(
&store,
std::fs::read("../../lib/c-api/examples/assets/qjs.wasm").unwrap(),
Expand Down
2 changes: 1 addition & 1 deletion lib/cli/src/commands/run/wasi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ pub struct Wasi {

/// Enable experimental IO devices
#[cfg(feature = "experimental-io-devices")]
#[structopt(long = "enable-experimental-io-devices")]
#[cfg_attr(feature = "experimental-io-devices", structopt(long = "enable-experimental-io-devices"))]
enable_experimental_io_devices: bool,

/// Allow WASI modules to import multiple versions of WASI without a warning.
Expand Down
4 changes: 2 additions & 2 deletions lib/vfs/src/host_fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ impl TryInto<Metadata> for fs::Metadata {
pub struct FileOpener;

impl crate::FileOpener for FileOpener {
fn open(&mut self, path: &Path, conf: &OpenOptionsConfig) -> Result<Box<dyn VirtualFile>> {
fn open(&mut self, path: &Path, conf: &OpenOptionsConfig) -> Result<Box<dyn VirtualFile + Sync>> {
// TODO: handle create implying write, etc.
let read = conf.read();
let write = conf.write();
Expand All @@ -193,7 +193,7 @@ impl crate::FileOpener for FileOpener {
.map_err(Into::into)
.map(|file| {
Box::new(File::new(file, path.to_owned(), read, write, append))
as Box<dyn VirtualFile>
as Box<dyn VirtualFile + Sync>
})
}
}
Expand Down
4 changes: 2 additions & 2 deletions lib/vfs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ impl dyn FileSystem + 'static {
}

pub trait FileOpener {
fn open(&mut self, path: &Path, conf: &OpenOptionsConfig) -> Result<Box<dyn VirtualFile>>;
fn open(&mut self, path: &Path, conf: &OpenOptionsConfig) -> Result<Box<dyn VirtualFile + Sync>>;
}

#[derive(Debug, Clone)]
Expand Down Expand Up @@ -142,7 +142,7 @@ impl OpenOptions {
self
}

pub fn open<P: AsRef<Path>>(&mut self, path: P) -> Result<Box<dyn VirtualFile>> {
pub fn open<P: AsRef<Path>>(&mut self, path: P) -> Result<Box<dyn VirtualFile + Sync>> {
self.opener.open(path.as_ref(), &self.conf)
}
}
Expand Down
2 changes: 1 addition & 1 deletion lib/vfs/src/mem_fs/file_opener.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ pub struct FileOpener {
}

impl crate::FileOpener for FileOpener {
fn open(&mut self, path: &Path, conf: &OpenOptionsConfig) -> Result<Box<dyn VirtualFile>> {
fn open(&mut self, path: &Path, conf: &OpenOptionsConfig) -> Result<Box<dyn VirtualFile + Sync>> {
let read = conf.read();
let mut write = conf.write();
let append = conf.append();
Expand Down
9 changes: 7 additions & 2 deletions lib/wasi-experimental-io-devices/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::collections::{BTreeSet, VecDeque};
use std::convert::TryInto;
use std::io::{Read, Seek, SeekFrom, Write};
use tracing::debug;
use wasmer_wasi::types::*;
use wasmer_wasi::{types::*, WasiInodes};
use wasmer_wasi::{Fd, VirtualFile, WasiFs, WasiFsError, ALL_RIGHTS, VIRTUAL_ROOT_FD};

use minifb::{Key, KeyRepeat, MouseButton, Scale, Window, WindowOptions};
Expand Down Expand Up @@ -426,7 +426,7 @@ impl VirtualFile for FrameBuffer {
}
}

pub fn initialize(fs: &mut WasiFs) -> Result<(), String> {
pub fn initialize(inodes: &mut WasiInodes, fs: &mut WasiFs) -> Result<(), String> {
let frame_buffer_file = Box::new(FrameBuffer {
fb_type: FrameBufferFileType::Buffer,
cursor: 0,
Expand All @@ -446,6 +446,7 @@ pub fn initialize(fs: &mut WasiFs) -> Result<(), String> {

let base_dir_fd = unsafe {
fs.open_dir_all(
inodes,
VIRTUAL_ROOT_FD,
"_wasmer/dev/fb0".to_string(),
ALL_RIGHTS,
Expand All @@ -457,6 +458,7 @@ pub fn initialize(fs: &mut WasiFs) -> Result<(), String> {

let _fd = fs
.open_file_at(
inodes,
base_dir_fd,
input_file,
Fd::READ,
Expand All @@ -471,6 +473,7 @@ pub fn initialize(fs: &mut WasiFs) -> Result<(), String> {

let _fd = fs
.open_file_at(
inodes,
base_dir_fd,
frame_buffer_file,
Fd::READ | Fd::WRITE,
Expand All @@ -485,6 +488,7 @@ pub fn initialize(fs: &mut WasiFs) -> Result<(), String> {

let _fd = fs
.open_file_at(
inodes,
base_dir_fd,
resolution_file,
Fd::READ | Fd::WRITE,
Expand All @@ -499,6 +503,7 @@ pub fn initialize(fs: &mut WasiFs) -> Result<(), String> {

let _fd = fs
.open_file_at(
inodes,
base_dir_fd,
draw_file,
Fd::READ | Fd::WRITE,
Expand Down
145 changes: 136 additions & 9 deletions lib/wasi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ mod utils;
use crate::syscalls::*;

pub use crate::state::{
Fd, Pipe, Stderr, Stdin, Stdout, WasiFs, WasiState, WasiStateBuilder, WasiStateCreationError,
Fd, Pipe, Stderr, Stdin, Stdout, WasiFs, WasiInodes, WasiState, WasiStateBuilder, WasiStateCreationError,
ALL_RIGHTS, VIRTUAL_ROOT_FD,
};
pub use crate::syscalls::types;
Expand All @@ -52,13 +52,15 @@ pub use wasmer_vfs::FsError as WasiFsError;
#[deprecated(since = "2.1.0", note = "Please use `wasmer_vfs::VirtualFile`")]
pub use wasmer_vfs::VirtualFile as WasiFile;
pub use wasmer_vfs::{FsError, VirtualFile};
use wasmer_wasi_types::__WASI_CLOCK_MONOTONIC;

use thiserror::Error;
use wasmer::{
imports, Function, Imports, LazyInit, Memory, MemoryAccessError, Module, Store, WasmerEnv,
};

use std::sync::{Arc, Mutex, MutexGuard};
use std::time::Duration;
use std::sync::{atomic::AtomicU32, atomic::Ordering, Arc, RwLockReadGuard, RwLockWriteGuard};

/// This is returned in `RuntimeError`.
/// Use `downcast` or `downcast_ref` to retrieve the `ExitCode`.
Expand All @@ -84,12 +86,95 @@ pub struct WasiEnv {
memory: LazyInit<Memory>,
}

impl WasiEnv {
pub fn new(state: WasiState) -> Self {
Self {
state: Arc::new(Mutex::new(state)),
memory: LazyInit::new(),
/// The WASI thread dereferences into the WASI environment
impl Deref for WasiThread {
type Target = WasiEnv;

fn deref(&self) -> &WasiEnv {
&self.env
}
}

impl WasiThread {
/// Returns the unique ID of this thread
pub fn thread_id(&self) -> u32 {
self.id
}

/// Yields execution
pub fn yield_callback(&self) -> Result<(), WasiError> {
if let Some(callback) = self.on_yield.as_ref() {
callback(self)?;
}
Ok(())
}

// Yields execution
pub fn yield_now(&self) -> Result<(), WasiError> {
self.yield_callback()?;
Ok(())
}

// Sleeps for a period of time
pub fn sleep(&self, duration: Duration) -> Result<(), WasiError> {
let duration = duration.as_nanos();
let start = platform_clock_time_get(__WASI_CLOCK_MONOTONIC, 1_000_000).unwrap() as u128;
self.yield_now()?;
loop {
let now = platform_clock_time_get(__WASI_CLOCK_MONOTONIC, 1_000_000).unwrap() as u128;
let delta = match now.checked_sub(start) {
Some(a) => a,
None => { break; }
};
if delta >= duration {
break;
}
let remaining = match duration.checked_sub(delta) {
Some(a) => Duration::from_nanos(a as u64),
None => { break; }
};
std::thread::sleep(remaining.min(Duration::from_millis(10)));
self.yield_now()?;
}
Ok(())
}

/// Get a reference to the memory
pub fn memory(&self) -> &Memory {
self.memory_ref()
.expect("Memory should be set on `WasiEnv` first")
}

// Copy the lazy reference so that when its initialized during the
// export phase that all the other references get a copy of it
pub fn memory_clone(&self) -> LazyInit<Memory> {
self.memory.clone()
}

pub(crate) fn get_memory_and_wasi_state(&self, _mem_index: u32) -> (&Memory, &WasiState) {
let memory = self.memory();
let state = self.state.deref();
(memory, state)
}

pub(crate) fn get_memory_and_wasi_state_and_inodes(
&self,
_mem_index: u32,
) -> (&Memory, &WasiState, RwLockReadGuard<WasiInodes>) {
let memory = self.memory();
let state = self.state.deref();
let inodes = state.inodes.read().unwrap();
(memory, state, inodes)
}

pub(crate) fn get_memory_and_wasi_state_and_inodes_mut(
&self,
_mem_index: u32,
) -> (&Memory, &WasiState, RwLockWriteGuard<WasiInodes>) {
let memory = self.memory();
let state = self.state.deref();
let inodes = state.inodes.write().unwrap();
(memory, state, inodes)
}

/// Get an `Imports` for a specific version of WASI detected in the module.
Expand Down Expand Up @@ -121,14 +206,56 @@ impl WasiEnv {
}
Ok(resolver)
}
}

/// The environment provided to the WASI imports.
#[derive(Derivative, Clone)]
#[derivative(Debug)]
pub struct WasiEnv {
/// Represents a reference to the memory
memory: LazyInit<Memory>,
/// Shared state of the WASI system. Manages all the data that the
/// executing WASI program can see.
///
/// Holding a read lock across WASM calls now is allowed
pub state: Arc<WasiState>,
/// Optional callback thats invoked whenever a syscall is made
/// which is used to make callbacks to the process without breaking
/// the single threaded WASM modules
#[derivative(Debug = "ignore")]
pub(crate) on_yield: Option<Arc<dyn Fn(&WasiThread) -> Result<(), WasiError> + Send + Sync + 'static>>,
/// The thread ID seed is used to generate unique thread identifiers
/// for each WasiThread. These are needed for multithreading code that needs
/// this information in the syscalls
pub(crate) thread_id_seed: Arc<AtomicU32>,
}

impl WasiEnv {
pub fn new(state: WasiState) -> Self {
Self {
state: Arc::new(state),
memory: LazyInit::new(),
on_yield: None,
thread_id_seed: Arc::new(AtomicU32::new(1u32)),
}
}

/// Creates a new thread only this wasi environment
pub fn new_thread(&self) -> WasiThread {
WasiThread {
id: self.thread_id_seed.fetch_add(1, Ordering::Relaxed),
env: self.clone(),
memory: self.memory_clone(),
}
}

/// Get the WASI state
///
/// Be careful when using this in host functions that call into Wasm:
/// if the lock is held and the Wasm calls into a host function that tries
/// to lock this mutex, the program will deadlock.
pub fn state(&self) -> MutexGuard<WasiState> {
self.state.lock().unwrap()
pub fn state(&self) -> &WasiState {
self.state.deref()
}

/// Get a reference to the memory
Expand Down
Loading

0 comments on commit 891b1c7

Please sign in to comment.