Copy spin-core and spin-app crates from prototype

Signed-off-by: Lann Martin <lann.martin@fermyon.com>
This commit is contained in:
Lann Martin 2022-09-12 09:55:03 -04:00
parent 0de1d494a4
commit 216a5f7a23
No known key found for this signature in database
GPG Key ID: A30EEE9905396642
13 changed files with 1226 additions and 0 deletions

81
Cargo.lock generated
View File

@ -2,6 +2,12 @@
# It is not intended for manual editing.
version = 3
[[package]]
name = "Inflector"
version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3"
[[package]]
name = "addr2line"
version = "0.17.0"
@ -37,6 +43,12 @@ dependencies = [
"memchr",
]
[[package]]
name = "aliasable"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd"
[[package]]
name = "ambient-authority"
version = "0.0.1"
@ -2183,6 +2195,29 @@ version = "6.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff"
[[package]]
name = "ouroboros"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfbb50b356159620db6ac971c6d5c9ab788c9cc38a6f49619fca2a27acb062ca"
dependencies = [
"aliasable",
"ouroboros_macro",
]
[[package]]
name = "ouroboros_macro"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a0d9d1a6191c4f391f87219d1ea42b23f09ee84d64763cd05ee6ea88d9f384d"
dependencies = [
"Inflector",
"proc-macro-error",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "outbound-http"
version = "0.2.0"
@ -3268,6 +3303,19 @@ dependencies = [
"wit-bindgen-wasmtime",
]
[[package]]
name = "spin-app"
version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
"ouroboros",
"serde",
"serde_json",
"spin-core",
"thiserror",
]
[[package]]
name = "spin-build"
version = "0.2.0"
@ -3349,6 +3397,19 @@ dependencies = [
"wit-bindgen-wasmtime",
]
[[package]]
name = "spin-core"
version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
"tracing",
"wasi-cap-std-sync",
"wasi-common",
"wasmtime",
"wasmtime-wasi",
]
[[package]]
name = "spin-engine"
version = "0.2.0"
@ -4312,6 +4373,24 @@ dependencies = [
"windows-sys",
]
[[package]]
name = "wasi-tokio"
version = "0.39.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ab325bba31ae9286b8ebdc18d32a43d6471312c9bc4e477240be444e00ec4f4"
dependencies = [
"anyhow",
"cap-std 0.25.2",
"io-extras 0.15.0",
"io-lifetimes 0.7.3",
"lazy_static",
"rustix 0.35.9",
"tokio",
"wasi-cap-std-sync",
"wasi-common",
"wiggle",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.82"
@ -4590,6 +4669,7 @@ dependencies = [
"anyhow",
"wasi-cap-std-sync",
"wasi-common",
"wasi-tokio",
"wasmtime",
"wiggle",
]
@ -4677,6 +4757,7 @@ dependencies = [
"tracing",
"wasmtime",
"wiggle-macro",
"witx",
]
[[package]]

View File

@ -71,8 +71,10 @@ e2e-tests = []
[workspace]
members = [
"crates/abi-conformance",
"crates/app",
"crates/build",
"crates/config",
"crates/core",
"crates/engine",
"crates/http",
"crates/loader",

13
crates/app/Cargo.toml Normal file
View File

@ -0,0 +1,13 @@
[package]
name = "spin-app"
version = "0.1.0"
edition = "2021"
[dependencies]
anyhow = "1.0"
async-trait = "0.1"
ouroboros = "0.15"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
spin-core = { path = "../core" }
thiserror = "1.0"

View File

@ -0,0 +1,51 @@
use std::sync::Arc;
use spin_core::{EngineBuilder, HostComponent, HostComponentsData};
use crate::AppComponent;
pub trait DynamicHostComponent: HostComponent {
fn update_data(&self, data: &mut Self::Data, component: &AppComponent) -> anyhow::Result<()>;
}
impl<DHC: DynamicHostComponent> DynamicHostComponent for Arc<DHC> {
fn update_data(&self, data: &mut Self::Data, component: &AppComponent) -> anyhow::Result<()> {
(**self).update_data(data, component)
}
}
type DataUpdater =
Box<dyn Fn(&mut HostComponentsData, &AppComponent) -> anyhow::Result<()> + Send + Sync>;
#[derive(Default)]
pub struct DynamicHostComponents {
data_updaters: Vec<DataUpdater>,
}
impl DynamicHostComponents {
pub fn add_dynamic_host_component<T: Send + Sync, DHC: DynamicHostComponent>(
&mut self,
engine_builder: &mut EngineBuilder<T>,
host_component: DHC,
) -> anyhow::Result<()> {
let host_component = Arc::new(host_component);
let handle = engine_builder.add_host_component(host_component.clone())?;
self.data_updaters
.push(Box::new(move |host_components_data, component| {
let data = host_components_data.get_or_insert(handle);
host_component.update_data(data, component)
}));
Ok(())
}
pub fn update_data(
&self,
host_components_data: &mut HostComponentsData,
component: &AppComponent,
) -> anyhow::Result<()> {
for data_updater in &self.data_updaters {
data_updater(host_components_data, component)?;
}
Ok(())
}
}

268
crates/app/src/lib.rs Normal file
View File

@ -0,0 +1,268 @@
mod host_component;
pub mod locked;
pub mod values;
use ouroboros::self_referencing;
use serde::Deserialize;
use spin_core::{wasmtime, Engine, EngineBuilder, StoreBuilder};
use host_component::DynamicHostComponents;
use locked::{ContentPath, LockedApp, LockedComponent, LockedComponentSource, LockedTrigger};
pub use async_trait::async_trait;
pub use host_component::DynamicHostComponent;
pub use locked::Variable;
// TODO(lann): Should this migrate to spin-loader?
#[async_trait]
pub trait Loader {
async fn load_app(&self, uri: &str) -> anyhow::Result<LockedApp>;
async fn load_module(
&self,
engine: &wasmtime::Engine,
source: &LockedComponentSource,
) -> anyhow::Result<spin_core::Module>;
async fn mount_files(
&self,
store_builder: &mut StoreBuilder,
component: &AppComponent,
) -> anyhow::Result<()>;
}
pub struct AppLoader {
inner: Box<dyn Loader + Send + Sync>,
dynamic_host_components: DynamicHostComponents,
}
impl AppLoader {
pub fn new(loader: impl Loader + Send + Sync + 'static) -> Self {
Self {
inner: Box::new(loader),
dynamic_host_components: Default::default(),
}
}
pub fn add_dynamic_host_component<T: Send + Sync, DHC: DynamicHostComponent>(
&mut self,
engine_builder: &mut EngineBuilder<T>,
host_component: DHC,
) -> anyhow::Result<()> {
self.dynamic_host_components
.add_dynamic_host_component(engine_builder, host_component)
}
pub async fn load_app(&self, uri: String) -> Result<App> {
let locked = self
.inner
.load_app(&uri)
.await
.map_err(Error::LoaderError)?;
Ok(App {
loader: self,
uri,
locked,
})
}
pub async fn load_owned_app(self, uri: String) -> Result<OwnedApp> {
OwnedApp::try_new_async(self, |loader| Box::pin(loader.load_app(uri))).await
}
}
impl std::fmt::Debug for AppLoader {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AppLoader").finish()
}
}
#[self_referencing]
#[derive(Debug)]
pub struct OwnedApp {
loader: AppLoader,
#[borrows(loader)]
#[covariant]
app: App<'this>,
}
impl std::ops::Deref for OwnedApp {
type Target = App<'static>;
fn deref(&self) -> &Self::Target {
unsafe {
// We know that App's lifetime param is for AppLoader, which is owned by self here.
std::mem::transmute::<&App, &App<'static>>(self.borrow_app())
}
}
}
#[derive(Debug)]
pub struct App<'a> {
loader: &'a AppLoader,
uri: String,
locked: LockedApp,
}
impl<'a> App<'a> {
pub fn uri(&self) -> &str {
&self.uri
}
pub fn get_metadata<'this, T: Deserialize<'this>>(&'this self, key: &str) -> Option<Result<T>> {
self.locked
.metadata
.get(key)
.map(|value| Ok(T::deserialize(value)?))
}
pub fn require_metadata<'this, T: Deserialize<'this>>(&'this self, key: &str) -> Result<T> {
self.get_metadata(key)
.ok_or_else(|| Error::ManifestError(format!("missing required {key:?}")))?
}
pub fn variables(&self) -> impl Iterator<Item = (&String, &Variable)> {
self.locked.variables.iter()
}
pub fn components(&self) -> impl Iterator<Item = AppComponent> {
self.locked
.components
.iter()
.map(|locked| AppComponent { app: self, locked })
}
pub fn get_component(&self, component_id: &str) -> Option<AppComponent> {
self.components()
.find(|component| component.locked.id == component_id)
}
pub fn triggers(&self) -> impl Iterator<Item = AppTrigger> {
self.locked
.triggers
.iter()
.map(|locked| AppTrigger { app: self, locked })
}
pub fn triggers_with_type(&'a self, trigger_type: &'a str) -> impl Iterator<Item = AppTrigger> {
self.triggers()
.filter(move |trigger| trigger.locked.trigger_type == trigger_type)
}
}
pub struct AppComponent<'a> {
pub app: &'a App<'a>,
locked: &'a LockedComponent,
}
impl<'a> AppComponent<'a> {
pub fn id(&self) -> &str {
&self.locked.id
}
pub fn source(&self) -> &LockedComponentSource {
&self.locked.source
}
pub fn files(&self) -> std::slice::Iter<ContentPath> {
self.locked.files.iter()
}
pub fn get_metadata<T: Deserialize<'a>>(&self, key: &str) -> Option<Result<T>> {
self.locked
.metadata
.get(key)
.map(|value| Ok(T::deserialize(value)?))
}
pub fn config(&self) -> impl Iterator<Item = (&String, &String)> {
self.locked.config.iter()
}
pub async fn load_module<T: Send + Sync>(
&self,
engine: &Engine<T>,
) -> Result<spin_core::Module> {
self.app
.loader
.inner
.load_module(engine.as_ref(), &self.locked.source)
.await
.map_err(Error::LoaderError)
}
pub async fn apply_store_config(&self, builder: &mut StoreBuilder) -> Result<()> {
builder.env(&self.locked.env).map_err(Error::CoreError)?;
let loader = self.app.loader;
loader
.inner
.mount_files(builder, self)
.await
.map_err(Error::LoaderError)?;
loader
.dynamic_host_components
.update_data(builder.host_components_data(), self)
.map_err(Error::HostComponentError)?;
Ok(())
}
}
pub struct AppTrigger<'a> {
pub app: &'a App<'a>,
locked: &'a LockedTrigger,
}
impl<'a> AppTrigger<'a> {
pub fn id(&self) -> &str {
&self.locked.id
}
pub fn trigger_type(&self) -> &str {
&self.locked.trigger_type
}
pub fn component(&self) -> Result<AppComponent<'a>> {
let component_id = self.locked.trigger_config.get("component").ok_or_else(|| {
Error::ManifestError(format!(
"trigger {:?} missing 'component' config field",
self.locked.id
))
})?;
let component_id = component_id.as_str().ok_or_else(|| {
Error::ManifestError(format!(
"trigger {:?} 'component' field has unexpected value {:?}",
self.locked.id, component_id
))
})?;
self.app.get_component(component_id).ok_or_else(|| {
Error::ManifestError(format!(
"missing component {:?} configured for trigger {:?}",
component_id, self.locked.id
))
})
}
pub fn typed_config<Config: Deserialize<'a>>(&self) -> Result<Config> {
Ok(Config::deserialize(&self.locked.trigger_config)?)
}
}
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("spin core error: {0}")]
CoreError(anyhow::Error),
#[error("host component error: {0}")]
HostComponentError(anyhow::Error),
#[error("loader error: {0}")]
LoaderError(anyhow::Error),
#[error("manifest error: {0}")]
ManifestError(String),
#[error("json error: {0}")]
JsonError(#[from] serde_json::Error),
}

