-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #388 from bytecodealliance/components-1
Initial support for components
- Loading branch information
Showing
29 changed files
with
1,286 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
require_relative "bench" | ||
|
||
Bench.ips do |x| | ||
engine = Wasmtime::Engine.new | ||
linker = Wasmtime::Component::Linker.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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
mod convert; | ||
mod func; | ||
mod instance; | ||
mod linker; | ||
|
||
use super::root; | ||
use magnus::{class, function, method, r_string::RString, Error, Module, Object, Ruby}; | ||
use rb_sys::tracking_allocator::ManuallyTracked; | ||
use wasmtime::component::Component as ComponentImpl; | ||
|
||
pub use func::Func; | ||
pub use instance::Instance; | ||
|
||
use crate::{ | ||
error, | ||
helpers::{nogvl, Tmplock}, | ||
Engine, | ||
}; | ||
/// @yard | ||
/// @rename Wasmtime::Component::Component | ||
/// Represents a WebAssembly component. | ||
/// @note Support for Wasm components in the Ruby bindings is experimental. APIs may change in the future. | ||
/// @see https://docs.rs/wasmtime/latest/wasmtime/component/struct.Component.html Wasmtime's Rust doc | ||
#[magnus::wrap( | ||
class = "Wasmtime::Component::Component", | ||
size, | ||
free_immediately, | ||
frozen_shareable | ||
)] | ||
pub struct Component { | ||
inner: ComponentImpl, | ||
_track_memory_usage: ManuallyTracked<()>, | ||
} | ||
|
||
// Needed for ManuallyTracked | ||
unsafe impl Send for Component {} | ||
|
||
impl Component { | ||
/// @yard | ||
/// Creates a new component from the given binary data. | ||
/// @def new(engine, wat_or_wasm) | ||
/// @param engine [Wasmtime::Engine] | ||
/// @param wat_or_wasm [String] The String of WAT or Wasm. | ||
/// @return [Wasmtime::Component::Component] | ||
pub fn new(engine: &Engine, wat_or_wasm: RString) -> Result<Self, Error> { | ||
let eng = engine.get(); | ||
let (locked_slice, _locked_slice_guard) = wat_or_wasm.as_locked_slice()?; | ||
let component = nogvl(|| ComponentImpl::new(eng, locked_slice)) | ||
.map_err(|e| error!("Could not build component: {}", e))?; | ||
|
||
Ok(component.into()) | ||
} | ||
|
||
/// @yard | ||
/// @def from_file(engine, path) | ||
/// @param engine [Wasmtime::Engine] | ||
/// @param path [String] | ||
/// @return [Wasmtime::Component::Component] | ||
pub fn from_file(engine: &Engine, path: RString) -> Result<Self, Error> { | ||
let eng = engine.get(); | ||
let (path, _locked_str_guard) = path.as_locked_str()?; | ||
// SAFETY: this string is immediately copied and never moved off the stack | ||
let component = nogvl(|| ComponentImpl::from_file(eng, path)) | ||
.map_err(|e| error!("Could not build component from file: {}", e))?; | ||
|
||
Ok(component.into()) | ||
} | ||
|
||
/// @yard | ||
/// Instantiates a serialized component coming from either {#serialize} or {Wasmtime::Engine#precompile_component}. | ||
/// | ||
/// The engine serializing and the engine deserializing must: | ||
/// * have the same configuration | ||
/// * be of the same gem version | ||
/// | ||
/// @def deserialize(engine, compiled) | ||
/// @param engine [Wasmtime::Engine] | ||
/// @param compiled [String] String obtained with either {Wasmtime::Engine#precompile_component} or {#serialize}. | ||
/// @return [Wasmtime::Component::Component] | ||
pub fn deserialize(engine: &Engine, compiled: RString) -> Result<Self, Error> { | ||
// SAFETY: this string is immediately copied and never moved off the stack | ||
unsafe { ComponentImpl::deserialize(engine.get(), compiled.as_slice()) } | ||
.map(Into::into) | ||
.map_err(|e| error!("Could not deserialize component: {}", e)) | ||
} | ||
|
||
/// @yard | ||
/// Instantiates a serialized component from a file. | ||
/// | ||
/// @def deserialize_file(engine, path) | ||
/// @param engine [Wasmtime::Engine] | ||
/// @param path [String] | ||
/// @return [Wasmtime::Component::Component] | ||
/// @see .deserialize | ||
pub fn deserialize_file(engine: &Engine, path: RString) -> Result<Self, Error> { | ||
unsafe { ComponentImpl::deserialize_file(engine.get(), path.as_str()?) } | ||
.map(Into::into) | ||
.map_err(|e| error!("Could not deserialize component from file: {}", e)) | ||
} | ||
|
||
/// @yard | ||
/// Serialize the component. | ||
/// @return [String] | ||
/// @see .deserialize | ||
pub fn serialize(&self) -> Result<RString, Error> { | ||
let bytes = self.get().serialize(); | ||
|
||
bytes | ||
.map(|bytes| RString::from_slice(&bytes)) | ||
.map_err(|e| error!("{:?}", e)) | ||
} | ||
|
||
pub fn get(&self) -> &ComponentImpl { | ||
&self.inner | ||
} | ||
} | ||
|
||
impl From<ComponentImpl> for Component { | ||
fn from(inner: ComponentImpl) -> Self { | ||
let range = inner.image_range(); | ||
let start = range.start; | ||
let end = range.end; | ||
|
||
assert!(end > start); | ||
let size = unsafe { end.offset_from(start) }; | ||
|
||
Self { | ||
inner, | ||
_track_memory_usage: ManuallyTracked::new(size as usize), | ||
} | ||
} | ||
} | ||
|
||
pub fn init(ruby: &Ruby) -> Result<(), Error> { | ||
let namespace = root().define_module("Component")?; | ||
|
||
let class = namespace.define_class("Component", class::object())?; | ||
class.define_singleton_method("new", function!(Component::new, 2))?; | ||
class.define_singleton_method("from_file", function!(Component::from_file, 2))?; | ||
class.define_singleton_method("deserialize", function!(Component::deserialize, 2))?; | ||
class.define_singleton_method( | ||
"deserialize_file", | ||
function!(Component::deserialize_file, 2), | ||
)?; | ||
class.define_method("serialize", method!(Component::serialize, 0))?; | ||
|
||
linker::init(ruby, &namespace)?; | ||
instance::init(ruby, &namespace)?; | ||
|
||
Ok(()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
use crate::error; | ||
use crate::not_implemented; | ||
use crate::ruby_api::errors::ExceptionMessage; | ||
use crate::ruby_api::store::StoreContextValue; | ||
use magnus::exception::type_error; | ||
use magnus::rb_sys::AsRawValue; | ||
use magnus::value::ReprValue; | ||
use magnus::{prelude::*, value, Error, IntoValue, RArray, RHash, RString, Ruby, Value}; | ||
use wasmtime::component::{Type, Val}; | ||
|
||
pub(crate) fn component_val_to_rb(val: Val, _store: &StoreContextValue) -> Result<Value, Error> { | ||
match val { | ||
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(component_val_to_rb(val, _store)?)?; | ||
} | ||
Ok(array.into_value()) | ||
} | ||
Val::Record(fields) => { | ||
let hash = RHash::new(); | ||
for (name, val) in fields { | ||
let rb_value = component_val_to_rb(val, _store) | ||
.map_err(|e| e.append(format!(" (struct field \"{}\")", name)))?; | ||
hash.aset(name.as_str(), rb_value)? | ||
} | ||
|
||
Ok(hash.into_value()) | ||
} | ||
Val::Tuple(vec) => { | ||
let array = RArray::with_capacity(vec.len()); | ||
for val in vec { | ||
array.push(component_val_to_rb(val, _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(component_val_to_rb(*val, _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(crate) fn rb_to_component_val( | ||
value: Value, | ||
_store: &StoreContextValue, | ||
ty: &Type, | ||
) -> Result<Val, Error> { | ||
match ty { | ||
Type::Bool => { | ||
let ruby = Ruby::get().unwrap(); | ||
if value.as_raw() == ruby.qtrue().as_raw() { | ||
Ok(Val::Bool(true)) | ||
} else if value.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 { | ||
value.classname() | ||
}), | ||
)) | ||
} | ||
} | ||
Type::S8 => Ok(Val::S8(i8::try_convert(value)?)), | ||
Type::U8 => Ok(Val::U8(u8::try_convert(value)?)), | ||
Type::S16 => Ok(Val::S16(i16::try_convert(value)?)), | ||
Type::U16 => Ok(Val::U16(u16::try_convert(value)?)), | ||
Type::S32 => Ok(Val::S32(i32::try_convert(value)?)), | ||
Type::U32 => Ok(Val::U32(u32::try_convert(value)?)), | ||
Type::S64 => Ok(Val::S64(i64::try_convert(value)?)), | ||
Type::U64 => Ok(Val::U64(u64::try_convert(value)?)), | ||
Type::Float32 => Ok(Val::Float32(f32::try_convert(value)?)), | ||
Type::Float64 => Ok(Val::Float64(f64::try_convert(value)?)), | ||
Type::Char => Ok(Val::Char(value.to_r_string()?.to_char()?)), | ||
Type::String => Ok(Val::String(RString::try_convert(value)?.to_string()?)), | ||
Type::List(list) => { | ||
let ty = list.ty(); | ||
let rarray = RArray::try_convert(value)?; | ||
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 = rb_to_component_val(*value, _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(value) | ||
.map_err(|_| error!("invalid value for record: {}", value.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)) | ||
.and_then(|v| { | ||
rb_to_component_val(v, _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(value)?; | ||
|
||
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 = rb_to_component_val(*value, _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 value.is_nil() { | ||
Ok(Val::Option(None)) | ||
} else { | ||
Ok(Val::Option(Some(Box::new(rb_to_component_val( | ||
value, | ||
_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"), | ||
} | ||
} |
Oops, something went wrong.