Create a common-api crate to make it easier to write a wasm guest

This commit is contained in:
SeanOMik 2024-04-18 23:11:29 -04:00
parent 27c8f93611
commit a58dcfb53d
Signed by: SeanOMik
GPG Key ID: FEC9E2FC15235964
6 changed files with 311 additions and 132 deletions

147
common-api/src/bundle.rs Normal file
View File

@ -0,0 +1,147 @@
use crate::{lyra::api::ecs::ComponentInfo, Component, ComponentSerializationError};
use std::collections::VecDeque;
use std::io::Read;
#[derive(Default)]
pub struct ComponentBundle {
pub(crate) bundle: Vec<u8>,
pub(crate) infos: Vec<ComponentInfo>,
}
impl ComponentBundle {
pub fn new() -> Self {
Self::default()
}
pub fn push<C: Component>(&mut self, c: C) {
let info = C::component_info();
self.infos.push(info);
c.to_bytes_into(&mut self.bundle).unwrap();
}
}
pub trait Bundle: Sized {
fn component_info() -> Vec<ComponentInfo>;
fn to_bytes(self) -> Result<Vec<u8>, ComponentSerializationError>;
fn from_bytes(bytes: Vec<u8>) -> Result<Self, ComponentSerializationError>;
}
macro_rules! impl_into_bundle_tuple {
($last: tt, $( $name: tt ),+) => {
#[allow(non_snake_case)]
impl<$($name: Component,)* $last: Component> Bundle for ($($name,)* $last,) {
fn component_info() -> Vec<ComponentInfo> {
vec![$($name::component_info(),)+ $last::component_info()]
}
fn to_bytes(self) -> Result<Vec<u8>, ComponentSerializationError> {
let ($($name,)+ $last,) = self;
let mut bytes = vec![];
$(bytes.extend($name.to_bytes()?.into_iter());)+
bytes.extend($last.to_bytes()?.into_iter());
Ok(bytes)
}
fn from_bytes(bytes: Vec<u8>) -> Result<Self, ComponentSerializationError> {
let len = {
let mut acc = 0;
$(acc += $name::component_info().size;)+
acc
};
if bytes.len() < len as _ {
return Err(ComponentSerializationError::ExpectedEndOfBuffer);
}
let mut bytes = VecDeque::from(bytes);
$(
let info = $name::component_info();
let mut part_bytes = Vec::new();
part_bytes.resize(info.size as _, 0u8);
bytes.read_exact(&mut part_bytes)
.map_err(|_| ComponentSerializationError::ExpectedEndOfBuffer)?;
let $name = $name::from_bytes(&part_bytes)?;
)+
let info = $last::component_info();
let mut part_bytes = Vec::new();
part_bytes.resize(info.size as _, 0u8);
bytes.read_exact(&mut part_bytes)
.map_err(|_| ComponentSerializationError::ExpectedEndOfBuffer)?;
let $last = $last::from_bytes(&part_bytes)?;
Ok(($($name,)+ $last,))
}
}
impl_into_bundle_tuple!($( $name ),+);
};
($only: tt) => {
#[allow(non_snake_case)]
impl<$only: Component> Bundle for ($only,) {
fn component_info() -> Vec<ComponentInfo> {
vec![$only::component_info()]
}
fn to_bytes(self) -> Result<Vec<u8>, ComponentSerializationError> {
let mut bytes = vec![];
bytes.extend(self.0.to_bytes()?.into_iter());
Ok(bytes)
}
fn from_bytes(bytes: Vec<u8>) -> Result<Self, ComponentSerializationError> {
let mut bytes = VecDeque::from(bytes);
let info = $only::component_info();
let mut part_bytes = Vec::new();
part_bytes.resize(info.size as _, 0u8);
bytes.read_exact(&mut part_bytes)
.map_err(|_| ComponentSerializationError::ExpectedEndOfBuffer)?;
let $only = $only::from_bytes(&part_bytes)?;
Ok(($only,))
}
}
#[allow(non_snake_case)]
impl<$only: Component> Bundle for $only {
fn component_info() -> Vec<ComponentInfo> {
vec![$only::component_info()]
}
fn to_bytes(self) -> Result<Vec<u8>, ComponentSerializationError> {
let mut bytes = vec![];
bytes.extend(<Self as Component>::to_bytes(&self)?.into_iter());
Ok(bytes)
}
fn from_bytes(bytes: Vec<u8>) -> Result<Self, ComponentSerializationError> {
let mut bytes = VecDeque::from(bytes);
let info = $only::component_info();
let mut part_bytes = Vec::new();
part_bytes.resize(info.size as _, 0u8);
bytes.read_exact(&mut part_bytes)
.map_err(|_| ComponentSerializationError::ExpectedEndOfBuffer)?;
let $only = $only::from_bytes(&part_bytes)?;
Ok($only)
}
}
};
}
//impl_into_bundle_tuple!(C1, C2, C3, C4, C5, C6, C7, C8, C9, C10, C11, C12, C13, C14, C15, C16 );
impl_into_bundle_tuple!(C1, C2);

