Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

components: Implement the ability to call component exports #4039

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions crates/environ/src/component/info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,15 +137,24 @@ pub struct LiftedFunction {
}

/// Canonical ABI options associated with a lifted function.
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CanonicalOptions {
/// The optionally-specified encoding used for strings.
pub string_encoding: Option<StringEncoding>,
/// The encoding used for strings.
pub string_encoding: StringEncoding,
/// Representation of the `into` option where intrinsics are peeled out and
/// identified from an instance.
pub intrinsics: Option<Intrinsics>,
}

impl Default for CanonicalOptions {
fn default() -> CanonicalOptions {
CanonicalOptions {
string_encoding: StringEncoding::Utf8,
intrinsics: None,
}
}
}

/// Possible encodings of strings within the component model.
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[allow(missing_docs)]
Expand Down
6 changes: 3 additions & 3 deletions crates/environ/src/component/translate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -650,13 +650,13 @@ impl<'a, 'data> Translator<'a, 'data> {
for opt in opts {
match opt {
wasmparser::CanonicalOption::UTF8 => {
ret.string_encoding = Some(StringEncoding::Utf8);
ret.string_encoding = StringEncoding::Utf8;
}
wasmparser::CanonicalOption::UTF16 => {
ret.string_encoding = Some(StringEncoding::Utf16);
ret.string_encoding = StringEncoding::Utf16;
}
wasmparser::CanonicalOption::CompactUTF16 => {
ret.string_encoding = Some(StringEncoding::CompactUtf16);
ret.string_encoding = StringEncoding::CompactUtf16;
}
wasmparser::CanonicalOption::Into(instance) => {
let instance = InstanceIndex::from_u32(*instance);
Expand Down
189 changes: 183 additions & 6 deletions crates/wasmtime/src/component/func.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
use crate::component::instance::lookup;
use crate::store::{StoreOpaque, Stored};
use crate::{AsContext, StoreContextMut};
use anyhow::{bail, Context, Result};
use std::convert::TryFrom;
use std::sync::Arc;
use wasmtime_environ::component::{
ComponentTypes, FuncTypeIndex, LiftedFunction, RuntimeInstanceIndex, StringEncoding,
};
use wasmtime_environ::PrimaryMap;
use wasmtime_runtime::{Export, ExportFunction, ExportMemory, VMTrampoline};

mod typed;
pub use self::typed::*;

/// A WebAssembly component function.
//
// FIXME: write more docs here
#[derive(Copy, Clone, Debug)]
pub struct Func(Stored<FuncData>);

#[doc(hidden)]
#[allow(dead_code)] // FIXME: remove this when fields are actually used
pub struct FuncData {
trampoline: VMTrampoline,
export: ExportFunction,
Expand All @@ -23,18 +28,15 @@ pub struct FuncData {
options: Options,
}

#[derive(Clone)]
#[allow(dead_code)] // FIXME: remove this when fields are actually used
pub(crate) struct Options {
string_encoding: Option<StringEncoding>,
string_encoding: StringEncoding,
intrinsics: Option<Intrinsics>,
}

#[derive(Clone)]
#[allow(dead_code)] // FIXME: remove this when fields are actually used
struct Intrinsics {
memory: ExportMemory,
realloc: ExportFunction,
#[allow(dead_code)] // FIXME: remove this when actually used
free: ExportFunction,
}

Expand Down Expand Up @@ -80,4 +82,179 @@ impl Func {
types: types.clone(),
}))
}

/// Attempt to cast this [`Func`] to a statically typed [`TypedFunc`] with
/// the provided `Params` and `Return`.
///
/// This function will perform a type-check at runtime that the [`Func`]
/// takes `Params` as parameters and returns `Return`. If the type-check
/// passes then a [`TypedFunc`] will be returned which can be used to invoke
/// the function in an efficient, statically-typed, and ergonomic manner.
///
/// The `Params` type parameter here is a tuple of the parameters to the
/// function. A function which takes no arguments should use `()`, a
/// function with one argument should use `(T,)`, etc.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we have impls of plain T for TypedFunc? Can we do the same here? I guess that would introduce ambiguity between a single tuple parameter and N parameters?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do have impls for plain T, yeah. But yes as you deduced the ambiguity prevents the impl from existing for interface types.

///
/// The `Return` type parameter is the return value of this function. A
/// return value of `()` means that there's no return (similar to a Rust
/// unit return) and otherwise a type `T` can be specified.
///
/// Types specified here are mainly those that implement the
/// [`ComponentValue`] trait. This trait is implemented for built-in types
/// to Rust such as integer primitives, floats, `Option<T>`, `Result<T, E>`,
/// strings, and `Vec<T>`. As parameters you'll be passing native Rust
/// types.
///
/// For the `Return` type parameter many types need to be wrapped in a
/// [`Value<T>`]. For example functions which return a string should use the
/// `Return` type parameter as `Value<String>` instead of a bare `String`.
/// The usage of [`Value`] indicates that a type is stored in linear memory.
//
// FIXME: Having to remember when to use `Value<T>` vs `T` is going to trip
// people up using this API. It's not clear, though, how to fix that.
Comment on lines +108 to +114
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe easier for folks to understand if we talk about how scalars don't use Value vs compound types do use Value? I think they are the same, right? Unless the canonical ABI packs, e.g., an interface type (tuple u32 u32) into a Wasm i64 return.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately this isn't 100% accurate advice in the sense that (tuple u32) is returned by-value. No fancy bit-packing happens but if the number of return values is small enough (e.g. 1) then it's returned by-value.

///
/// # Errors
///
/// If the function does not actually take `Params` as its parameters or
/// return `Return` then an error will be returned.
///
/// # Panics
///
/// This function will panic if `self` is not owned by the `store`
/// specified.
///
/// # Examples
///
/// Calling a function which takes no parameters and has no return value:
///
/// ```
/// # use wasmtime::component::Func;
/// # use wasmtime::Store;
/// # fn foo(func: &Func, store: &mut Store<()>) -> anyhow::Result<()> {
/// let typed = func.typed::<(), (), _>(&store)?;
/// typed.call(store, ())?;
/// # Ok(())
/// # }
/// ```
///
/// Calling a function which takes one string parameter and returns a
/// string:
///
/// ```
/// # use wasmtime::component::{Func, Value};
/// # use wasmtime::Store;
/// # fn foo(func: &Func, mut store: Store<()>) -> anyhow::Result<()> {
/// let typed = func.typed::<(&str,), Value<String>, _>(&store)?;
/// let ret = typed.call(&mut store, ("Hello, ",))?;
/// let ret = ret.cursor(&store);
/// println!("returned string was: {}", ret.to_str()?);
/// # Ok(())
/// # }
/// ```
///
/// Calling a function which takes multiple parameters and returns a boolean:
///
/// ```
/// # use wasmtime::component::Func;
/// # use wasmtime::Store;
/// # fn foo(func: &Func, mut store: Store<()>) -> anyhow::Result<()> {
/// let typed = func.typed::<(u32, Option<&str>, &[u8]), bool, _>(&store)?;
/// let ok: bool = typed.call(&mut store, (1, Some("hello"), b"bytes!"))?;
/// println!("return value was: {ok}");
/// # Ok(())
/// # }
/// ```
pub fn typed<Params, Return, S>(&self, store: S) -> Result<TypedFunc<Params, Return>>
where
Params: ComponentParams,
Return: ComponentReturn,
S: AsContext,
{
self.typecheck::<Params, Return>(store.as_context().0)?;
unsafe { Ok(TypedFunc::new_unchecked(*self)) }
}

fn typecheck<Params, Return>(&self, store: &StoreOpaque) -> Result<()>
where
Params: ComponentParams,
Return: ComponentReturn,
{
let data = &store[self.0];
let ty = &data.types[data.ty];

Params::typecheck(&ty.params, &data.types).context("type mismatch with parameters")?;
Return::typecheck(&ty.result, &data.types).context("type mismatch with result")?;

Ok(())
}

fn realloc<'a, T>(
&self,
store: &'a mut StoreContextMut<'_, T>,
old: usize,
old_size: usize,
old_align: u32,
new_size: usize,
) -> Result<(&'a mut [u8], usize)> {
let (realloc, memory) = match &store.0[self.0].options.intrinsics {
Some(Intrinsics {
memory, realloc, ..
}) => (realloc.clone(), memory.clone()),
None => unreachable!(),
};

// Invoke the wasm malloc function using its raw and statically known
// signature.
let result = unsafe {
// FIXME: needs memory64 support
assert!(!memory.memory.memory.memory64);
usize::try_from(crate::TypedFunc::<(u32, u32, u32, u32), u32>::call_raw(
alexcrichton marked this conversation as resolved.
Show resolved Hide resolved
store,
realloc.anyfunc,
(
u32::try_from(old)?,
u32::try_from(old_size)?,
old_align,
u32::try_from(new_size)?,
),
)?)?
};

let memory = self.memory_mut(store.0);

let result_slice = match memory.get_mut(result..).and_then(|s| s.get_mut(..new_size)) {
Some(end) => end,
None => bail!("realloc return: beyond end of memory"),
};

Ok((result_slice, result))
}

/// Asserts that this function has an associated memory attached to it and
/// then returns the slice of memory tied to the lifetime of the provided
/// store.
fn memory<'a>(&self, store: &'a StoreOpaque) -> &'a [u8] {
let memory = match &store[self.0].options.intrinsics {
Some(Intrinsics { memory, .. }) => memory,
None => unreachable!(),
};

unsafe {
let memory = &*memory.definition;
std::slice::from_raw_parts(memory.base, memory.current_length)
}
}

/// Same as above, just `_mut`
fn memory_mut<'a>(&self, store: &'a mut StoreOpaque) -> &'a mut [u8] {
let memory = match &store[self.0].options.intrinsics {
Some(Intrinsics { memory, .. }) => memory.clone(),
None => unreachable!(),
};

unsafe {
let memory = &*memory.definition;
std::slice::from_raw_parts_mut(memory.base, memory.current_length)
}
}
}
Loading