Skip to content

Commit

Permalink
Support calling component exports
Browse files Browse the repository at this point in the history
  • Loading branch information
jbourassa committed Oct 16, 2024
1 parent 0506eaa commit 21cf524
Show file tree
Hide file tree
Showing 8 changed files with 439 additions and 12 deletions.
20 changes: 20 additions & 0 deletions bench/component_id.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
require_relative "bench"

Bench.ips do |x|
engine = Wasmtime::Engine.new
linker = Wasmtime::Component::Linker.new(engine)
store = Wasmtime::Store.new(engine)
component = Wasmtime::Component::Component.from_file(engine, "spec/fixtures/component_types.wasm")
store = Wasmtime::Store.new(engine)
instance = linker.instantiate(store, component)

point_record = {"x" => 1, "y" => 2}

x.report("identity point record") do
instance.invoke("id-record", point_record)
end

x.report("identity u32") do
instance.invoke("id-u32", 10)
end
end
3 changes: 3 additions & 0 deletions ext/src/ruby_api/component.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
mod convert;
mod func;
mod instance;
mod linker;

Expand All @@ -6,6 +8,7 @@ use magnus::{class, function, method, r_string::RString, Error, Module, Object,
use rb_sys::tracking_allocator::ManuallyTracked;
use wasmtime::component::Component as ComponentImpl;

pub use func::Func;
pub use instance::Instance;

use crate::{
Expand Down
178 changes: 178 additions & 0 deletions ext/src/ruby_api/component/convert.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
use crate::not_implemented;
use crate::ruby_api::errors::ExceptionMessage;
use crate::ruby_api::{convert::ToRubyValue, store::StoreContextValue};
use crate::{err, error, helpers::SymbolEnum};
use magnus::exception::type_error;
use magnus::rb_sys::AsRawValue;
use magnus::value::{qtrue, ReprValue};
use magnus::{
prelude::*, value, Error, IntoValue, RArray, RHash, RString, Ruby, Symbol, TryConvert,
TypedData, Value,
};
use wasmtime::component::{Type, Val};

impl ToRubyValue for Val {
fn to_ruby_value(&self, store: &StoreContextValue) -> Result<Value, Error> {
match self {
Val::Bool(bool) => Ok(bool.into_value()),
Val::S8(n) => Ok(n.into_value()),
Val::U8(n) => Ok(n.into_value()),
Val::S16(n) => Ok(n.into_value()),
Val::U16(n) => Ok(n.into_value()),
Val::S32(n) => Ok(n.into_value()),
Val::U32(n) => Ok(n.into_value()),
Val::S64(n) => Ok(n.into_value()),
Val::U64(n) => Ok(n.into_value()),
Val::Float32(n) => Ok(n.into_value()),
Val::Float64(n) => Ok(n.into_value()),
Val::Char(c) => Ok(c.into_value()),
Val::String(s) => Ok(s.as_str().into_value()),
Val::List(vec) => {
let array = RArray::with_capacity(vec.len());
for val in vec {
array.push(val.to_ruby_value(store)?)?;
}
Ok(array.into_value())
}
Val::Record(fields) => {
let hash = RHash::new();
for (name, component_val) in fields {
let ruby_value = component_val
.to_ruby_value(store)
.map_err(|e| e.append(format!(" (struct field \"{}\")", name)))?;
hash.aset(name.as_str(), ruby_value)?
}

Ok(hash.into_value())
}
Val::Tuple(vec) => {
let array = RArray::with_capacity(vec.len());
for val in vec {
array.push(val.to_ruby_value(store)?)?;
}
Ok(array.into_value())
}
Val::Variant(_kind, _val) => not_implemented!("Variant not implemented"),
Val::Enum(kind) => Ok(kind.as_str().into_value()),
Val::Option(val) => match val {
Some(val) => Ok(val.to_ruby_value(store)?),
None => Ok(value::qnil().as_value()),
},
Val::Result(_val) => not_implemented!("Result not implemented"),
Val::Flags(_vec) => not_implemented!("Flags not implemented"),
Val::Resource(_resource_any) => not_implemented!("Resource not implemented"),
}
}
}

pub trait ToComponentVal {
fn to_component_val(&self, store: &StoreContextValue, ty: &Type) -> Result<Val, Error>;
}

impl ToComponentVal for Value {
fn to_component_val(&self, store: &StoreContextValue, ty: &Type) -> Result<Val, Error> {
match ty {
Type::Bool => {
let ruby = Ruby::get().unwrap();
if self.as_raw() == ruby.qtrue().as_raw() {
Ok(Val::Bool(true))
} else if self.as_raw() == ruby.qfalse().as_raw() {
Ok(Val::Bool(false))
} else {
Err(Error::new(
type_error(),
// SAFETY: format will copy classname directly, before we call back in to Ruby
format!("no implicit conversion of {} into boolean", unsafe {
self.classname()
}),
))
}
}
Type::S8 => Ok(Val::S8(i8::try_convert(*self)?.into())),
Type::U8 => Ok(Val::U8(u8::try_convert(*self)?.into())),
Type::S16 => Ok(Val::S16(i16::try_convert(*self)?.into())),
Type::U16 => Ok(Val::U16(u16::try_convert(*self)?.into())),
Type::S32 => Ok(Val::S32(i32::try_convert(*self)?.into())),
Type::U32 => Ok(Val::U32(u32::try_convert(*self)?.into())),
Type::S64 => Ok(Val::S64(i64::try_convert(*self)?.into())),
Type::U64 => Ok(Val::U64(u64::try_convert(*self)?.into())),
Type::Float32 => Ok(Val::Float32(f32::try_convert(*self)?.into())),
Type::Float64 => Ok(Val::Float64(f64::try_convert(*self)?.into())),
Type::Char => Ok(Val::Char(self.to_r_string()?.to_char()?)),
Type::String => Ok(Val::String(RString::try_convert(*self)?.to_string()?)),
Type::List(list) => {
let ty = list.ty();
let rarray = RArray::try_convert(*self)?;
let mut vals: Vec<Val> = Vec::with_capacity(rarray.len());
// SAFETY: we don't mutate the RArray and we don't call into
// user code so user code can't mutate it either.
for (i, value) in unsafe { rarray.as_slice() }.iter().enumerate() {
let component_val = value
.to_component_val(store, &ty)
.map_err(|e| e.append(format!(" (list item at index {})", i)))?;

vals.push(component_val);
}
Ok(Val::List(vals))
}
Type::Record(record) => {
let hash = RHash::try_convert(*self)
.map_err(|_| error!("Invalid value for record: {}", self.inspect()))?;

let mut kv = Vec::with_capacity(record.fields().len());
for field in record.fields() {
let value = hash
.aref::<_, Value>(field.name)
.map_err(|_| error!("Struct field missing: {}", field.name))?
.to_component_val(store, &field.ty)
.map_err(|e| e.append(format!(" (struct field \"{}\")", field.name)))?;

kv.push((field.name.to_string(), value))
}
Ok(Val::Record(kv))
}
Type::Tuple(tuple) => {
let types = tuple.types();
let rarray = RArray::try_convert(*self)?;

if types.len() != rarray.len() {
return Err(Error::new(
magnus::exception::type_error(),
format!(
"invalid array length for tuple (given {}, expected {})",
rarray.len(),
types.len()
),
));
}

let mut vals: Vec<Val> = Vec::with_capacity(rarray.len());

for (i, (ty, value)) in types.zip(unsafe { rarray.as_slice() }.iter()).enumerate() {
let component_val = value
.to_component_val(store, &ty)
.map_err(|error| error.append(format!(" (tuple value at index {})", i)))?;

vals.push(component_val);
}

Ok(Val::Tuple(vals))
}
Type::Variant(_variant) => not_implemented!("Variant not implemented"),
Type::Enum(_enum) => not_implemented!("Enum not implementend"),
Type::Option(option_type) => {
if self.is_nil() {
Ok(Val::Option(None))
} else {
Ok(Val::Option(Some(Box::new(
self.to_component_val(store, &option_type.ty())?,
))))
}
}
Type::Result(_result_type) => not_implemented!("Result not implemented"),
Type::Flags(_flags) => not_implemented!("Flags not implemented"),
Type::Own(_resource_type) => not_implemented!("Resource not implemented"),
Type::Borrow(_resource_type) => not_implemented!("Resource not implemented"),
}
}
}
80 changes: 80 additions & 0 deletions ext/src/ruby_api/component/func.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
use super::convert::ToComponentVal;
use crate::{
err, error,
ruby_api::{convert::ToRubyValue, errors::ExceptionMessage, store::StoreContextValue},
};
use magnus::{
class,
error::ErrorType,
exception::arg_error,
typed_data::Obj,
value::{self, ReprValue},
DataTypeFunctions, Error, Module as _, Object, RArray, Ruby, TryConvert, TypedData, Value,
};
use magnus::{IntoValue, RModule};
use std::{borrow::BorrowMut, cell::RefCell};
use wasmtime::component::{Func as FuncImpl, Type, Val};

pub struct Func;

impl Func {
pub fn invoke(
store: &StoreContextValue,
func: &FuncImpl,
args: &[Value],
) -> Result<Value, Error> {
let results_ty = func.results(store.context()?);
let mut results = vec![wasmtime::component::Val::Bool(false); results_ty.len()];
let params = convert_params(&store, &func.params(store.context()?), args)?;

func.call(store.context_mut()?, &params, &mut results)
.map_err(|e| error!("{}", e))?;

let result = match results_ty.len() {
0 => Ok(value::qnil().as_value()),
1 => Ok(results.first().unwrap().to_ruby_value(&store)?),
_ => Ok(results
.iter()
.map(|v| v.to_ruby_value(&store))
.collect::<Result<RArray, Error>>()?
.into_value()),
};

func.post_return(store.context_mut()?)
.map_err(|e| error!("{}", e))?; // TODO: should this be a Wasmtime::Error::Trap?

result
}
}

fn convert_params(
store: &StoreContextValue,
ty: &[Type],
params_slice: &[Value],
) -> Result<Vec<Val>, Error> {
if ty.len() != params_slice.len() {
return Err(Error::new(
arg_error(),
format!(
"wrong number of arguments (given {}, expected {})",
params_slice.len(),
ty.len()
),
));
}

let mut params = Vec::with_capacity(ty.len());
for (i, (ty, value)) in ty.iter().zip(params_slice.iter()).enumerate() {
let i: u32 = i
.try_into()
.map_err(|_| Error::new(arg_error(), "too many params"))?;

let component_val = value
.to_component_val(store, ty)
.map_err(|error| error.append(format!(" (param at index {})", i)))?;

params.push(component_val);
}

Ok(params)
}
25 changes: 25 additions & 0 deletions ext/src/ruby_api/component/instance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,35 @@ impl Instance {
pub fn from_inner(store: Obj<Store>, inner: InstanceImpl) -> Self {
Self { inner, store }
}

/// @yard
/// Retrieves a Wasm function from the component instance and calls it.
///
/// @def invoke(name, *args)
/// @param name [String] The name of function to run.
/// @param (see Component::Func#call)
/// @return (see Component::Func#call)
/// @see Component::Func#call
pub fn invoke(&self, args: &[Value]) -> Result<Value, Error> {
let name = RString::try_convert(*args.first().ok_or_else(|| {
Error::new(
magnus::exception::type_error(),
"wrong number of arguments (given 0, expected 1+)",
)
})?)?;

let func = self
.inner
.get_func(self.store.context_mut(), unsafe { name.as_str()? })
.ok_or_else(|| error!("function \"{}\" not found", name))?;

Func::invoke(&self.store.into(), &func, &args[1..])
}
}

pub fn init(_ruby: &Ruby, namespace: &RModule) -> Result<(), Error> {
let instance = namespace.define_class("Instance", class::object())?;
instance.define_method("invoke", method!(Instance::invoke, -1))?;

Ok(())
}
27 changes: 26 additions & 1 deletion ext/src/ruby_api/errors.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::ruby_api::root;
use magnus::{value::Lazy, Error, ExceptionClass, Module, Ruby};
use magnus::{error::ErrorType, value::Lazy, Error, ExceptionClass, Module, Ruby};
use std::borrow::Cow;

/// Base error class for all Wasmtime errors.
pub fn base_error() -> ExceptionClass {
Expand Down Expand Up @@ -58,6 +59,30 @@ macro_rules! conversion_err {
};
}

/// Utilities for reformatting error messages
pub trait ExceptionMessage {
/// Append a message to an exception
fn append<T>(self, extra: T) -> Self
where
T: Into<Cow<'static, str>>;
}

impl ExceptionMessage for magnus::Error {
fn append<T>(self, extra: T) -> Self
where
T: Into<Cow<'static, str>>,
{
match self.error_type() {
ErrorType::Error(class, msg) => Error::new(*class, format!("{}{}", msg, extra.into())),
ErrorType::Exception(exception) => Error::new(
exception.exception_class(),
format!("{}{}", exception, extra.into()),
),
_ => self,
}
}
}

mod bundled {
include!(concat!(env!("OUT_DIR"), "/bundled/error.rs"));
}
Expand Down
13 changes: 2 additions & 11 deletions ext/src/ruby_api/params.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use super::{convert::ToWasmVal, store::StoreContextValue};
use super::{convert::ToWasmVal, errors::ExceptionMessage, store::StoreContextValue};
use magnus::{error::ErrorType, exception::arg_error, Error, Value};
use static_assertions::assert_eq_size;
use wasmtime::{FuncType, ValType};
Expand All @@ -19,16 +19,7 @@ impl Param {
fn to_wasmtime_val(&self, store: &StoreContextValue) -> Result<wasmtime::Val, Error> {
self.val
.to_wasm_val(store, self.ty.clone())
.map_err(|error| match error.error_type() {
ErrorType::Error(class, msg) => {
Error::new(*class, format!("{} (param at index {})", msg, self.index))
}
ErrorType::Exception(exception) => Error::new(
exception.exception_class(),
format!("{} (param at index {})", exception, self.index),
),
_ => error,
})
.map_err(|error| error.append(format!(" (param at index {})", self.index)))
}
}

Expand Down
Loading

0 comments on commit 21cf524

Please sign in to comment.