View File

@ -0,0 +1,70 @@
use std::alloc::Layout;
use crate::lyra::api::ecs::{ComponentInfo, WasmTypeId};
impl ComponentInfo {
pub fn of<C: Component>() -> Self {
let layout = Layout::new::<C>();
Self {
size: layout.size() as _,
alignment: layout.align() as _,
type_id: WasmTypeId::of::<C>(),
}
}
}
#[derive(Debug)]
pub enum ComponentSerializationMethod {
/// Serializes the component into bytes using [`bytemuck::bytes_of`].
Bytemuck,
/// Serializes the component into bytes using
/// [bincode](https://github.com/bincode-org/bincode).
Bincode
}
#[derive(Debug, thiserror::Error)]
pub enum ComponentSerializationError {
#[error("{0}")]
Bytemuck(bytemuck::PodCastError),
#[error("unexpected end of buffer")]
ExpectedEndOfBuffer,
}
pub trait Component: Sized + 'static {
const SERIALIZATION: ComponentSerializationMethod;
fn component_info() -> ComponentInfo;
fn to_bytes(&self) -> Result<Vec<u8>, ComponentSerializationError>;
fn to_bytes_into(&self, target: &mut Vec<u8>) -> Result<(), ComponentSerializationError>;
fn from_bytes(bytes: &[u8]) -> Result<Self, ComponentSerializationError>;
}
#[macro_export]
macro_rules! bytemuck_component_impl {
($type:ident) => {
impl common_api::Component for $type {
const SERIALIZATION: common_api::ComponentSerializationMethod = common_api::ComponentSerializationMethod::Bytemuck;
fn component_info() -> common_api::bindings::lyra::api::ecs::ComponentInfo {
common_api::bindings::lyra::api::ecs::ComponentInfo::of::<$type>()
}
fn to_bytes(&self) -> Result<Vec<u8>, common_api::ComponentSerializationError> {
Ok(common_api::bytemuck::bytes_of(self).to_vec())
}
fn to_bytes_into(&self, target: &mut Vec<u8>) -> Result<(), common_api::ComponentSerializationError> {
let bytes = self.to_bytes()?;
target.extend(bytes.into_iter());
Ok(())
}
fn from_bytes(bytes: &[u8]) -> Result<Self, common_api::ComponentSerializationError> {
common_api::bytemuck::try_from_bytes::<Self>(&bytes)
.map_err(|e| common_api::ComponentSerializationError::Bytemuck(e))
.cloned()
}
}
};
}

View File