154
crates/app/src/locked.rs Normal file
View File

@ -0,0 +1,154 @@
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::values::ValuesMap;
// LockedMap gives deterministic encoding, which we want.
pub type LockedMap<T> = std::collections::BTreeMap<String, T>;
/// A LockedApp represents a "fully resolved" Spin application.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct LockedApp {
/// Locked schema version
pub spin_lock_version: FixedVersion<0>,
/// Application metadata
#[serde(default, skip_serializing_if = "ValuesMap::is_empty")]
pub metadata: ValuesMap,
/// Custom config variables
#[serde(default, skip_serializing_if = "LockedMap::is_empty")]
pub variables: LockedMap<Variable>,
/// Application triggers
pub triggers: Vec<LockedTrigger>,
/// Application components
pub components: Vec<LockedComponent>,
}
impl LockedApp {
pub fn from_json(contents: &[u8]) -> serde_json::Result<Self> {
serde_json::from_slice(contents)
}
pub fn to_json(&self) -> serde_json::Result<Vec<u8>> {
serde_json::to_vec_pretty(&self)
}
}
/// A LockedComponent represents a "fully resolved" Spin component.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct LockedComponent {
/// Application-unique component identifier
pub id: String,
/// Component metadata
#[serde(default, skip_serializing_if = "ValuesMap::is_empty")]
pub metadata: ValuesMap,
/// Wasm source
pub source: LockedComponentSource,
/// WASI environment variables
#[serde(default, skip_serializing_if = "LockedMap::is_empty")]
pub env: LockedMap<String>,
/// WASI filesystem contents
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub files: Vec<ContentPath>,
/// Custom config values
#[serde(default, skip_serializing_if = "LockedMap::is_empty")]
pub config: LockedMap<String>,
}
/// A LockedComponentSource specifies a Wasm source.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct LockedComponentSource {
/// Wasm source content type (e.g. "application/wasm")
pub content_type: String,
/// Wasm source content specification
#[serde(flatten)]
pub content: ContentRef,
}
/// A ContentPath specifies content mapped to a WASI path.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ContentPath {
/// Content specification
#[serde(flatten)]
pub content: ContentRef,
/// WASI mount path
pub path: PathBuf,
}
/// A ContentRef represents content used by an application.
///
/// At least one of `source` or `digest` must be specified. Implementations may
/// require one or the other (or both).
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct ContentRef {
/// A URI where the content can be accessed. Implementations may support
/// different URI schemes.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source: Option<String>,
/// If set, the content must have the given SHA-256 digest.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub digest: Option<String>,
}
/// A LockedTrigger specifies configuration for an application trigger.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct LockedTrigger {
/// Application-unique trigger identifier
pub id: String,
/// Trigger type (e.g. "http")
pub trigger_type: String,
/// Trigger-type-specific configuration
pub trigger_config: Value,
}
/// A Variable specifies a custom configuration variable.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Variable {
/// The variable's default value. If unset, the variable is required.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default: Option<String>,
/// If set, the variable's value may be sensitive and e.g. shouldn't be logged.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub secret: bool,
}
/// FixedVersion represents a schema version field with a const value.
#[allow(unused)]
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(into = "usize", try_from = "usize")]
pub struct FixedVersion<const V: usize>;
impl<const V: usize> From<FixedVersion<V>> for usize {
fn from(_: FixedVersion<V>) -> usize {
V
}
}
impl<const V: usize> From<FixedVersion<V>> for String {
fn from(_: FixedVersion<V>) -> String {
V.to_string()
}
}
impl<const V: usize> TryFrom<usize> for FixedVersion<V> {
type Error = String;
fn try_from(value: usize) -> Result<Self, Self::Error> {
if value != V {
return Err(format!("invalid version {} != {}", value, V));
}
Ok(Self)
}
}
impl<const V: usize> TryFrom<String> for FixedVersion<V> {
type Error = String;
fn try_from(value: String) -> Result<Self, Self::Error> {
let value: usize = value
.parse()
.map_err(|err| format!("invalid version: {}", err))?;
value.try_into()
}
}

