From c7aa1ca39a999d30fc18e152d03d9eeee4dddd10 Mon Sep 17 00:00:00 2001 From: teleios Date: Thu, 2 Jul 2026 17:46:50 +0200 Subject: [PATCH 1/2] Document Component::Instance#get_func export name formats The `handle` param doc only said "the path of the function to retrieve", leaving the accepted name formats to guesswork. Spell them out: plain kebab-case export names, fully-qualified interface names including namespace, package and version (e.g. "ns:pkg/iface@0.1.0"), and canonical-ABI mangled resource function names ("[constructor]r", "[method]r.f", "[static]r.f"), plus an example combining interface and resource-constructor lookup. --- ext/src/ruby_api/component/instance.rs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/ext/src/ruby_api/component/instance.rs b/ext/src/ruby_api/component/instance.rs index 32106012..8d964b5f 100644 --- a/ext/src/ruby_api/component/instance.rs +++ b/ext/src/ruby_api/component/instance.rs @@ -45,7 +45,22 @@ impl Instance { /// Retrieves a Wasm function from the component instance. /// /// @def get_func(handle) - /// @param handle [String, Array] The path of the function to retrieve + /// @param handle [String, Array] 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: @@ -53,6 +68,9 @@ impl Instance { /// /// @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, handle: Value) -> Result, Error> { let func = rb_self .export_index(handle)? From 15441be8de123a79728c94e5549544fd071b93a4 Mon Sep 17 00:00:00 2001 From: teleios Date: Fri, 3 Jul 2026 21:45:12 +0200 Subject: [PATCH 2/2] Add Component::Instance#get_resource and WIT resource support Instance previously only exposed #get_func, making it impossible to fully drive a component that exports a WIT `resource` (e.g. a constructor plus instance methods) from Ruby: there was no way to look up the resource's exported type, and Val::Resource/Type::Own/Type::Borrow conversions were hard `not_implemented!` stubs. - Instance#get_resource wraps wasmtime's method of the same name; the shared lookup helper now internally prefers get_export_index over get_export since the ComponentItem the latter returns is never used. - New Wasmtime::Component::Resource wraps ResourceAny + its owning Store, requiring an explicit #resource_drop (wasmtime requires every ResourceAny, even borrows, to be explicitly dropped; there's no safe GC finalizer hook for re-entering a Store during collection). - New Wasmtime::Component::ResourceType wraps the matching wasmtime type; it is inert Copy data with no Store dependency, so unlike Resource it needs no GC mark. - convert.rs now lifts/lowers resource values for the Func::invoke path (calling exported functions), and guards against passing a Resource obtained from one Store into a call on a different Store, which would otherwise silently index into the wrong store's resource table. - Host-defined import functions (Linker#root.func_new) still don't support resources in either direction; that path never had a Ruby-visible Store handle to thread through, and stays out of scope here. Address review feedback on Component::Resource - Clarify that an own Resource becomes a dangling handle after a call transfers ownership. - Refactor the Store mismatch branch in rb_to_component_val to be clearer. - Add a test covering the dangling-handle behavior after an own transfer. --- ext/src/ruby_api/component.rs | 3 + ext/src/ruby_api/component/convert.rs | 63 +++++++---- ext/src/ruby_api/component/func.rs | 5 +- ext/src/ruby_api/component/instance.rs | 73 ++++++++---- ext/src/ruby_api/component/resource.rs | 147 +++++++++++++++++++++++++ ext/src/ruby_api/store.rs | 10 ++ spec/unit/component/convert_spec.rb | 61 +++++++++- spec/unit/component/instance_spec.rb | 32 ++++++ 8 files changed, 351 insertions(+), 43 deletions(-) create mode 100644 ext/src/ruby_api/component/resource.rs diff --git a/ext/src/ruby_api/component.rs b/ext/src/ruby_api/component.rs index 1ceb68ff..a2cb1678 100644 --- a/ext/src/ruby_api/component.rs +++ b/ext/src/ruby_api/component.rs @@ -2,6 +2,7 @@ mod convert; mod func; mod instance; mod linker; +mod resource; mod wasi_command; use super::root; @@ -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 { @@ -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)?; diff --git a/ext/src/ruby_api/component/convert.rs b/ext/src/ruby_api/component/convert.rs index 0f4f9f7f..cd7d4167 100644 --- a/ext/src/ruby_api/component/convert.rs +++ b/ext/src/ruby_api/component/convert.rs @@ -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}; @@ -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 { match val { Val::Bool(bool) => Ok(bool.into_value_with(ruby)), @@ -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)? } @@ -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), }; @@ -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) => { @@ -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"), @@ -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 { let ruby = Ruby::get_with(value); @@ -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); @@ -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))) })?; @@ -184,7 +193,7 @@ pub(crate) fn rb_to_component_val( let mut vals: Vec = 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); @@ -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))), @@ -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(), )?)))) } @@ -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() { @@ -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() { @@ -284,8 +293,22 @@ pub(crate) fn rb_to_component_val( } } Type::Flags(_) => Vec::::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"), diff --git a/ext/src/ruby_api/component/func.rs b/ext/src/ruby_api/component/func.rs index 9e115b68..60244664 100644 --- a/ext/src/ruby_api/component/func.rs +++ b/ext/src/ruby_api/component/func.rs @@ -53,7 +53,10 @@ use wasmtime::component::{Func as FuncImpl, Type, Val}; /// - invalid {Variant#name}, /// - unparametrized variant and not nil {Variant#value}. /// resource (own or borrow):: -/// 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 { diff --git a/ext/src/ruby_api/component/instance.rs b/ext/src/ruby_api/component/instance.rs index 8d964b5f..bd0ecbee 100644 --- a/ext/src/ruby_api/component/instance.rs +++ b/ext/src/ruby_api/component/instance.rs @@ -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; @@ -80,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] 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, handle: Value) -> Result, 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, Error> { let ruby = Ruby::get_with(handle); let invalid_arg = || { @@ -92,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(()) } diff --git a/ext/src/ruby_api/component/resource.rs b/ext/src/ruby_api/component/resource.rs new file mode 100644 index 00000000..ea768a2a --- /dev/null +++ b/ext/src/ruby_api/component/resource.rs @@ -0,0 +1,147 @@ +use std::cell::RefCell; + +use crate::error; +use crate::ruby_api::store::Store; +use magnus::{ + gc::Marker, method, prelude::*, typed_data::Obj, DataTypeFunctions, Error, Module, RModule, + Ruby, TypedData, +}; +use wasmtime::component::{ResourceAny as ResourceAnyImpl, ResourceType as ResourceTypeImpl}; + +/// @yard +/// @rename Wasmtime::Component::ResourceType +/// Represents the type of a WIT +resource+. Two {ResourceType}s are equal iff +/// they describe the same resource type in the same {Component} instantiation. +/// Returned by {Instance#get_resource} and {Resource#type}. There is no public +/// constructor. +/// @see https://docs.rs/wasmtime/latest/wasmtime/component/struct.ResourceType.html Wasmtime's Rust doc +#[magnus::wrap( + class = "Wasmtime::Component::ResourceType", + size, + free_immediately, + frozen_shareable +)] +pub struct ResourceType { + inner: ResourceTypeImpl, +} + +unsafe impl Send for ResourceType {} + +impl ResourceType { + pub fn from_inner(inner: ResourceTypeImpl) -> Self { + Self { inner } + } + + /// @yard + /// @def ==(other) + /// @param other [Object] + /// @return [Boolean] + fn eq(&self, other: &ResourceType) -> bool { + self.inner == other.inner + } +} + +/// @yard +/// @rename Wasmtime::Component::Resource +/// Represents a handle to a WIT +resource+ (either +own+ or +borrow+), +/// as produced by calling a Wasm component export that returns a resource, or +/// passed as an argument to one that accepts one. +/// +/// IMPORTANT: a {Resource} MUST be explicitly destroyed with {#resource_drop} +/// once it is no longer needed. This holds for +own+ *and* +borrow+ handles +/// alike: both hold state in the owning {Store} that must be released. There +/// is no automatic finalizer: Ruby's GC may run at times where it would be +/// unsafe to re-enter the {Store} (e.g. to invoke a guest-defined +/// destructor), so failing to call {#resource_drop} will leak store-side +/// resource-table state for the life of the {Store}. +/// +/// Passing an +own+ {Resource} into a function transfers ownership of the +/// underlying resource to the callee. Once the call returns, this {Resource} +/// object is a dangling handle; it still exists, but the store-side resource +/// it referred to now belongs to the guest. Calling {#resource_drop} on it, or +/// passing it into another call, may then raise. (+borrow+ parameters do not +/// transfer ownership, the same {Resource} stays valid and reusable.) +/// +/// A {Resource} is tied to the {Store} it was obtained from; passing one into +/// a function call on a different {Store} raises. +/// @see https://docs.rs/wasmtime/latest/wasmtime/component/struct.ResourceAny.html Wasmtime's Rust doc for +ResourceAny+ +#[derive(TypedData)] +#[magnus(class = "Wasmtime::Component::Resource", size, mark, free_immediately)] +pub struct Resource { + store: Obj, + inner: RefCell>, +} + +unsafe impl Send for Resource {} + +impl DataTypeFunctions for Resource { + fn mark(&self, marker: &Marker) { + marker.mark(self.store); + } +} + +impl Resource { + pub fn from_inner(store: Obj, inner: ResourceAnyImpl) -> Self { + Self { + store, + inner: RefCell::new(Some(inner)), + } + } + + /// Returns the live inner resource handle, without consuming it. + /// Errors if the resource has already been dropped. + pub(crate) fn get(&self) -> Result { + (*self.inner.borrow()).ok_or_else(|| error!("Resource has already been dropped")) + } + + /// The `Store` this resource was obtained from. + pub(crate) fn store(&self) -> Obj { + self.store + } + + /// @yard + /// @def owned? + /// @return [Boolean] whether this is an +own+ (+true+) or +borrow+ + /// (+false+) handle. + fn owned(&self) -> Result { + Ok(self.get()?.owned()) + } + + /// @yard + /// @def type + /// @return [ResourceType] + fn type_(&self) -> Result { + Ok(ResourceType::from_inner(self.get()?.ty())) + } + + /// @yard + /// Explicitly destroys this resource, releasing store-side state and + /// invoking the guest-defined destructor if applicable. MUST be called + /// exactly once for every {Resource}, including borrows. + /// @def resource_drop + /// @return [nil] + fn resource_drop(&self) -> Result<(), Error> { + let resource_any = self + .inner + .borrow_mut() + .take() + .ok_or_else(|| error!("Resource has already been dropped"))?; + + resource_any + .resource_drop(self.store.context_mut()) + .map_err(|e| error!("{}", e)) + } +} + +pub fn init(ruby: &Ruby, namespace: &RModule) -> Result<(), Error> { + let resource_type = namespace.define_class("ResourceType", ruby.class_object())?; + resource_type.define_method("==", method!(ResourceType::eq, 1))?; + resource_type.define_method("eql?", method!(ResourceType::eq, 1))?; + + let resource = namespace.define_class("Resource", ruby.class_object())?; + resource.define_method("owned?", method!(Resource::owned, 0))?; + resource.define_method("type", method!(Resource::type_, 0))?; + resource.define_method("resource_drop", method!(Resource::resource_drop, 0))?; + + Ok(()) +} diff --git a/ext/src/ruby_api/store.rs b/ext/src/ruby_api/store.rs index 822e22c2..c2cc4ca6 100644 --- a/ext/src/ruby_api/store.rs +++ b/ext/src/ruby_api/store.rs @@ -369,6 +369,16 @@ impl StoreContextValue<'_> { } } + /// Returns the `Obj` backing this context, if any. `None` for the + /// `Caller` variant, since a `Caller` is only valid on the stack for the + /// duration of a host call and must not be persisted. + pub fn as_store(&self) -> Option> { + match self { + Self::Store(store) => Some(Ruby::get().unwrap().get_inner(*store)), + Self::Caller(_) => None, + } + } + pub fn set_last_error(&self, error: Error) { let ruby = Ruby::get().unwrap(); match self { diff --git a/spec/unit/component/convert_spec.rb b/spec/unit/component/convert_spec.rb index 6b75c03a..f3655b4d 100644 --- a/spec/unit/component/convert_spec.rb +++ b/spec/unit/component/convert_spec.rb @@ -58,7 +58,66 @@ def call_func(name, *args) end end - # TODO resource + describe "resources" do + let(:ctor) { instance.get_func(["resource", "[constructor]wrapped-string"]) } + let(:to_string) { instance.get_func(["resource", "[method]wrapped-string.to-string"]) } + let(:resource_owned) { instance.get_func(["resource", "resource-owned"]) } + + it "constructs a resource, calls a borrow method (repeatedly), and drops it" do + resource = ctor.call("hello") + + expect(resource).to be_instance_of(Resource) + expect(resource.owned?).to be true + expect(resource.type).to eq(instance.get_resource(["resource", "wrapped-string"])) + + expect(to_string.call(resource)).to eq("hello") + expect(to_string.call(resource)).to eq("hello") # borrow: reusable + + expect(resource.resource_drop).to be_nil + end + + it "raises on double drop" do + resource = ctor.call("hello") + resource.resource_drop + + expect { resource.resource_drop }.to raise_error(/already been dropped/) + end + + it "raises when using a dropped resource" do + resource = ctor.call("hello") + resource.resource_drop + + expect { to_string.call(resource) }.to raise_error(/already been dropped/) + end + + it "transfers ownership into a function taking own" do + resource = ctor.call("owned") + + expect { resource_owned.call(resource) }.not_to raise_error + end + + it "invalidates an own Resource after transferring ownership" do + resource = ctor.call("owned") + resource_owned.call(resource) + + expect { to_string.call(resource) }.to raise_error(Wasmtime::Error, /unknown handle index/) + expect { resource.resource_drop }.to raise_error(Wasmtime::Error, /unknown handle index/) + end + + it "raises TypeError when passing a non-Resource where a resource is expected" do + expect { resource_owned.call("not a resource") } + .to raise_error(TypeError, /Resource/) + end + + it "raises when passing a Resource obtained from a different Store" do + other_instance = linker.instantiate(Store.new(GLOBAL_ENGINE), @types_component) + other_ctor = other_instance.get_func(["resource", "[constructor]wrapped-string"]) + resource = other_ctor.call("hello") + + expect { to_string.call(resource) } + .to raise_error(/different Store/) + end + end describe "failures" do [ diff --git a/spec/unit/component/instance_spec.rb b/spec/unit/component/instance_spec.rb index b403aef3..fa1ae0f6 100644 --- a/spec/unit/component/instance_spec.rb +++ b/spec/unit/component/instance_spec.rb @@ -5,10 +5,12 @@ module Component RSpec.describe Instance do before(:all) do @adder_component = Component.from_file(GLOBAL_ENGINE, "spec/fixtures/component_adder.wat") + @types_component = Component.from_file(GLOBAL_ENGINE, "spec/fixtures/component_types.wasm") end let(:linker) { Linker.new(engine) } let(:adder_instance) { linker.instantiate(store, @adder_component) } + let(:types_instance) { linker.instantiate(store, @types_component) } describe "#get_func" do it "returns a root func" do @@ -31,6 +33,36 @@ module Component expect { adder_instance.get_func([nil]) } .to raise_error(TypeError, /invalid argument for component index/) end + + it "returns a func nested under an exported resource" do + expect(types_instance.get_func(["resource", "[constructor]wrapped-string"])) + .to be_instance_of(Wasmtime::Component::Func) + end + end + + describe "#get_resource" do + it "returns a ResourceType for an exported resource" do + expect(types_instance.get_resource(["resource", "wrapped-string"])) + .to be_instance_of(Wasmtime::Component::ResourceType) + end + + it "returns nil for a non-resource or missing export" do + expect(types_instance.get_resource(["resource", "resource-owned"])).to be_nil + expect(types_instance.get_resource(["resource", "no"])).to be_nil + expect(types_instance.get_resource("no")).to be_nil + end + + it "raises for invalid arg" do + expect { types_instance.get_resource(3) } + .to raise_error(TypeError, /invalid argument for component index/) + end + + it "returns equal ResourceTypes for repeated lookups of the same resource" do + a = types_instance.get_resource(["resource", "wrapped-string"]) + b = types_instance.get_resource(["resource", "wrapped-string"]) + + expect(a).to eq(b) + end end end end