use crate::AsContextMut; use crate::component::func::{LiftContext, LowerContext, bad_type_info, desc}; use crate::component::matching::InstanceType; use crate::component::resources::{HostResourceIndex, HostResourceTables}; use crate::component::{ComponentType, Lift, Lower, ResourceAny, ResourceType}; use crate::prelude::*; use core::fmt; use core::marker; use core::mem::MaybeUninit; use core::sync::atomic::{AtomicU32, Ordering::Relaxed}; use wasmtime_environ::component::{CanonicalAbiInfo, InterfaceType}; /// Internal type in Wasmtime used to represent host-defined resources that are /// not tracked within the store. pub struct HostResource, D> { /// The host-defined 32-bit representation of this resource. rep: u32, /// Metadata, if necessary, tracking the type of this resource. ty: D, /// Internal state about borrows and such. state: AtomicResourceState, _marker: marker::PhantomData T>, } // FIXME(rust-lang/rust#110338) the `D` type parameter here should be an // associated type. In a first attempt at doing that the `wasmtime-wasi-io` // crate failed to compile with obscure errors. At least this is an internal // trait for now... pub trait HostResourceType { /// Tests whether `ty` matches this resource type. fn typecheck(ty: ResourceType) -> Option; /// Converts `Data` to a `ResourceType`. fn resource_type(data: D) -> ResourceType; } /// Internal dynamic state tracking for this resource. This can be one of /// four different states: /// /// * `BORROW` / `u64::MAX` - this indicates that this is a borrowed /// resource. The `rep` doesn't live in the host table and this `Resource` /// instance is transiently available. It's the host's responsibility to /// discard this resource when the borrow duration has finished. /// /// * `NOT_IN_TABLE` / `u64::MAX - 1` - this indicates that this is an owned /// resource not present in any store's table. This resource is not lent /// out. It can be passed as an `(own $t)` directly into a guest's table /// or it can be passed as a borrow to a guest which will insert it into /// a host store's table for dynamic borrow tracking. /// /// * `TAKEN` / `u64::MAX - 2` - while the `rep` is available the resource /// has been dynamically moved into a guest and cannot be moved in again. /// This is used for example to prevent the same resource from being /// passed twice to a guest. /// /// * All other values - any other value indicates that the value is an /// index into a store's table of host resources. It's guaranteed that the /// table entry represents a host resource and the resource may have /// borrow tracking associated with it. The low 32-bits of the value are /// the table index and the upper 32-bits are the generation. /// /// Note that this is two `AtomicU32` fields but it's not intended to actually /// be used in conjunction with threads as generally a `Store` lives on one /// thread at a time. The pair of `AtomicU32` here is used to ensure that this /// type is `Send + Sync` when captured as a reference to make async /// programming more ergonomic. /// /// Also note that two `AtomicU32` here are used instead of `AtomicU64` to be /// more portable to platforms without 64-bit atomics. struct AtomicResourceState { index: AtomicU32, generation: AtomicU32, } #[derive(Debug, PartialEq, Eq, Copy, Clone)] enum ResourceState { Borrow, NotInTable, Taken, Index(HostResourceIndex), } impl AtomicResourceState { const BORROW: Self = AtomicResourceState::new(ResourceState::Borrow); const NOT_IN_TABLE: Self = AtomicResourceState::new(ResourceState::NotInTable); const fn new(state: ResourceState) -> AtomicResourceState { let (index, generation) = state.encode(); Self { index: AtomicU32::new(index), generation: AtomicU32::new(generation), } } fn get(&self) -> ResourceState { ResourceState::decode(self.index.load(Relaxed), self.generation.load(Relaxed)) } fn swap(&self, state: ResourceState) -> ResourceState { let (index, generation) = state.encode(); let index_prev = self.index.load(Relaxed); self.index.store(index, Relaxed); let generation_prev = self.generation.load(Relaxed); self.generation.store(generation, Relaxed); ResourceState::decode(index_prev, generation_prev) } } impl ResourceState { // See comments on `state` above for info about these values. const BORROW: u32 = u32::MAX; const NOT_IN_TABLE: u32 = u32::MAX - 1; const TAKEN: u32 = u32::MAX - 2; fn decode(idx: u32, generation: u32) -> ResourceState { match generation { Self::BORROW => Self::Borrow, Self::NOT_IN_TABLE => Self::NotInTable, Self::TAKEN => Self::Taken, _ => Self::Index(HostResourceIndex::new(idx, generation)), } } const fn encode(&self) -> (u32, u32) { match self { Self::Borrow => (0, Self::BORROW), Self::NotInTable => (0, Self::NOT_IN_TABLE), Self::Taken => (0, Self::TAKEN), Self::Index(index) => (index.index(), index.generation()), } } } impl HostResource where T: HostResourceType, D: PartialEq + Send + Sync + Copy + 'static, { pub fn new_own(rep: u32, ty: D) -> Self { HostResource { state: AtomicResourceState::NOT_IN_TABLE, rep, ty, _marker: marker::PhantomData, } } pub fn new_borrow(rep: u32, ty: D) -> Self { HostResource { state: AtomicResourceState::BORROW, rep, ty, _marker: marker::PhantomData, } } pub fn rep(&self) -> u32 { self.rep } pub fn ty(&self) -> D { self.ty } pub fn owned(&self) -> bool { match self.state.get() { ResourceState::Borrow => false, ResourceState::Taken | ResourceState::NotInTable | ResourceState::Index(_) => true, } } fn lower_to_index(&self, cx: &mut LowerContext<'_, U>, ty: InterfaceType) -> Result { match ty { InterfaceType::Own(t) => { match T::typecheck(cx.resource_type(t)) { Some(t) if t == self.ty => {} _ => bail!("resource type mismatch"), } let rep = match self.state.get() { // If this is a borrow resource then this is a dynamic // error on behalf of the embedder. ResourceState::Borrow => { bail!("cannot lower a `borrow` resource into an `own`") } // If this resource does not yet live in a table then we're // dynamically transferring ownership to wasm. Record that // it's no longer present and then pass through the // representation. ResourceState::NotInTable => { let prev = self.state.swap(ResourceState::Taken); assert_eq!(prev, ResourceState::NotInTable); self.rep } // This resource has already been moved into wasm so this is // a dynamic error on behalf of the embedder. ResourceState::Taken => bail!("host resource already consumed"), // If this resource lives in a host table then try to take // it out of the table, which may fail, and on success we // can move the rep into the guest table. ResourceState::Index(idx) => cx.host_resource_lift_own(idx)?, }; cx.guest_resource_lower_own(t, rep) } InterfaceType::Borrow(t) => { match T::typecheck(cx.resource_type(t)) { Some(t) if t == self.ty => {} _ => bail!("resource type mismatch"), } let rep = match self.state.get() { // If this is already a borrowed resource, nothing else to // do and the rep is passed through. ResourceState::Borrow => self.rep, // If this resource is already gone, that's a dynamic error // for the embedder. ResourceState::Taken => bail!("host resource already consumed"), // If this resource is not currently in a table then it // needs to move into a table to participate in state // related to borrow tracking. Execute the // `host_resource_lower_own` operation here and update our // state. // // Afterwards this is the same as the `idx` case below. // // Note that flags/dtor are passed as `None` here since // `Resource` doesn't offer destruction support. ResourceState::NotInTable => { let idx = cx.host_resource_lower_own(self.rep, None, None)?; let prev = self.state.swap(ResourceState::Index(idx)); assert_eq!(prev, ResourceState::NotInTable); cx.host_resource_lift_borrow(idx)? } // If this resource lives in a table then it needs to come // out of the table with borrow-tracking employed. ResourceState::Index(idx) => cx.host_resource_lift_borrow(idx)?, }; cx.guest_resource_lower_borrow(t, rep) } _ => bad_type_info(), } } fn lift_from_index(cx: &mut LiftContext<'_>, ty: InterfaceType, index: u32) -> Result { let (state, rep, ty) = match ty { // Ownership is being transferred from a guest to the host, so move // it from the guest table into a new `Resource`. Note that this // isn't immediately inserted into the host table and that's left // for the future if it's necessary to take a borrow from this owned // resource. InterfaceType::Own(t) => { let (rep, dtor, flags) = cx.guest_resource_lift_own(t, index)?; assert!(dtor.is_some()); assert!(flags.is_none()); (AtomicResourceState::NOT_IN_TABLE, rep, t) } // The borrow here is lifted from the guest, but note the lack of // `host_resource_lower_borrow` as it's intentional. Lowering // a borrow has a special case in the canonical ABI where if the // receiving module is the owner of the resource then it directly // receives the `rep` and no other dynamic tracking is employed. // This effectively mirrors that even though the canonical ABI // isn't really all that applicable in host context here. InterfaceType::Borrow(t) => { let rep = cx.guest_resource_lift_borrow(t, index)?; (AtomicResourceState::BORROW, rep, t) } _ => bad_type_info(), }; let ty = T::typecheck(cx.resource_type(ty)).unwrap(); Ok(HostResource { state, rep, ty, _marker: marker::PhantomData, }) } pub fn try_into_resource_any(self, mut store: impl AsContextMut) -> Result { let HostResource { rep, state, ty, _marker: _, } = self; let store = store.as_context_mut(); let mut tables = HostResourceTables::new_host(store.0); let (idx, owned) = match state.get() { ResourceState::Borrow => (tables.host_resource_lower_borrow(rep)?, false), ResourceState::NotInTable => { let idx = tables.host_resource_lower_own(rep, None, None)?; (idx, true) } ResourceState::Taken => bail!("host resource already consumed"), ResourceState::Index(idx) => (idx, true), }; Ok(ResourceAny::new(idx, T::resource_type(ty), owned)) } } unsafe impl ComponentType for HostResource where T: HostResourceType, D: PartialEq + Send + Sync + Copy + 'static, { const ABI: CanonicalAbiInfo = CanonicalAbiInfo::SCALAR4; type Lower = ::Lower; fn typecheck(ty: &InterfaceType, types: &InstanceType<'_>) -> Result<()> { let resource = match ty { InterfaceType::Own(t) | InterfaceType::Borrow(t) => *t, other => bail!("expected `own` or `borrow`, found `{}`", desc(other)), }; if T::typecheck(types.resource_type(resource)).is_none() { bail!("resource type mismatch"); } Ok(()) } } unsafe impl Lower for HostResource where T: HostResourceType, D: PartialEq + Send + Sync + Copy + 'static, { fn linear_lower_to_flat( &self, cx: &mut LowerContext<'_, U>, ty: InterfaceType, dst: &mut MaybeUninit, ) -> Result<()> { self.lower_to_index(cx, ty)? .linear_lower_to_flat(cx, InterfaceType::U32, dst) } fn linear_lower_to_memory( &self, cx: &mut LowerContext<'_, U>, ty: InterfaceType, offset: usize, ) -> Result<()> { self.lower_to_index(cx, ty)? .linear_lower_to_memory(cx, InterfaceType::U32, offset) } } unsafe impl Lift for HostResource where T: HostResourceType, D: PartialEq + Send + Sync + Copy + 'static, { fn linear_lift_from_flat( cx: &mut LiftContext<'_>, ty: InterfaceType, src: &Self::Lower, ) -> Result { let index = u32::linear_lift_from_flat(cx, InterfaceType::U32, src)?; HostResource::lift_from_index(cx, ty, index) } fn linear_lift_from_memory( cx: &mut LiftContext<'_>, ty: InterfaceType, bytes: &[u8], ) -> Result { let index = u32::linear_lift_from_memory(cx, InterfaceType::U32, bytes)?; HostResource::lift_from_index(cx, ty, index) } } impl fmt::Debug for HostResource where T: HostResourceType, D: PartialEq + Send + Sync + Copy + 'static, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let state = match self.state.get() { ResourceState::Borrow => "borrow", ResourceState::NotInTable => "own (not in table)", ResourceState::Taken => "taken", ResourceState::Index(_) => "own", }; f.debug_struct("Resource") .field("rep", &self.rep) .field("state", &state) .finish() } }