57
crates/app/src/values.rs Normal file
View File

@ -0,0 +1,57 @@
use serde::Serialize;
use serde_json::Value;
// ValuesMap stores dynamically-typed values.
pub type ValuesMap = serde_json::Map<String, Value>;
/// ValuesMapBuilder assists in building a ValuesMap.
#[derive(Default)]
pub struct ValuesMapBuilder(ValuesMap);
impl ValuesMapBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn string(&mut self, key: impl Into<String>, value: impl Into<String>) -> &mut Self {
self.entry(key, value.into())
}
pub fn string_option(
&mut self,
key: impl Into<String>,
value: Option<impl Into<String>>,
) -> &mut Self {
if let Some(value) = value {
self.0.insert(key.into(), value.into().into());
}
self
}
pub fn string_array<T: Into<String>>(
&mut self,
key: impl Into<String>,
iter: impl IntoIterator<Item = T>,
) -> &mut Self {
self.entry(key, iter.into_iter().map(|s| s.into()).collect::<Vec<_>>())
}
pub fn entry(&mut self, key: impl Into<String>, value: impl Into<Value>) -> &mut Self {
self.0.insert(key.into(), value.into());
self
}
pub fn serializable(
&mut self,
key: impl Into<String>,
value: impl Serialize,
) -> serde_json::Result<&mut Self> {
let value = serde_json::to_value(value)?;
self.0.insert(key.into(), value);
Ok(self)
}
pub fn build(&mut self) -> ValuesMap {
std::mem::take(&mut self.0)
}
}