@ -1,24 +1,23 @@
/* pub mod bindings {
wasmtime::component::bindgen!({
world: "example",
path: "../witguest/wit/world.wit",
//tracing: true,
async: true,
});
}
use bindings::{component::witguest::ecs::EcsWorld, *}; */
use std::{alloc::Layout, any::TypeId, marker::PhantomData, mem};
use std::{any::TypeId, marker::PhantomData, mem};
pub use bytemuck;
wit_bindgen::generate!({
world: "api",
path: "wit",
});
mod component;
pub use component::*;
use lyra::api::ecs::{ComponentInfo, EcsDynamicView, EcsWorld, Entity, WasmTypeId};
mod bundle;
pub use bundle::*;
pub mod bindings {
wit_bindgen::generate!({
world: "api",
path: "wit",
});
}
use bindings::*;
use lyra::api::ecs::{EcsDynamicView, EcsWorld, Entity, WasmTypeId};
impl WasmTypeId {
pub fn of<T: 'static>() -> Self {
@ -28,95 +27,6 @@ impl WasmTypeId {
}
}
impl ComponentInfo {
pub fn of<C: Component>() -> Self {
let layout = Layout::new::<C>();
Self {
size: layout.size() as _,
alignment: layout.align() as _,
type_id: WasmTypeId::of::<C>(),
}
}
}
#[derive(Debug)]
pub enum ComponentSerializationMethod {
/// Serializes the component into bytes using [`bytemuck::bytes_of`].
Bytemuck,
/// Serializes the component into bytes using
/// [bincode](https://github.com/bincode-org/bincode).
Bincode
}
#[derive(Debug, thiserror::Error)]
pub enum ComponentSerializationError {
#[error("{0}")]
Bytemuck(bytemuck::PodCastError),
}
pub trait Component: Sized + 'static {
const SERIALIZATION: ComponentSerializationMethod;
fn component_info() -> ComponentInfo;
fn to_bytes(&self) -> Result<Vec<u8>, ComponentSerializationError>;
fn to_bytes_into(&self, target: &mut Vec<u8>) -> Result<(), ComponentSerializationError>;
fn from_bytes(bytes: &[u8]) -> Result<Self, ComponentSerializationError>;
}
#[macro_export]
macro_rules! bytemuck_component_impl {
($type:ident) => {
impl common_api::Component for $type {
const SERIALIZATION: common_api::ComponentSerializationMethod = common_api::ComponentSerializationMethod::Bytemuck;
fn component_info() -> common_api::lyra::api::ecs::ComponentInfo {
common_api::lyra::api::ecs::ComponentInfo::of::<$type>()
}
fn to_bytes(&self) -> Result<Vec<u8>, common_api::ComponentSerializationError> {
Ok(common_api::bytemuck::bytes_of(self).to_vec())
}
fn to_bytes_into(&self, target: &mut Vec<u8>) -> Result<(), common_api::ComponentSerializationError> {
let bytes = self.to_bytes()?;
target.extend(bytes.into_iter());
Ok(())
}
fn from_bytes(bytes: &[u8]) -> Result<Self, common_api::ComponentSerializationError> {
common_api::bytemuck::try_from_bytes::<Self>(&bytes)
.map_err(|e| common_api::ComponentSerializationError::Bytemuck(e))
.cloned()
}
}
};
}
#[derive(Default)]
pub struct ComponentBundle {
bundle: Vec<u8>,
infos: Vec<ComponentInfo>,
}
impl ComponentBundle {
pub fn new() -> Self {
Self::default()
}
pub fn push<C: Component>(&mut self, c: C) {
let info = C::component_info();
self.infos.push(info);
c.to_bytes_into(&mut self.bundle);
/* self.bundle.reserve(info.size as _);
self.bundle.extend(comp_bytes.into_iter()); */
// dont call the destructor on C to avo
//mem::forget(c);
}
}
pub struct World {
inner: EcsWorld,
}
@ -128,33 +38,48 @@ impl World {
}
}
pub fn spawn(&self, components: ComponentBundle) -> Entity {
self.inner.spawn(&components.bundle, &components.infos)
pub fn spawn<B: Bundle>(&self, bundle: B) -> Result<Entity, ComponentSerializationError> {
let infos = B::component_info();
let bundle = bundle.to_bytes()?;
Ok(self.inner.spawn(&bundle, &infos))
}
pub fn view(&self, components: &[ComponentInfo]) -> View {
View::from(self.inner.view(components))
pub fn view<B: Bundle>(&self) -> View<B> {
let infos = B::component_info();
View::from(self.inner.view(&infos))
}
}
pub struct View {
pub struct View<B: Bundle> {
inner: EcsDynamicView,
_marker: PhantomData<B>,
}
impl From<EcsDynamicView> for View {
impl<B: Bundle> From<EcsDynamicView> for View<B> {
fn from(value: EcsDynamicView) -> Self {
Self {
inner: value
inner: value,
_marker: PhantomData
}
}
}
impl View {
pub fn next<C: Component>(&mut self) -> Option<C> {
let row = self.inner.next();
impl<B: Bundle> View<B> {
pub fn into_dynamic(self) -> EcsDynamicView {
self.inner
}
if let Some(row) = row {
Some(C::from_bytes(&row).unwrap())
pub fn as_dynamic(&self) -> &EcsDynamicView {
&self.inner
}
}
impl<B: Bundle> Iterator for View<B> {
type Item = Result<B, ComponentSerializationError>;
fn next(&mut self) -> Option<Self::Item> {
if let Some(row) = self.inner.next() {
Some(B::from_bytes(row))
} else {
None
}

View File

@ -11,26 +11,49 @@ interface ecs {
}
record wasm-type-id {
// represents a u128, can be converted into that with mem::transmute
inner: tuple<u64, u64>,
}
record component-info {
/// The size of the component in memory.
size: u64,
/// The alignment of the component in memory.
alignment: u64,
/// The type id of the component.
///
/// This must be unique between component types since its used to identify the components
/// in spawning and querying.
type-id: wasm-type-id, // a u128
}
resource ecs-dynamic-view {
constructor(wrld: borrow<ecs-world>, component-infos: list<component-info>);
/// Get the bytes of the next row in the view.
///
/// A row contains multiple component serialized as bytes. The buffer is tighly packed.
next: func() -> option<list<u8>>;
}
resource ecs-world {
constructor();
// expects components to be tightly packed in the same order of component-infos
/// Spawn an entity.
///
/// Parameters:
/// * `components`: A tightly packed byte buffer containing the components to spawn
/// with the entity. This expects the components in the same order of `component-infos`.
/// * `component-infos`: A list of `component-infos` uses to identify the components
/// and specify the layouts of them.
spawn: func(components: list<u8>, component-infos: list<component-info>) -> entity;
/// Query for a list of entities and their components.
///
/// Parameters:
/// * `component-infos`: The `component-info`'s of the components that you are querying.
///
/// Returns: an iterator that returns the byte buffers of each row.
view: func(component-infos: list<component-info>) -> ecs-dynamic-view;
}
}

View File

@ -45,7 +45,7 @@ impl From<ecs::Entity> for wasm_ecs::Entity {
impl Into<ecs::Entity> for wasm_ecs::Entity {
fn into(self) -> ecs::Entity {
let mut id = ecs::EntityId(self.id.id);
let id = ecs::EntityId(self.id.id);
ecs::Entity::new(id, self.generation)
}
}

View File

@ -2,10 +2,7 @@
mod bindings;
use bindings::Guest;
//use bindings::{component::witguest::ecs::{ComponentInfo, EcsWorld, Entity, WasmTypeId}, Guest};
use common_api::{bytemuck_component_impl, Component as EcsComponent, ComponentBundle, World};
use std::{alloc::Layout, any::TypeId, mem};
use common_api::{bytemuck_component_impl, World};
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
@ -25,22 +22,39 @@ impl Vec3 {
bytemuck_component_impl!(Vec3);
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
struct Vec4 {
x: f32,
y: f32,
z: f32,
w: f32,
}
impl Vec4 {
pub fn new(x: f32, y: f32, z: f32, w: f32) -> Self {
Self {
x, y, z, w
}
}
}
bytemuck_component_impl!(Vec4);
struct Component;
impl Guest for Component {
fn on_init() -> Result<(), ()> {
let world = World::new();
let mut bundle = ComponentBundle::new();
bundle.push(Vec3::new(7.0, 30.0, 18.0));
let en = world.spawn(bundle);
let en = world.spawn((Vec3::new(7.0, 30.0, 18.0), Vec4::new(10.0, 332.35, 329.0, 95.0)));
println!("guest spawned {:?}", en);
let mut view = world.view(&[Vec3::component_info()]);
while let Some(c) = view.next::<Vec3>() {
println!("Retrieved vec3: {:?}", c);
let view = world.view::<(Vec3, Vec4)>();
for pos in view {
let (p3, p4) = pos.unwrap();
println!("Retrieved vec3: {:?}", p3);
println!("Retrieved vec4: {:?}", p4);
}
Ok(())