Skip to content
Open
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
3 changes: 3 additions & 0 deletions ext/src/ruby_api/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ mod convert;
mod func;
mod instance;
mod linker;
mod resource;
mod wasi_command;

use super::root;
Expand All @@ -20,6 +21,7 @@ use wasmtime::component::Component as ComponentImpl;
pub use func::Func;
pub use instance::Instance;
pub use linker::Linker;
pub use resource::{Resource, ResourceType};
pub use wasi_command::WasiCommand;

pub fn component_namespace(ruby: &Ruby) -> RModule {
Expand Down Expand Up @@ -169,6 +171,7 @@ pub fn init(ruby: &Ruby) -> Result<(), Error> {
linker::init(ruby, &namespace)?;
instance::init(ruby, &namespace)?;
func::init(ruby, &namespace)?;
resource::init(ruby, &namespace)?;
convert::init(ruby)?;
wasi_command::init(ruby, &namespace)?;

Expand Down
63 changes: 43 additions & 20 deletions ext/src/ruby_api/component/convert.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
use crate::ruby_api::component::component_namespace;
use crate::ruby_api::component::{component_namespace, Resource};
use crate::ruby_api::errors::ExceptionMessage;
use crate::ruby_api::store::StoreContextValue;
use crate::{define_rb_intern, err, error, not_implemented};
use magnus::rb_sys::AsRawValue;
use magnus::value::{IntoId, Lazy, ReprValue};
use magnus::{
prelude::*, try_convert, value, Error, IntoValue, RArray, RClass, RHash, RString, Ruby, Value,
prelude::*, try_convert, value, Error, IntoValue, RArray, RClass, RHash, RString, Ruby,
TryConvert, Value,
};
use wasmtime::component::{Type, Val};

Expand All @@ -25,7 +26,7 @@ define_rb_intern!(
pub(crate) fn component_val_to_rb(
ruby: &Ruby,
val: Val,
_store: Option<&StoreContextValue>,
store: Option<&StoreContextValue>,
) -> Result<Value, Error> {
match val {
Val::Bool(bool) => Ok(bool.into_value_with(ruby)),
Expand All @@ -44,14 +45,14 @@ pub(crate) fn component_val_to_rb(
Val::List(vec) => {
let array = ruby.ary_new_capa(vec.len());
for val in vec {
array.push(component_val_to_rb(ruby, val, _store)?)?;
array.push(component_val_to_rb(ruby, val, store)?)?;
}
Ok(array.into_value_with(ruby))
}
Val::Record(fields) => {
let hash = ruby.hash_new();
for (name, val) in fields {
let rb_value = component_val_to_rb(ruby, val, _store)
let rb_value = component_val_to_rb(ruby, val, store)
.map_err(|e| e.append(format!(" (struct field \"{name}\")")))?;
hash.aset(name.as_str(), rb_value)?
}
Expand All @@ -61,13 +62,13 @@ pub(crate) fn component_val_to_rb(
Val::Tuple(vec) => {
let array = ruby.ary_new_capa(vec.len());
for val in vec {
array.push(component_val_to_rb(ruby, val, _store)?)?;
array.push(component_val_to_rb(ruby, val, store)?)?;
}
Ok(array.into_value_with(ruby))
}
Val::Variant(kind, val) => {
let payload = match val {
Some(val) => component_val_to_rb(ruby, *val, _store)?,
Some(val) => component_val_to_rb(ruby, *val, store)?,
None => ruby.qnil().into_value_with(ruby),
};

Expand All @@ -78,7 +79,7 @@ pub(crate) fn component_val_to_rb(
}
Val::Enum(kind) => Ok(kind.as_str().into_value_with(ruby)),
Val::Option(val) => match val {
Some(val) => Ok(component_val_to_rb(ruby, *val, _store)?),
Some(val) => Ok(component_val_to_rb(ruby, *val, store)?),
None => Ok(ruby.qnil().as_value()),
},
Val::Result(val) => {
Expand All @@ -87,13 +88,21 @@ pub(crate) fn component_val_to_rb(
Err(val) => (ERROR.into_id_with(ruby), val),
};
let ruby_argument = match val {
Some(val) => component_val_to_rb(ruby, *val, _store)?,
Some(val) => component_val_to_rb(ruby, *val, store)?,
None => ruby.qnil().as_value(),
};
result_class(ruby).funcall(ruby_method, (ruby_argument,))
}
Val::Flags(vec) => Ok(vec.into_value_with(ruby)),
Val::Resource(_resource_any) => not_implemented!(ruby, "Resource not implemented"),
Val::Resource(resource_any) => match store.and_then(StoreContextValue::as_store) {
Some(store) => Ok(ruby
.obj_wrap(Resource::from_inner(store, resource_any))
.as_value()),
None => not_implemented!(
ruby,
"Resource not implemented for host-defined component functions"
),
},
Val::Future(_) => not_implemented!(ruby, "Future not implemented"),
Val::ErrorContext(_) => not_implemented!(ruby, "ErrorContext not implemented"),
Val::Stream(_) => not_implemented!(ruby, "Stream not implemented"),
Expand All @@ -103,7 +112,7 @@ pub(crate) fn component_val_to_rb(

pub(crate) fn rb_to_component_val(
value: Value,
_store: Option<&StoreContextValue>,
store: Option<&StoreContextValue>,
ty: &Type,
) -> Result<Val, Error> {
let ruby = Ruby::get_with(value);
Expand Down Expand Up @@ -142,7 +151,7 @@ pub(crate) fn rb_to_component_val(
// 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 = rb_to_component_val(*value, _store, &ty)
let component_val = rb_to_component_val(*value, store, &ty)
.map_err(|e| e.append(format!(" (list item at index {i})")))?;

vals.push(component_val);
Expand All @@ -158,7 +167,7 @@ pub(crate) fn rb_to_component_val(
.get(field.name)
.ok_or_else(|| error!("struct field missing: {}", field.name))
.and_then(|v| {
rb_to_component_val(v, _store, &field.ty)
rb_to_component_val(v, store, &field.ty)
.map_err(|e| e.append(format!(" (struct field \"{}\")", field.name)))
})?;

Expand All @@ -184,7 +193,7 @@ pub(crate) fn rb_to_component_val(
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 = rb_to_component_val(*value, _store, &ty)
let component_val = rb_to_component_val(*value, store, &ty)
.map_err(|error| error.append(format!(" (tuple value at index {i})")))?;

vals.push(component_val);
Expand Down Expand Up @@ -213,7 +222,7 @@ pub(crate) fn rb_to_component_val(

let payload_rb: Value = value.funcall(VALUE.into_id_with(&ruby), ())?;
let payload_val = match (&case.ty, payload_rb.is_nil()) {
(Some(ty), _) => rb_to_component_val(payload_rb, _store, ty)
(Some(ty), _) => rb_to_component_val(payload_rb, store, ty)
.map(|val| Some(Box::new(val)))
.map_err(|e| e.append(format!(" (variant value for \"{}\")", &name))),

Expand All @@ -240,7 +249,7 @@ pub(crate) fn rb_to_component_val(
} else {
Ok(Val::Option(Some(Box::new(rb_to_component_val(
value,
_store,
store,
&option_type.ty(),
)?))))
}
Expand All @@ -252,7 +261,7 @@ pub(crate) fn rb_to_component_val(
if is_ok {
let ok_value = value.funcall::<_, (), Value>(OK.into_id_with(&ruby), ())?;
match result_type.ok() {
Some(ty) => rb_to_component_val(ok_value, _store, &ty)
Some(ty) => rb_to_component_val(ok_value, store, &ty)
.map(|val| Val::Result(Result::Ok(Some(Box::new(val))))),
None => {
if ok_value.is_nil() {
Expand All @@ -268,7 +277,7 @@ pub(crate) fn rb_to_component_val(
} else {
let err_value = value.funcall::<_, (), Value>(ERROR.into_id_with(&ruby), ())?;
match result_type.err() {
Some(ty) => rb_to_component_val(err_value, _store, &ty)
Some(ty) => rb_to_component_val(err_value, store, &ty)
.map(|val| Val::Result(Result::Err(Some(Box::new(val))))),
None => {
if err_value.is_nil() {
Expand All @@ -284,8 +293,22 @@ pub(crate) fn rb_to_component_val(
}
}
Type::Flags(_) => Vec::<String>::try_convert(value).map(Val::Flags),
Type::Own(_resource_type) => not_implemented!(ruby, "Resource not implemented"),
Type::Borrow(_resource_type) => not_implemented!(ruby, "Resource not implemented"),
Type::Own(_) | Type::Borrow(_) => {
let resource = <&Resource>::try_convert(value)?;

if let Some(current_store) = store.and_then(StoreContextValue::as_store) {
if resource.store().as_value().as_raw() == current_store.as_value().as_raw() {
Ok(Val::Resource(resource.get()?))
} else {
err!("Resource belongs to a different Store than the one being called into")
}
} else {
not_implemented!(
ruby,
"Resource not implemented for host-defined component functions"
)
}
}
Type::Future(_) => not_implemented!(ruby, "Future not implemented"),
Type::Stream(_) => not_implemented!(ruby, "Stream not implemented"),
Type::ErrorContext => not_implemented!(ruby, "ErrorContext not implemented"),
Expand Down
5 changes: 4 additions & 1 deletion ext/src/ruby_api/component/func.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,10 @@ use wasmtime::component::{Func as FuncImpl, Type, Val};
/// - invalid {Variant#name},
/// - unparametrized variant and not nil {Variant#value}.
/// resource (own<T> or borrow<T>)::
/// Not yet supported.
/// {Wasmtime::Component::Resource} instance. MUST be explicitly destroyed
/// with {Wasmtime::Component::Resource#resource_drop} once no longer
/// needed — see {Wasmtime::Component::Resource}'s class documentation for
/// details.
#[derive(TypedData)]
#[magnus(class = "Wasmtime::Component::Func", size, mark, free_immediately)]
pub struct Func {
Expand Down
93 changes: 71 additions & 22 deletions ext/src/ruby_api/component/instance.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use crate::ruby_api::{component::Func, Store};
use crate::ruby_api::{
component::{Func, ResourceType},
Store,
};
use std::{borrow::BorrowMut, cell::RefCell};

use crate::error;
Expand Down Expand Up @@ -45,14 +48,32 @@ impl Instance {
/// Retrieves a Wasm function from the component instance.
///
/// @def get_func(handle)
/// @param handle [String, Array<String>] The path of the function to retrieve
/// @param handle [String, Array<String>] The name of the export to
/// retrieve, or an +Array+ of names forming a path through nested
/// exports (each name resolved within the export found by the previous
/// one).
///
/// Each name must match the export's name in the component's WIT world
/// exactly:
/// - a plain (kebab-case) export name, e.g. +"add"+ for
/// +export add: func(...)+;
/// - a fully-qualified interface name, including namespace, package and
/// any version, e.g. +"my-namespace:my-package/my-interface@0.1.0"+
/// for +export my-interface+ — the interface's own members are then
/// addressed by the next name in the path;
/// - for functions attached to a WIT +resource+, the name is mangled in
/// the canonical ABI format: +"[constructor]my-resource"+,
/// +"[method]my-resource.my-func"+ or +"[static]my-resource.my-func"+.
/// @return [Func, nil] The function if it exists, nil otherwise
///
/// @example Retrieve a top-level +add+ export:
/// instance.get_func("add")
///
/// @example Retrieve an +add+ export nested under an +adder+ instance top-level export:
/// instance.get_func(["adder", "add"])
///
/// @example Retrieve the constructor of a +counter+ resource exported by a versioned interface:
/// instance.get_func(["my-namespace:my-package/types@0.1.0", "[constructor]counter"])
pub fn get_func(rb_self: Obj<Self>, handle: Value) -> Result<Option<Func>, Error> {
let func = rb_self
.export_index(handle)?
Expand All @@ -62,6 +83,32 @@ impl Instance {
Ok(func)
}

/// @yard
/// Retrieves an exported WIT +resource+ type from the component instance.
///
/// @def get_resource(handle)
/// @param handle [String, Array<String>] The name of the resource type
/// export to retrieve, or an +Array+ of names forming a path through
/// nested exports. Names follow the same rules as {#get_func}: plain
/// kebab-case export names, or fully-qualified interface names such as
/// +"my-namespace:my-package/my-interface@0.1.0"+.
/// @return [ResourceType, nil] The resource type if it exists, nil otherwise
///
/// @example Retrieve the +wrapped-string+ resource type nested under a +resource+ export:
/// instance.get_resource(["resource", "wrapped-string"])
pub fn get_resource(rb_self: Obj<Self>, handle: Value) -> Result<Option<ResourceType>, Error> {
let resource_type = rb_self
.export_index(handle)?
.and_then(|index| {
rb_self
.inner
.get_resource(rb_self.store.context_mut(), index)
})
.map(ResourceType::from_inner);

Ok(resource_type)
}

fn export_index(&self, handle: Value) -> Result<Option<ComponentExportIndex>, Error> {
let ruby = Ruby::get_with(handle);
let invalid_arg = || {
Expand All @@ -74,34 +121,36 @@ impl Instance {
)
};

let index = if let Some(name) = RString::from_value(handle) {
self.inner
.get_export(self.store.context_mut(), None, unsafe { name.as_str()? })
.map(|(_, index)| index)
} else if let Some(names) = RArray::from_value(handle) {
unsafe { names.as_slice() }
if let Some(name) = RString::from_value(handle) {
return Ok(self
.inner
.get_export_index(self.store.context_mut(), None, unsafe { name.as_str()? }));
}

if let Some(elements) = RArray::from_value(handle) {
let index = unsafe { elements.as_slice() }
.iter()
.try_fold::<_, _, Result<_, Error>>(None, |index, name| {
let name = RString::from_value(*name).ok_or_else(invalid_arg)?;

Ok(self
.inner
.get_export(self.store.context_mut(), index.as_ref(), unsafe {
name.as_str()?
})
.map(|(_, index)| index))
})?
} else {
return Err(invalid_arg());
};
.try_fold::<_, _, Result<_, Error>>(None, |index, element| {
let name = RString::from_value(*element).ok_or_else(invalid_arg)?;

Ok(self.inner.get_export_index(
self.store.context_mut(),
index.as_ref(),
unsafe { name.as_str()? },
))
})?;

return Ok(index);
}

Ok(index)
Err(invalid_arg())
}
}

pub fn init(ruby: &Ruby, namespace: &RModule) -> Result<(), Error> {
let instance = namespace.define_class("Instance", ruby.class_object())?;
instance.define_method("get_func", method!(Instance::get_func, 1))?;
instance.define_method("get_resource", method!(Instance::get_resource, 1))?;

Ok(())
}
Loading