13
crates/core/Cargo.toml Normal file
View File

@ -0,0 +1,13 @@
[package]
name = "spin-core"
version = "0.1.0"
edition = "2021"
[dependencies]
anyhow = "1.0"
tracing = "0.1"
async-trait = "0.1"
wasi-cap-std-sync = "0.39"
wasi-common = "0.39"
wasmtime = "0.39"
wasmtime-wasi = { version = "0.39", features = ["tokio"] }

View File

@ -0,0 +1,129 @@
use std::{any::Any, marker::PhantomData, sync::Arc};
use anyhow::Result;
use super::{Data, Linker};
pub trait HostComponent: Send + Sync + 'static {
/// Host component runtime data.
type Data: Send + Sized + 'static;
/// Add this component to the given Linker, using the given runtime state-getting handle.
// This function signature mirrors those generated by wit-bindgen.
fn add_to_linker<T: Send>(
linker: &mut Linker<T>,
get: impl Fn(&mut Data<T>) -> &mut Self::Data + Send + Sync + Copy + 'static,
) -> Result<()>;
fn build_data(&self) -> Self::Data;
}
impl<HC: HostComponent> HostComponent for Arc<HC> {
type Data = HC::Data;
fn add_to_linker<T: Send>(
linker: &mut Linker<T>,
get: impl Fn(&mut Data<T>) -> &mut Self::Data + Send + Sync + Copy + 'static,
) -> Result<()> {
HC::add_to_linker(linker, get)
}
fn build_data(&self) -> Self::Data {
(**self).build_data()
}
}
pub struct HostComponentDataHandle<HC: HostComponent> {
idx: usize,
_phantom: PhantomData<fn() -> HC::Data>,
}
impl<HC: HostComponent> Copy for HostComponentDataHandle<HC> {}
impl<HC: HostComponent> Clone for HostComponentDataHandle<HC> {
fn clone(&self) -> Self {
Self {
idx: self.idx,
_phantom: PhantomData,
}
}
}
type DataBuilder = Box<dyn Fn() -> Box<dyn Any + Send> + Send + Sync>;
pub struct HostComponentsBuilder {
data_builders: Vec<DataBuilder>,
}
impl HostComponentsBuilder {
pub fn add_host_component<T: Send, HC: HostComponent + 'static>(
&mut self,
linker: &mut Linker<T>,
host_component: HC,
) -> Result<HostComponentDataHandle<HC>> {
let idx = self.data_builders.len();
self.data_builders
.push(Box::new(move || Box::new(host_component.build_data())));
HC::add_to_linker(linker, move |data| {
data.host_components_data
.get_or_insert_idx(idx)
.downcast_mut()
.unwrap()
})?;
Ok(HostComponentDataHandle::<HC> {
idx,
_phantom: PhantomData,
})
}
pub fn build(self) -> HostComponents {
let data_builders = Arc::new(self.data_builders);
HostComponents { data_builders }
}
}
pub struct HostComponents {
data_builders: Arc<Vec<DataBuilder>>,
}
impl HostComponents {
pub fn builder() -> HostComponentsBuilder {
HostComponentsBuilder {
data_builders: Default::default(),
}
}
pub fn new_data(&self) -> HostComponentsData {
// Fill with `None`
let data = std::iter::repeat_with(Default::default)
.take(self.data_builders.len())
.collect();
HostComponentsData {
data,
data_builders: self.data_builders.clone(),
}
}
}
pub struct HostComponentsData {
data: Vec<Option<Box<dyn Any + Send>>>,
data_builders: Arc<Vec<DataBuilder>>,
}
impl HostComponentsData {
pub fn get_or_insert<HC: HostComponent>(
&mut self,
handle: HostComponentDataHandle<HC>,
) -> &mut HC::Data {
let x = self.get_or_insert_idx(handle.idx);
x.downcast_mut().unwrap()
}
fn get_or_insert_idx(&mut self, idx: usize) -> &mut Box<dyn Any + Send> {
self.data[idx].get_or_insert_with(|| self.data_builders[idx]())
}
pub fn set<HC: HostComponent>(&mut self, handle: HostComponentDataHandle<HC>, data: HC::Data) {
self.data[handle.idx] = Some(Box::new(data));
}
}

