Implement RFC 11: Redesigning Wasmtime's APIs (#2897)
Implement Wasmtime's new API as designed by RFC 11. This is quite a large commit which has had lots of discussion externally, so for more information it's best to read the RFC thread and the PR thread.
This commit is contained in:
259
crates/wiggle/src/borrow.rs
Normal file
259
crates/wiggle/src/borrow.rs
Normal file
@@ -0,0 +1,259 @@
|
||||
use crate::{BorrowHandle, GuestError, Region};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Mutex;
|
||||
|
||||
pub struct BorrowChecker {
|
||||
/// Unfortunately, since the terminology of std::cell and the problem domain of borrow checking
|
||||
/// overlap, the method calls on this member will be confusing.
|
||||
///
|
||||
/// Also, unfortunately, for now this uses a `Mutex`. The reason for that is
|
||||
/// that this is shared as `&BorrowChecker` in a bunch of `GuestPtr` values.
|
||||
/// Through this sharing we still want each `GuestPtr` to be `Send` and the
|
||||
/// "naive" way to make `&T` `Send` with interior mutability is to use a
|
||||
/// `Mutex`. Fixing this will likely require rethinking `GuestPtr` one way
|
||||
/// or another. That needs to happen for other reasons as well (for example
|
||||
/// to allow for wasm calls to happen while `GuestPtr` values are active),
|
||||
/// so it's hoped that in a later rethinking of `GuestPtr` we can revisit
|
||||
/// this and remove this `Mutex`.
|
||||
bc: Mutex<InnerBorrowChecker>,
|
||||
}
|
||||
|
||||
impl BorrowChecker {
|
||||
/// A `BorrowChecker` manages run-time validation of borrows from a
|
||||
/// `GuestMemory`. It keeps track of regions of guest memory which are
|
||||
/// possible to alias with Rust references (via the `GuestSlice` and
|
||||
/// `GuestStr` structs, which implement `std::ops::Deref` and
|
||||
/// `std::ops::DerefMut`. It also enforces that `GuestPtr::read`
|
||||
/// does not access memory with an outstanding mutable borrow, and
|
||||
/// `GuestPtr::write` does not access memory with an outstanding
|
||||
/// shared or mutable borrow.
|
||||
pub fn new() -> Self {
|
||||
BorrowChecker {
|
||||
bc: Mutex::new(InnerBorrowChecker::new()),
|
||||
}
|
||||
}
|
||||
/// Indicates whether any outstanding shared or mutable borrows are known
|
||||
/// to the `BorrowChecker`. This function must be `false` in order for it
|
||||
/// to be safe to recursively call into a WebAssembly module, or to
|
||||
/// manipulate the WebAssembly memory by any other means.
|
||||
pub fn has_outstanding_borrows(&self) -> bool {
|
||||
self.bc.lock().unwrap().has_outstanding_borrows()
|
||||
}
|
||||
pub fn shared_borrow(&self, r: Region) -> Result<BorrowHandle, GuestError> {
|
||||
self.bc.lock().unwrap().shared_borrow(r)
|
||||
}
|
||||
pub fn mut_borrow(&self, r: Region) -> Result<BorrowHandle, GuestError> {
|
||||
self.bc.lock().unwrap().mut_borrow(r)
|
||||
}
|
||||
pub fn shared_unborrow(&self, h: BorrowHandle) {
|
||||
self.bc.lock().unwrap().shared_unborrow(h)
|
||||
}
|
||||
pub fn mut_unborrow(&self, h: BorrowHandle) {
|
||||
self.bc.lock().unwrap().mut_unborrow(h)
|
||||
}
|
||||
pub fn is_shared_borrowed(&self, r: Region) -> bool {
|
||||
self.bc.lock().unwrap().is_shared_borrowed(r)
|
||||
}
|
||||
pub fn is_mut_borrowed(&self, r: Region) -> bool {
|
||||
self.bc.lock().unwrap().is_mut_borrowed(r)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// This is a pretty naive way to account for borrows. This datastructure
|
||||
/// could be made a lot more efficient with some effort.
|
||||
struct InnerBorrowChecker {
|
||||
/// Maps from handle to region borrowed. A HashMap is probably not ideal
|
||||
/// for this but it works. It would be more efficient if we could
|
||||
/// check `is_borrowed` without an O(n) iteration, by organizing borrows
|
||||
/// by an ordering of Region.
|
||||
shared_borrows: HashMap<BorrowHandle, Region>,
|
||||
mut_borrows: HashMap<BorrowHandle, Region>,
|
||||
/// Handle to give out for the next borrow. This is the bare minimum of
|
||||
/// bookkeeping of free handles, and in a pathological case we could run
|
||||
/// out, hence [`GuestError::BorrowCheckerOutOfHandles`]
|
||||
next_handle: BorrowHandle,
|
||||
}
|
||||
|
||||
impl InnerBorrowChecker {
|
||||
fn new() -> Self {
|
||||
InnerBorrowChecker {
|
||||
shared_borrows: HashMap::new(),
|
||||
mut_borrows: HashMap::new(),
|
||||
next_handle: BorrowHandle(0),
|
||||
}
|
||||
}
|
||||
|
||||
fn has_outstanding_borrows(&self) -> bool {
|
||||
!(self.shared_borrows.is_empty() && self.mut_borrows.is_empty())
|
||||
}
|
||||
|
||||
fn is_shared_borrowed(&self, r: Region) -> bool {
|
||||
self.shared_borrows.values().any(|b| b.overlaps(r))
|
||||
}
|
||||
fn is_mut_borrowed(&self, r: Region) -> bool {
|
||||
self.mut_borrows.values().any(|b| b.overlaps(r))
|
||||
}
|
||||
|
||||
fn new_handle(&mut self) -> Result<BorrowHandle, GuestError> {
|
||||
// Reset handles to 0 if all handles have been returned.
|
||||
if self.shared_borrows.is_empty() && self.mut_borrows.is_empty() {
|
||||
self.next_handle = BorrowHandle(0);
|
||||
}
|
||||
let h = self.next_handle;
|
||||
// Get the next handle. Since we don't recycle handles until all of
|
||||
// them have been returned, there is a pathological case where a user
|
||||
// may make a Very Large (usize::MAX) number of valid borrows and
|
||||
// unborrows while always keeping at least one borrow outstanding, and
|
||||
// we will run out of borrow handles.
|
||||
self.next_handle = BorrowHandle(
|
||||
h.0.checked_add(1)
|
||||
.ok_or_else(|| GuestError::BorrowCheckerOutOfHandles)?,
|
||||
);
|
||||
Ok(h)
|
||||
}
|
||||
|
||||
fn shared_borrow(&mut self, r: Region) -> Result<BorrowHandle, GuestError> {
|
||||
if self.is_mut_borrowed(r) {
|
||||
return Err(GuestError::PtrBorrowed(r));
|
||||
}
|
||||
let h = self.new_handle()?;
|
||||
self.shared_borrows.insert(h, r);
|
||||
Ok(h)
|
||||
}
|
||||
|
||||
fn mut_borrow(&mut self, r: Region) -> Result<BorrowHandle, GuestError> {
|
||||
if self.is_shared_borrowed(r) || self.is_mut_borrowed(r) {
|
||||
return Err(GuestError::PtrBorrowed(r));
|
||||
}
|
||||
let h = self.new_handle()?;
|
||||
self.mut_borrows.insert(h, r);
|
||||
Ok(h)
|
||||
}
|
||||
|
||||
fn shared_unborrow(&mut self, h: BorrowHandle) {
|
||||
let removed = self.shared_borrows.remove(&h);
|
||||
debug_assert!(removed.is_some(), "double-freed shared borrow");
|
||||
}
|
||||
|
||||
fn mut_unborrow(&mut self, h: BorrowHandle) {
|
||||
let removed = self.mut_borrows.remove(&h);
|
||||
debug_assert!(removed.is_some(), "double-freed mut borrow");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn nonoverlapping() {
|
||||
let mut bs = InnerBorrowChecker::new();
|
||||
let r1 = Region::new(0, 10);
|
||||
let r2 = Region::new(10, 10);
|
||||
assert!(!r1.overlaps(r2));
|
||||
bs.mut_borrow(r1).expect("can borrow r1");
|
||||
bs.mut_borrow(r2).expect("can borrow r2");
|
||||
|
||||
let mut bs = InnerBorrowChecker::new();
|
||||
let r1 = Region::new(10, 10);
|
||||
let r2 = Region::new(0, 10);
|
||||
assert!(!r1.overlaps(r2));
|
||||
bs.mut_borrow(r1).expect("can borrow r1");
|
||||
bs.mut_borrow(r2).expect("can borrow r2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overlapping() {
|
||||
let mut bs = InnerBorrowChecker::new();
|
||||
let r1 = Region::new(0, 10);
|
||||
let r2 = Region::new(9, 10);
|
||||
assert!(r1.overlaps(r2));
|
||||
bs.shared_borrow(r1).expect("can borrow r1");
|
||||
assert!(bs.mut_borrow(r2).is_err(), "cant mut borrow r2");
|
||||
bs.shared_borrow(r2).expect("can shared borrow r2");
|
||||
|
||||
let mut bs = InnerBorrowChecker::new();
|
||||
let r1 = Region::new(0, 10);
|
||||
let r2 = Region::new(2, 5);
|
||||
assert!(r1.overlaps(r2));
|
||||
bs.shared_borrow(r1).expect("can borrow r1");
|
||||
assert!(bs.mut_borrow(r2).is_err(), "cant borrow r2");
|
||||
bs.shared_borrow(r2).expect("can shared borrow r2");
|
||||
|
||||
let mut bs = InnerBorrowChecker::new();
|
||||
let r1 = Region::new(9, 10);
|
||||
let r2 = Region::new(0, 10);
|
||||
assert!(r1.overlaps(r2));
|
||||
bs.shared_borrow(r1).expect("can borrow r1");
|
||||
assert!(bs.mut_borrow(r2).is_err(), "cant borrow r2");
|
||||
bs.shared_borrow(r2).expect("can shared borrow r2");
|
||||
|
||||
let mut bs = InnerBorrowChecker::new();
|
||||
let r1 = Region::new(2, 5);
|
||||
let r2 = Region::new(0, 10);
|
||||
assert!(r1.overlaps(r2));
|
||||
bs.shared_borrow(r1).expect("can borrow r1");
|
||||
assert!(bs.mut_borrow(r2).is_err(), "cant borrow r2");
|
||||
bs.shared_borrow(r2).expect("can shared borrow r2");
|
||||
|
||||
let mut bs = InnerBorrowChecker::new();
|
||||
let r1 = Region::new(2, 5);
|
||||
let r2 = Region::new(10, 5);
|
||||
let r3 = Region::new(15, 5);
|
||||
let r4 = Region::new(0, 10);
|
||||
assert!(r1.overlaps(r4));
|
||||
bs.shared_borrow(r1).expect("can borrow r1");
|
||||
bs.shared_borrow(r2).expect("can borrow r2");
|
||||
bs.shared_borrow(r3).expect("can borrow r3");
|
||||
assert!(bs.mut_borrow(r4).is_err(), "cant mut borrow r4");
|
||||
bs.shared_borrow(r4).expect("can shared borrow r4");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unborrowing() {
|
||||
let mut bs = InnerBorrowChecker::new();
|
||||
let r1 = Region::new(0, 10);
|
||||
let r2 = Region::new(10, 10);
|
||||
assert!(!r1.overlaps(r2));
|
||||
assert_eq!(bs.has_outstanding_borrows(), false, "start with no borrows");
|
||||
let h1 = bs.mut_borrow(r1).expect("can borrow r1");
|
||||
assert_eq!(bs.has_outstanding_borrows(), true, "h1 is outstanding");
|
||||
let h2 = bs.mut_borrow(r2).expect("can borrow r2");
|
||||
|
||||
assert!(bs.mut_borrow(r2).is_err(), "can't borrow r2 twice");
|
||||
bs.mut_unborrow(h2);
|
||||
assert_eq!(
|
||||
bs.has_outstanding_borrows(),
|
||||
true,
|
||||
"h1 is still outstanding"
|
||||
);
|
||||
bs.mut_unborrow(h1);
|
||||
assert_eq!(bs.has_outstanding_borrows(), false, "no remaining borrows");
|
||||
|
||||
let _h3 = bs
|
||||
.mut_borrow(r2)
|
||||
.expect("can borrow r2 again now that its been unborrowed");
|
||||
|
||||
// Lets try again with shared:
|
||||
|
||||
let mut bs = InnerBorrowChecker::new();
|
||||
let r1 = Region::new(0, 10);
|
||||
let r2 = Region::new(10, 10);
|
||||
assert!(!r1.overlaps(r2));
|
||||
assert_eq!(bs.has_outstanding_borrows(), false, "start with no borrows");
|
||||
let h1 = bs.shared_borrow(r1).expect("can borrow r1");
|
||||
assert_eq!(bs.has_outstanding_borrows(), true, "h1 is outstanding");
|
||||
let h2 = bs.shared_borrow(r2).expect("can borrow r2");
|
||||
let h3 = bs.shared_borrow(r2).expect("can shared borrow r2 twice");
|
||||
|
||||
bs.shared_unborrow(h2);
|
||||
assert_eq!(
|
||||
bs.has_outstanding_borrows(),
|
||||
true,
|
||||
"h1, h3 still outstanding"
|
||||
);
|
||||
bs.shared_unborrow(h1);
|
||||
bs.shared_unborrow(h3);
|
||||
assert_eq!(bs.has_outstanding_borrows(), false, "no remaining borrows");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user