diff --git a/.gitignore b/.gitignore index ea8c4bf..a9f000c 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +wasi_snapshot_preview1.reactor.wasm \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 334d451..3da2223 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -46,9 +46,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.82" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "api-derive" @@ -2470,6 +2470,7 @@ version = "0.1.0" dependencies = [ "bytemuck", "common-api", + "wit-bindgen", "wit-bindgen-rt", ] @@ -2477,6 +2478,7 @@ dependencies = [ name = "wittest" version = "0.1.0" dependencies = [ + "anyhow", "async-trait", "common-api", "lyra-ecs", diff --git a/Cargo.toml b/Cargo.toml index 71d0f0b..f05e23a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,4 +16,5 @@ lyra-ecs = { path = "./lyra-engine/lyra-ecs" } slab = "0.4.9" thiserror = "1.0.58" -common-api = { path = "./common-api" } \ No newline at end of file +common-api = { path = "./common-api" } +anyhow = "1.0.86" diff --git a/README.md b/README.md index f865d73..1e241c2 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,8 @@ Install [`cargo-component`](https://github.com/bytecodealliance/cargo-component) cargo install wasm-tools ``` -Build the `witguest` project: +Download [`wasi_snapshot_preview1.reactor.wasm`](https://github.com/bytecodealliance/wasmtime/releases/download/v17.0.0/wasi_snapshot_preview1.reactor.wasm) in the `witguest` folder. Build the `witguest` project then use an adapter to convert the WASM module to a component: ``` cd witguest -cargo component build +cargo build --target wasm32-wasip1 && wasm-tools component new ../target/wasm32-wasip1/debug/witguest.wasm -o ../target/wasm32-wasip1/debug/witguest-component.wasm --adapt ../wasi_snapshot_preview1.reactor.wasm ``` \ No newline at end of file diff --git a/common-api/Cargo.toml b/common-api/Cargo.toml index 18546d4..a68770c 100644 --- a/common-api/Cargo.toml +++ b/common-api/Cargo.toml @@ -12,4 +12,9 @@ thiserror = "1.0.58" #wasmtime = "19.0.2" wit-bindgen = "0.28.0" #wit-bindgen = { version = "0.24.0", default-features = false, features = ["realloc"] } -api-derive = { path = "./api-derive" } \ No newline at end of file +api-derive = { path = "./api-derive" } + +[package.metadata.component] +package = "lyra:api" + +[package.metadata.component.dependencies] \ No newline at end of file diff --git a/common-api/src/lib.rs b/common-api/src/lib.rs index 2e0cf92..8d49480 100755 --- a/common-api/src/lib.rs +++ b/common-api/src/lib.rs @@ -1,4 +1,4 @@ -use std::{any::TypeId, marker::PhantomData, mem}; +use std::{any::TypeId, marker::PhantomData, mem, ops::Deref}; pub use bytemuck; @@ -14,7 +14,7 @@ pub use api_derive as macros; pub mod bindings { wit_bindgen::generate!({ - world: "api", + world: "imports", path: "wit", }); } @@ -31,14 +31,46 @@ impl WasmTypeId { } } -pub struct World { - inner: EcsWorld, +enum OwnedBorrow<'a, T> { + Borrowed(&'a T), + Owned(T), } -impl World { +impl<'a, T> Deref for OwnedBorrow<'a, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + match self { + OwnedBorrow::Borrowed(v) => *v, + OwnedBorrow::Owned(v) => &v, + } + } +} + +pub struct World<'a> { + inner: OwnedBorrow<'a, EcsWorld>, +} + +impl<'a> From for World<'a> { + fn from(value: EcsWorld) -> Self { + Self { + inner: OwnedBorrow::Owned(value), + } + } +} + +impl<'a> From<&'a EcsWorld> for World<'a> { + fn from(value: &'a EcsWorld) -> Self { + Self { + inner: OwnedBorrow::Borrowed(value), + } + } +} + +impl<'a> World<'a> { pub fn new() -> Self { Self { - inner: EcsWorld::new(), + inner: OwnedBorrow::Owned(EcsWorld::new()), } } diff --git a/common-api/wit/ecs.wit b/common-api/wit/ecs.wit index 6871e3f..5e717e5 100644 --- a/common-api/wit/ecs.wit +++ b/common-api/wit/ecs.wit @@ -56,5 +56,7 @@ interface ecs { /// /// Returns: an iterator that returns the byte buffers of each row. view: func(component-infos: list) -> ecs-dynamic-view; + + //with_system: func(stage: string, component-infos: list, system: func(components: list)); } } \ No newline at end of file diff --git a/common-api/wit/world.wit b/common-api/wit/world.wit index d8262d2..4947c02 100644 --- a/common-api/wit/world.wit +++ b/common-api/wit/world.wit @@ -1,5 +1,5 @@ package lyra:api; -world api { +world imports { import ecs; } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 9b97d68..bd39ef6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,12 @@ use std::alloc::Layout; +use std::borrow::Borrow; use std::ptr::NonNull; +use anyhow::{anyhow, Context}; use async_trait::async_trait; use component::witguest::math; use ecs::query::dynamic::{DynamicViewState, DynamicViewStateIter, QueryDynamicType}; -use lyra_ecs::Component; +use lyra_ecs::{Component, World}; use lyra_ecs as ecs; @@ -12,13 +14,57 @@ use slab::Slab; use thiserror::Error; use wasmtime_wasi::{ResourceTable, WasiCtx, WasiCtxBuilder, WasiView}; -use wasmtime::component::Resource as WasmResource; +use wasmtime::component::{Resource as WasmResource, ResourceAny}; + +use wasmtime::component::Val as ComponentVal; #[allow(unused_imports)] pub(crate) mod lyra_engine { pub use lyra_ecs as ecs; } +/* mod bindings { + wasmtime::component::bindgen!({ + world: "imports", + path: "common-api/wit", + //tracing: true, + async: true, + }); + + mod component { + wasmtime::component::bindgen!({ + world: "example", + path: "witguest/wit", + //tracing: true, + async: true, + + with: { + "lyra:api": super::lyra::api, + } + }); + } +} + +use bindings::lyra::api::ecs as wasm_ecs; */ + +/* wasmtime::component::bindgen!({ + world: "imports", + path: "common-api/wit", + //tracing: true, + async: true, +}); */ + +wasmtime::component::bindgen!({ + world: "example", + path: "witguest/wit", + //tracing: true, + async: true, + + with: { + //"lyra:api": lyra::api, + } +}); + use lyra::api::ecs as wasm_ecs; impl Into for wasm_ecs::WasmTypeId { @@ -50,24 +96,6 @@ impl Into for wasm_ecs::Entity { } } -wasmtime::component::bindgen!({ - world: "api", - path: "common-api/wit", - //tracing: true, - async: true, -}); - -wasmtime::component::bindgen!({ - world: "example", - path: "witguest/wit", - //tracing: true, - async: true, - - with: { - //"lyra:api": lyra::api, - } -}); - #[repr(C)] #[derive(Debug, Clone, Copy, PartialEq, Component)] struct Vec3 { @@ -220,10 +248,7 @@ impl wasm_ecs::HostEcsDynamicView for Imports { WasmResource::new_own(view_index as _) } - async fn next( - &mut self, - view: WasmResource, - ) -> Option> { + async fn next(&mut self, view: WasmResource) -> Option> { let view_entry = self.world_views_slab.get_mut(view.rep() as _).unwrap(); let world_entry = self.world_slab.get(view_entry.world_index).unwrap(); let view = &mut view_entry.view; @@ -246,7 +271,7 @@ impl wasm_ecs::HostEcsDynamicView for Imports { dst, comp.info.layout().size(), ); - + // make sure to tell the vec that it now has the bytes of this component row_buf.set_len(row_buf.len() + comp_size); } @@ -291,12 +316,13 @@ async fn main() -> wasmtime::Result<()> { // Configure the linker let mut linker = wasmtime::component::Linker::new(&engine); - + //wasmtime_wasi::preview0::add_to_linker_sync(&mut linker, |s| s)?; //wasmtime_wasi::bindings::Imports::add_to_linker(&mut linker, |s| s)?; wasmtime_wasi::add_to_linker_async(&mut linker)?; Example::add_to_linker(&mut linker, |s| s)?; - Api::add_to_linker(&mut linker, |s| s)?; + //lyra::api::ecs::add_to_linker(&mut linker, |s| s)?; + //Api::add_to_linker(&mut linker, |s| s)?; let mut builder = WasiCtxBuilder::new(); builder.inherit_stdio(); @@ -314,14 +340,53 @@ async fn main() -> wasmtime::Result<()> { ); // Load the component from disk - let bytes = std::fs::read("target/wasm32-wasip1/debug/witguest.wasm")?; + let bytes = std::fs::read("target/wasm32-wasip1/debug/witguest-component.wasm")?; let component = wasmtime::component::Component::new(&engine, bytes)?; + let (script_en, (world_res_a, world_res_b)) = { + let mut world = World::new(); + let script_en = world.spawn(()); + + let data = store.data_mut(); + let world_idx = data.world_slab.insert(WorldEntry { + world, + views: Slab::with_capacity(10), + }); + let resa = WasmResource::::new_borrow(world_idx as _); + let resb = WasmResource::::new_borrow(world_idx as _); + + (script_en, (resa, resb)) + }; + // Instantiate the component - println!("creating example"); - let example = Example::instantiate_async(&mut store, &component, &linker).await?; - example.call_on_init(&mut store).await?.unwrap(); + println!("creating example component"); + let instance = linker.instantiate_async(&mut store, &component).await?; + + call_script_stage_async(&mut store, &instance, "init", world_res_a, script_en.into()).await?; + call_script_stage_async(&mut store, &instance, "update", world_res_b, script_en.into()).await?; + println!("Guest is done"); Ok(()) } + +async fn call_script_stage_async(mut store: &mut wasmtime::Store, instance: &wasmtime::component::Instance, stage_name: &str, world_res: wasmtime::component::Resource, script_entity: Entity) -> anyhow::Result<()> { + let func_name = format!("on-{}", stage_name); + let func = instance + .get_func(&mut store, &func_name) + .expect("Could not find on_init"); + + let typed_func = unsafe { + wasmtime::component::TypedFunc::< + (wasmtime::component::Resource, Entity), + (Result<(), ()>,), + >::new_unchecked(func) + }; + + let (ret,) = typed_func.call_async(&mut store, (world_res, script_entity.into())).await + .with_context(|| format!("failed to call guest function {func_name}"))?; + typed_func.post_return_async(&mut store).await + .with_context(|| format!("failed to call post-return function of guest after calling {func_name}"))?; + + ret.map_err(|_| anyhow!("unknown error returned from guest when calling function {func_name}")) +} diff --git a/witguest/Cargo.toml b/witguest/Cargo.toml index 60c76db..4caf9c5 100644 --- a/witguest/Cargo.toml +++ b/witguest/Cargo.toml @@ -3,10 +3,11 @@ name = "witguest" version = "0.1.0" edition = "2021" +#build = "build.rs" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -#wit-bindgen = { version = "0.28.0", default-features = false, features = ["realloc"] } +wit-bindgen = { version = "0.28.0", default-features = false, features = ["realloc"] } wit-bindgen-rt = { version = "0.28.0", features = ["bitflags"] } common-api = { path = "../common-api" } bytemuck = { version = "1.15.0", features = ["derive"] } diff --git a/witguest/build.rs b/witguest/build.rs new file mode 100644 index 0000000..9e1dad2 --- /dev/null +++ b/witguest/build.rs @@ -0,0 +1,27 @@ +use std::{fs, io, path::Path}; + + +// Example custom build script. +fn main() { + // Tell Cargo that if the given file changes, to rerun this build script. + println!("cargo::rerun-if-changed=../common-api/wit/"); + + fs::remove_dir_all("./wit/deps/lyraapi").unwrap(); + copy_dir_all("../common-api/wit", "./wit/deps/lyraapi").unwrap(); +} + +fn copy_dir_all(src: impl AsRef, dst: impl AsRef) -> io::Result<()> { + fs::create_dir_all(&dst)?; + + for entry in fs::read_dir(src)? { + let entry = entry?; + let ty = entry.file_type()?; + if ty.is_dir() { + copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?; + } else { + fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?; + } + } + + Ok(()) +} \ No newline at end of file diff --git a/witguest/src/bindings.rs b/witguest/src/bindings.rs deleted file mode 100644 index 33e6a51..0000000 --- a/witguest/src/bindings.rs +++ /dev/null @@ -1,118 +0,0 @@ -// Generated by `wit-bindgen` 0.25.0. DO NOT EDIT! -// Options used: -#[allow(unused_unsafe, clippy::all)] -pub fn host_print(msg: &str) { - unsafe { - let vec0 = msg; - let ptr0 = vec0.as_ptr().cast::(); - let len0 = vec0.len(); - - #[cfg(target_arch = "wasm32")] - #[link(wasm_import_module = "$root")] - extern "C" { - #[link_name = "host-print"] - fn wit_import(_: *mut u8, _: usize); - } - - #[cfg(not(target_arch = "wasm32"))] - fn wit_import(_: *mut u8, _: usize) { - unreachable!() - } - wit_import(ptr0.cast_mut(), len0); - } -} -#[doc(hidden)] -#[allow(non_snake_case)] -pub unsafe fn _export_on_init_cabi() -> i32 { - #[cfg(target_arch = "wasm32")] - _rt::run_ctors_once(); - let result0 = T::on_init(); - let result1 = match result0 { - Ok(_) => 0i32, - Err(_) => 1i32, - }; - result1 -} -pub trait Guest { - fn on_init() -> Result<(), ()>; -} -#[doc(hidden)] - -macro_rules! __export_world_example_cabi{ - ($ty:ident with_types_in $($path_to_types:tt)*) => (const _: () = { - - #[export_name = "on-init"] - unsafe extern "C" fn export_on_init() -> i32 { - $($path_to_types)*::_export_on_init_cabi::<$ty>() - } - };); -} -#[doc(hidden)] -pub(crate) use __export_world_example_cabi; -#[allow(dead_code)] -pub mod component { - #[allow(dead_code)] - pub mod witguest { - #[allow(dead_code, clippy::all)] - pub mod math { - #[used] - #[doc(hidden)] - #[cfg(target_arch = "wasm32")] - static __FORCE_SECTION_REF: fn() = - super::super::super::__link_custom_section_describing_imports; - } - } -} -mod _rt { - - #[cfg(target_arch = "wasm32")] - pub fn run_ctors_once() { - wit_bindgen_rt::run_ctors_once(); - } -} - -/// Generates `#[no_mangle]` functions to export the specified type as the -/// root implementation of all generated traits. -/// -/// For more information see the documentation of `wit_bindgen::generate!`. -/// -/// ```rust -/// # macro_rules! export{ ($($t:tt)*) => (); } -/// # trait Guest {} -/// struct MyType; -/// -/// impl Guest for MyType { -/// // ... -/// } -/// -/// export!(MyType); -/// ``` -#[allow(unused_macros)] -#[doc(hidden)] - -macro_rules! __export_example_impl { - ($ty:ident) => (self::export!($ty with_types_in self);); - ($ty:ident with_types_in $($path_to_types_root:tt)*) => ( - $($path_to_types_root)*::__export_world_example_cabi!($ty with_types_in $($path_to_types_root)*); - ) -} -#[doc(inline)] -pub(crate) use __export_example_impl as export; - -#[cfg(target_arch = "wasm32")] -#[link_section = "component-type:wit-bindgen:0.25.0:example:encoded world"] -#[doc(hidden)] -pub static __WIT_BINDGEN_COMPONENT_TYPE: [u8; 262] = *b"\ -\0asm\x0d\0\x01\0\0\x19\x16wit-component-encoding\x04\0\x07\x88\x01\x01A\x02\x01\ -A\x07\x01B\x02\x01r\x03\x01xv\x01yv\x01zv\x04\0\x04vec3\x03\0\0\x03\x01\x17compo\ -nent:witguest/math\x05\0\x01@\x01\x03msgs\x01\0\x03\0\x0ahost-print\x01\x01\x01j\ -\0\0\x01@\0\0\x02\x04\0\x07on-init\x01\x03\x04\x01\x1acomponent:witguest/example\ -\x04\0\x0b\x0d\x01\0\x07example\x03\0\0\0G\x09producers\x01\x0cprocessed-by\x02\x0d\ -wit-component\x070.208.1\x10wit-bindgen-rust\x060.25.0"; - -#[inline(never)] -#[doc(hidden)] -#[cfg(target_arch = "wasm32")] -pub fn __link_custom_section_describing_imports() { - wit_bindgen_rt::maybe_link_cabi_realloc(); -} diff --git a/witguest/src/lib.rs b/witguest/src/lib.rs index 36288b2..8d9d4a8 100644 --- a/witguest/src/lib.rs +++ b/witguest/src/lib.rs @@ -1,18 +1,28 @@ -#[allow(warnings)] -mod bindings; +use common_api::{math::{Vec3, Vec4}, World}; -use bindings::Guest; -use common_api::{World, math::{Vec3, Vec4}}; +wit_bindgen::generate!({ + world: "example", + + with: { + "lyra:api/ecs": common_api::bindings::lyra::api::ecs, + }, +}); struct Component; impl Guest for Component { - fn on_init() -> Result<(), ()> { - let world = World::new(); + fn on_init(world: &EcsWorld, _owning_entity: Entity) -> Result<(), ()> { + let world = World::from(world); 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); + Ok(()) + } + + fn on_update(world: &EcsWorld, _owning_entity: Entity) -> Result<(), ()> { + let world = World::from(world); + let view = world.view::<(Vec3, Vec4)>(); for pos in view { let (p3, p4) = pos.unwrap(); @@ -24,4 +34,4 @@ impl Guest for Component { } } -bindings::export!(Component with_types_in bindings); \ No newline at end of file +export!(Component); \ No newline at end of file diff --git a/witguest/wit/deps/lyraapi/ecs.wit b/witguest/wit/deps/lyraapi/ecs.wit new file mode 100644 index 0000000..5e717e5 --- /dev/null +++ b/witguest/wit/deps/lyraapi/ecs.wit @@ -0,0 +1,62 @@ +package lyra:api; + +interface ecs { + record entity-id { + id: u64, + } + + record entity { + id: entity-id, + generation: u64, + } + + record wasm-type-id { + // represents a u128, can be converted into that with mem::transmute + inner: tuple, + } + + record component-info { + host-name: option, + /// 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, + } + + resource ecs-dynamic-view { + constructor(wrld: borrow, component-infos: list); + + /// 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>; + } + + resource ecs-world { + constructor(); + + /// 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, component-infos: list) -> 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) -> ecs-dynamic-view; + + //with_system: func(stage: string, component-infos: list, system: func(components: list)); + } +} \ No newline at end of file diff --git a/witguest/wit/deps/lyraapi/world.wit b/witguest/wit/deps/lyraapi/world.wit new file mode 100644 index 0000000..4947c02 --- /dev/null +++ b/witguest/wit/deps/lyraapi/world.wit @@ -0,0 +1,5 @@ +package lyra:api; + +world imports { + import ecs; +} \ No newline at end of file diff --git a/witguest/wit/world.wit b/witguest/wit/world.wit index a31edd3..54a9781 100644 --- a/witguest/wit/world.wit +++ b/witguest/wit/world.wit @@ -11,7 +11,10 @@ interface math { /// An example world for the component to target. world example { import math; + use lyra:api/ecs.{ecs-world, entity}; import host-print: func(msg: string); - export on-init: func() -> result; + + export on-init: func(game-world: borrow, owning-entity: entity) -> result; + export on-update: func(game-world: borrow, owning-entity: entity) -> result; }