16
crates/core/src/io.rs Normal file
View File

@ -0,0 +1,16 @@
use std::sync::{Arc, RwLock};
use wasi_common::pipe::WritePipe;
#[derive(Default)]
pub struct OutputBuffer(Arc<RwLock<Vec<u8>>>);
impl OutputBuffer {
pub fn take(&mut self) -> Vec<u8> {
std::mem::take(&mut *self.0.write().unwrap())
}
pub(crate) fn writer(&self) -> WritePipe<Vec<u8>> {
WritePipe::from_shared(self.0.clone())
}
}

173
crates/core/src/lib.rs Normal file
View File

@ -0,0 +1,173 @@
mod host_component;
mod io;
mod limits;
mod store;
use std::sync::{Arc, Mutex};
use anyhow::Result;
use tracing::instrument;
use wasmtime_wasi::WasiCtx;
pub use wasmtime::{self, Instance, Module};
use self::host_component::{HostComponents, HostComponentsBuilder};
pub use host_component::{HostComponent, HostComponentDataHandle, HostComponentsData};
pub use store::{Store, StoreBuilder};
pub struct Config {
inner: wasmtime::Config,
}
impl Config {
/// Borrow the inner wasmtime::Config mutably.
/// WARNING: This is inherently unstable and may break at any time!
#[doc(hidden)]
pub fn wasmtime_config(&mut self) -> &mut wasmtime::Config {
&mut self.inner
}
}
impl Default for Config {
fn default() -> Self {
let mut inner = wasmtime::Config::new();
inner.async_support(true);
Self { inner }
}
}
pub struct Data<T> {
inner: T,
wasi: WasiCtx,
host_components_data: HostComponentsData,
store_limits: limits::StoreLimitsAsync,
}
impl<T> AsRef<T> for Data<T> {
fn as_ref(&self) -> &T {
&self.inner
}
}
impl<T> AsMut<T> for Data<T> {
fn as_mut(&mut self) -> &mut T {
&mut self.inner
}
}
pub type Linker<T> = wasmtime::Linker<Data<T>>;
pub struct EngineBuilder<T> {
engine: wasmtime::Engine,
linker: Linker<T>,
host_components_builder: HostComponentsBuilder,
}
impl<T: Send + Sync> EngineBuilder<T> {
fn new(config: &Config) -> Result<Self> {
let engine = wasmtime::Engine::new(&config.inner)?;
let mut linker: Linker<T> = Linker::new(&engine);
wasmtime_wasi::tokio::add_to_linker(&mut linker, |data| &mut data.wasi)?;
Ok(Self {
engine,
linker,
host_components_builder: HostComponents::builder(),
})
}
pub fn link_import(
&mut self,
f: impl FnOnce(&mut Linker<T>, fn(&mut Data<T>) -> &mut T) -> Result<()>,
) -> Result<()> {
f(&mut self.linker, Data::as_mut)
}
pub fn add_host_component<HC: HostComponent + Send + Sync + 'static>(
&mut self,
host_component: HC,
) -> Result<HostComponentDataHandle<HC>> {
self.host_components_builder
.add_host_component(&mut self.linker, host_component)
}
pub fn build_with_data(self, instance_pre_data: T) -> Engine<T> {
let host_components = self.host_components_builder.build();
let instance_pre_store = Arc::new(Mutex::new(
StoreBuilder::new(self.engine.clone(), &host_components)
.build_with_data(instance_pre_data)
.expect("instance_pre_store build should not fail"),
));
Engine {
inner: self.engine,
linker: self.linker,
host_components,
instance_pre_store,
}
}
}
impl<T: Default + Send + Sync> EngineBuilder<T> {
pub fn build(self) -> Engine<T> {
self.build_with_data(T::default())
}
}
pub struct Engine<T> {
inner: wasmtime::Engine,
linker: Linker<T>,
host_components: HostComponents,
instance_pre_store: Arc<Mutex<Store<T>>>,
}
impl<T: Send + Sync> Engine<T> {
pub fn builder(config: &Config) -> Result<EngineBuilder<T>> {
EngineBuilder::new(config)
}
pub fn store_builder(&self) -> StoreBuilder {
StoreBuilder::new(self.inner.clone(), &self.host_components)
}
#[instrument(skip_all)]
pub fn instantiate_pre(&self, module: &Module) -> Result<InstancePre<T>> {
let mut store = self.instance_pre_store.lock().unwrap();
let inner = self.linker.instantiate_pre(&mut *store, module)?;
Ok(InstancePre { inner })
}
}
impl<T> AsRef<wasmtime::Engine> for Engine<T> {
fn as_ref(&self) -> &wasmtime::Engine {
&self.inner
}
}
pub struct InstancePre<T> {
inner: wasmtime::InstancePre<Data<T>>,
}
impl<T: Send + Sync> InstancePre<T> {
#[instrument(skip_all)]
pub async fn instantiate_async(&self, store: &mut Store<T>) -> Result<Instance> {
self.inner.instantiate_async(store).await
}
}
impl<T> Clone for InstancePre<T> {
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
}
}
}
impl<T> AsRef<wasmtime::InstancePre<Data<T>>> for InstancePre<T> {
fn as_ref(&self) -> &wasmtime::InstancePre<Data<T>> {
&self.inner
}
}

