Skip to content

Commit

Permalink
Merge pull request #388 from bytecodealliance/components-1
Browse files Browse the repository at this point in the history
Initial support for components
  • Loading branch information
jbourassa authored Oct 24, 2024
2 parents 5376511 + 5f4577c commit 72a18aa
Show file tree
Hide file tree
Showing 29 changed files with 1,286 additions and 12 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ resolver = "2"
members = ["ext"]
exclude = [
"examples/rust-crate",
"spec/fixtures/component-types",
"spec/fixtures/wasi-debug",
"spec/fixtures/wasi-deterministic",
]
Expand Down
19 changes: 19 additions & 0 deletions bench/component_id.rb
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
151 changes: 151 additions & 0 deletions ext/src/ruby_api/component.rs
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(())
}
172 changes: 172 additions & 0 deletions ext/src/ruby_api/component/convert.rs
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"),
}
}
Loading

0 comments on commit 72a18aa

Please sign in to comment.