35
crates/core/src/limits.rs Normal file
View File

@ -0,0 +1,35 @@
use async_trait::async_trait;
use wasmtime::ResourceLimiterAsync;
/// Async implementation of wasmtime's `StoreLimits`: https://github.com/bytecodealliance/wasmtime/blob/main/crates/wasmtime/src/limits.rs
/// Used to limit the memory use and table size of each Instance
#[derive(Default)]
pub struct StoreLimitsAsync {
max_memory_size: Option<usize>,
max_table_elements: Option<u32>,
}
#[async_trait]
impl ResourceLimiterAsync for StoreLimitsAsync {
async fn memory_growing(
&mut self,
_current: usize,
desired: usize,
_maximum: Option<usize>,
) -> bool {
!matches!(self.max_memory_size, Some(limit) if desired > limit)
}
async fn table_growing(&mut self, _current: u32, desired: u32, _maximum: Option<u32>) -> bool {
!matches!(self.max_table_elements, Some(limit) if desired > limit)
}
}
impl StoreLimitsAsync {
pub fn new(max_memory_size: Option<usize>, max_table_elements: Option<u32>) -> Self {
Self {
max_memory_size,
max_table_elements,
}
}
}

234
crates/core/src/store.rs Normal file
View File

@ -0,0 +1,234 @@
use anyhow::{anyhow, Result};
use std::{
io::{Read, Write},
path::{Path, PathBuf},
};
use wasi_cap_std_sync::{ambient_authority, Dir};
use wasi_common::{dir::DirCaps, pipe::WritePipe, WasiFile};
use wasi_common::{file::FileCaps, pipe::ReadPipe};
use wasmtime_wasi::tokio::WasiCtxBuilder;
use crate::io::OutputBuffer;
use super::{
host_component::{HostComponents, HostComponentsData},
limits::StoreLimitsAsync,
Data,
};
pub struct Store<T> {
inner: wasmtime::Store<Data<T>>,
}
impl<T> Store<T> {
pub fn host_components_data(&mut self) -> &mut HostComponentsData {
&mut self.inner.data_mut().host_components_data
}
}
impl<T> AsRef<wasmtime::Store<Data<T>>> for Store<T> {
fn as_ref(&self) -> &wasmtime::Store<Data<T>> {
&self.inner
}
}
impl<T> AsMut<wasmtime::Store<Data<T>>> for Store<T> {
fn as_mut(&mut self) -> &mut wasmtime::Store<Data<T>> {
&mut self.inner
}
}
impl<T> wasmtime::AsContext for Store<T> {
type Data = Data<T>;
fn as_context(&self) -> wasmtime::StoreContext<'_, Self::Data> {
self.inner.as_context()
}
}
impl<T> wasmtime::AsContextMut for Store<T> {
fn as_context_mut(&mut self) -> wasmtime::StoreContextMut<'_, Self::Data> {
self.inner.as_context_mut()
}
}
// WASI expects preopened dirs in FDs starting at 3 (0-2 are stdio).
const WASI_FIRST_PREOPENED_DIR_FD: u32 = 3;
const READ_ONLY_DIR_CAPS: DirCaps = DirCaps::from_bits_truncate(
DirCaps::OPEN.bits()
| DirCaps::READDIR.bits()
| DirCaps::READLINK.bits()
| DirCaps::PATH_FILESTAT_GET.bits()
| DirCaps::FILESTAT_GET.bits(),
);
const READ_ONLY_FILE_CAPS: FileCaps = FileCaps::from_bits_truncate(
FileCaps::READ.bits()
| FileCaps::SEEK.bits()
| FileCaps::TELL.bits()
| FileCaps::FILESTAT_GET.bits()
| FileCaps::POLL_READWRITE.bits(),
);
pub struct StoreBuilder {
engine: wasmtime::Engine,
wasi: std::result::Result<Option<WasiCtxBuilder>, String>,
read_only_preopened_dirs: Vec<(Dir, PathBuf)>,
host_components_data: HostComponentsData,
store_limits: StoreLimitsAsync,
}
impl StoreBuilder {
pub(crate) fn new(engine: wasmtime::Engine, host_components: &HostComponents) -> Self {
Self {
engine,
wasi: Ok(Some(WasiCtxBuilder::new())),
read_only_preopened_dirs: Vec::new(),
host_components_data: host_components.new_data(),
store_limits: StoreLimitsAsync::default(),
}
}
pub fn max_memory_size(&mut self, max_memory_size: usize) {
self.store_limits = StoreLimitsAsync::new(Some(max_memory_size), None);
}
pub fn inherit_stdio(&mut self) {
self.with_wasi(|wasi| wasi.inherit_stdio());
}
pub fn stdin(&mut self, file: impl WasiFile + 'static) {
self.with_wasi(|wasi| wasi.stdin(Box::new(file)))
}
pub fn stdin_pipe(&mut self, r: impl Read + Send + Sync + 'static) {
self.stdin(ReadPipe::new(r))
}
pub fn stdout(&mut self, file: impl WasiFile + 'static) {
self.with_wasi(|wasi| wasi.stdout(Box::new(file)))
}
pub fn stdout_pipe(&mut self, w: impl Write + Send + Sync + 'static) {
self.stdout(WritePipe::new(w))
}
pub fn stdout_buffered(&mut self) -> OutputBuffer {
let buffer = OutputBuffer::default();
self.stdout(buffer.writer());
buffer
}
pub fn stderr(&mut self, file: impl WasiFile + 'static) {
self.with_wasi(|wasi| wasi.stderr(Box::new(file)))
}
pub fn stderr_pipe(&mut self, w: impl Write + Send + Sync + 'static) {
self.stderr(WritePipe::new(w))
}
pub fn stderr_buffered(&mut self) -> OutputBuffer {
let buffer = OutputBuffer::default();
self.stderr(buffer.writer());
buffer
}
pub fn args<'b>(&mut self, args: impl IntoIterator<Item = &'b str>) -> Result<()> {
self.try_with_wasi(|mut wasi| {
for arg in args {
wasi = wasi.arg(arg)?;
}
Ok(wasi)
})
}
pub fn env(
&mut self,
vars: impl IntoIterator<Item = (impl AsRef<str>, impl AsRef<str>)>,
) -> Result<()> {
self.try_with_wasi(|mut wasi| {
for (k, v) in vars {
wasi = wasi.env(k.as_ref(), v.as_ref())?;
}
Ok(wasi)
})
}
pub fn read_only_preopened_dir(
&mut self,
host_path: impl AsRef<Path>,
guest_path: PathBuf,
) -> Result<()> {
// WasiCtxBuilder::preopened_dir doesn't let you set capabilities, so we need
// to wait and call WasiCtx::insert_dir after building the WasiCtx.
let dir = wasmtime_wasi::Dir::open_ambient_dir(host_path, ambient_authority())?;
self.read_only_preopened_dirs.push((dir, guest_path));
Ok(())
}
pub fn read_write_preopened_dir(
&mut self,
host_path: impl AsRef<Path>,
guest_path: PathBuf,
) -> Result<()> {
let dir = wasmtime_wasi::Dir::open_ambient_dir(host_path, ambient_authority())?;
self.try_with_wasi(|wasi| wasi.preopened_dir(dir, guest_path))
}
pub fn host_components_data(&mut self) -> &mut HostComponentsData {
&mut self.host_components_data
}
pub fn build_with_data<T>(self, inner_data: T) -> Result<Store<T>> {
let mut wasi = self.wasi.map_err(anyhow::Error::msg)?.unwrap().build();
// Insert any read-only preopened dirs
for (idx, (dir, path)) in self.read_only_preopened_dirs.into_iter().enumerate() {
let fd = WASI_FIRST_PREOPENED_DIR_FD + idx as u32;
let dir = Box::new(wasmtime_wasi::tokio::Dir::from_cap_std(dir));
wasi.insert_dir(fd, dir, READ_ONLY_DIR_CAPS, READ_ONLY_FILE_CAPS, path);
}
let mut inner = wasmtime::Store::new(
&self.engine,
Data {
inner: inner_data,
wasi,
host_components_data: self.host_components_data,
store_limits: self.store_limits,
},
);
inner.limiter_async(move |data| &mut data.store_limits);
Ok(Store { inner })
}
pub fn build<T: Default>(self) -> Result<Store<T>> {
self.build_with_data(T::default())
}
fn with_wasi(&mut self, f: impl FnOnce(WasiCtxBuilder) -> WasiCtxBuilder) {
let _ = self.try_with_wasi(|wasi| Ok(f(wasi)));
}
fn try_with_wasi(
&mut self,
f: impl FnOnce(WasiCtxBuilder) -> Result<WasiCtxBuilder>,
) -> Result<()> {
let wasi = self
.wasi
.as_mut()
.map_err(|err| anyhow!("StoreBuilder already failed: {}", err))?
.take()
.unwrap();
match f(wasi) {
Ok(wasi) => {
self.wasi = Ok(Some(wasi));
Ok(())
}
Err(err) => {
self.wasi = Err(err.to_string());
Err(err)
}
}
}
}