change how bindings are generated, make it easier to call functions on the guest

This commit is contained in:
SeanOMik 2024-07-27 15:24:14 -04:00
parent f6933de559
commit 4deed47b3d
Signed by: SeanOMik
GPG Key ID: FEC9E2FC15235964
16 changed files with 269 additions and 171 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
/target /target
wasi_snapshot_preview1.reactor.wasm

6
Cargo.lock generated
View File

@ -46,9 +46,9 @@ dependencies = [
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.82" version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]] [[package]]
name = "api-derive" name = "api-derive"
@ -2470,6 +2470,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
"common-api", "common-api",
"wit-bindgen",
"wit-bindgen-rt", "wit-bindgen-rt",
] ]
@ -2477,6 +2478,7 @@ dependencies = [
name = "wittest" name = "wittest"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow",
"async-trait", "async-trait",
"common-api", "common-api",
"lyra-ecs", "lyra-ecs",

View File

@ -17,3 +17,4 @@ slab = "0.4.9"
thiserror = "1.0.58" thiserror = "1.0.58"
common-api = { path = "./common-api" } common-api = { path = "./common-api" }
anyhow = "1.0.86"

View File

@ -19,8 +19,8 @@ Install [`cargo-component`](https://github.com/bytecodealliance/cargo-component)
cargo install wasm-tools 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 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
``` ```

View File

@ -13,3 +13,8 @@ thiserror = "1.0.58"
wit-bindgen = "0.28.0" wit-bindgen = "0.28.0"
#wit-bindgen = { version = "0.24.0", default-features = false, features = ["realloc"] } #wit-bindgen = { version = "0.24.0", default-features = false, features = ["realloc"] }
api-derive = { path = "./api-derive" } api-derive = { path = "./api-derive" }
[package.metadata.component]
package = "lyra:api"
[package.metadata.component.dependencies]

View File

@ -1,4 +1,4 @@
use std::{any::TypeId, marker::PhantomData, mem}; use std::{any::TypeId, marker::PhantomData, mem, ops::Deref};
pub use bytemuck; pub use bytemuck;
@ -14,7 +14,7 @@ pub use api_derive as macros;
pub mod bindings { pub mod bindings {
wit_bindgen::generate!({ wit_bindgen::generate!({
world: "api", world: "imports",
path: "wit", path: "wit",
}); });
} }
@ -31,14 +31,46 @@ impl WasmTypeId {
} }
} }
pub struct World { enum OwnedBorrow<'a, T> {
inner: EcsWorld, 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<EcsWorld> 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 { pub fn new() -> Self {
Self { Self {
inner: EcsWorld::new(), inner: OwnedBorrow::Owned(EcsWorld::new()),
} }
} }

View File

@ -56,5 +56,7 @@ interface ecs {
/// ///
/// Returns: an iterator that returns the byte buffers of each row. /// Returns: an iterator that returns the byte buffers of each row.
view: func(component-infos: list<component-info>) -> ecs-dynamic-view; view: func(component-infos: list<component-info>) -> ecs-dynamic-view;
//with_system: func(stage: string, component-infos: list<component-info>, system: func(components: list<u8>));
} }
} }

View File

@ -1,5 +1,5 @@
package lyra:api; package lyra:api;
world api { world imports {
import ecs; import ecs;
} }

View File

@ -1,10 +1,12 @@
use std::alloc::Layout; use std::alloc::Layout;
use std::borrow::Borrow;
use std::ptr::NonNull; use std::ptr::NonNull;
use anyhow::{anyhow, Context};
use async_trait::async_trait; use async_trait::async_trait;
use component::witguest::math; use component::witguest::math;
use ecs::query::dynamic::{DynamicViewState, DynamicViewStateIter, QueryDynamicType}; use ecs::query::dynamic::{DynamicViewState, DynamicViewStateIter, QueryDynamicType};
use lyra_ecs::Component; use lyra_ecs::{Component, World};
use lyra_ecs as ecs; use lyra_ecs as ecs;
@ -12,13 +14,57 @@ use slab::Slab;
use thiserror::Error; use thiserror::Error;
use wasmtime_wasi::{ResourceTable, WasiCtx, WasiCtxBuilder, WasiView}; 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)] #[allow(unused_imports)]
pub(crate) mod lyra_engine { pub(crate) mod lyra_engine {
pub use lyra_ecs as ecs; 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; use lyra::api::ecs as wasm_ecs;
impl Into<ecs::DynTypeId> for wasm_ecs::WasmTypeId { impl Into<ecs::DynTypeId> for wasm_ecs::WasmTypeId {
@ -50,24 +96,6 @@ impl Into<ecs::Entity> 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)] #[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Component)] #[derive(Debug, Clone, Copy, PartialEq, Component)]
struct Vec3 { struct Vec3 {
@ -220,10 +248,7 @@ impl wasm_ecs::HostEcsDynamicView for Imports {
WasmResource::new_own(view_index as _) WasmResource::new_own(view_index as _)
} }
async fn next( async fn next(&mut self, view: WasmResource<wasm_ecs::EcsDynamicView>) -> Option<Vec<u8>> {
&mut self,
view: WasmResource<wasm_ecs::EcsDynamicView>,
) -> Option<Vec<u8>> {
let view_entry = self.world_views_slab.get_mut(view.rep() as _).unwrap(); 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 world_entry = self.world_slab.get(view_entry.world_index).unwrap();
let view = &mut view_entry.view; let view = &mut view_entry.view;
@ -296,7 +321,8 @@ async fn main() -> wasmtime::Result<()> {
//wasmtime_wasi::bindings::Imports::add_to_linker(&mut linker, |s| s)?; //wasmtime_wasi::bindings::Imports::add_to_linker(&mut linker, |s| s)?;
wasmtime_wasi::add_to_linker_async(&mut linker)?; wasmtime_wasi::add_to_linker_async(&mut linker)?;
Example::add_to_linker(&mut linker, |s| s)?; 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(); let mut builder = WasiCtxBuilder::new();
builder.inherit_stdio(); builder.inherit_stdio();
@ -314,14 +340,53 @@ async fn main() -> wasmtime::Result<()> {
); );
// Load the component from disk // 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 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::<EcsWorld>::new_borrow(world_idx as _);
let resb = WasmResource::<EcsWorld>::new_borrow(world_idx as _);
(script_en, (resa, resb))
};
// Instantiate the component // Instantiate the component
println!("creating example"); println!("creating example component");
let example = Example::instantiate_async(&mut store, &component, &linker).await?; let instance = linker.instantiate_async(&mut store, &component).await?;
example.call_on_init(&mut store).await?.unwrap();
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"); println!("Guest is done");
Ok(()) Ok(())
} }
async fn call_script_stage_async(mut store: &mut wasmtime::Store<Imports>, instance: &wasmtime::component::Instance, stage_name: &str, world_res: wasmtime::component::Resource<EcsWorld>, 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<EcsWorld>, 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}"))
}

View File

@ -3,10 +3,11 @@ name = "witguest"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
#build = "build.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [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"] } wit-bindgen-rt = { version = "0.28.0", features = ["bitflags"] }
common-api = { path = "../common-api" } common-api = { path = "../common-api" }
bytemuck = { version = "1.15.0", features = ["derive"] } bytemuck = { version = "1.15.0", features = ["derive"] }

27
witguest/build.rs Normal file
View File

@ -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<Path>, dst: impl AsRef<Path>) -> 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(())
}

View File

@ -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::<u8>();
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<T: Guest>() -> 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();
}

View File

@ -1,18 +1,28 @@
#[allow(warnings)] use common_api::{math::{Vec3, Vec4}, World};
mod bindings;
use bindings::Guest; wit_bindgen::generate!({
use common_api::{World, math::{Vec3, Vec4}}; world: "example",
with: {
"lyra:api/ecs": common_api::bindings::lyra::api::ecs,
},
});
struct Component; struct Component;
impl Guest for Component { impl Guest for Component {
fn on_init() -> Result<(), ()> { fn on_init(world: &EcsWorld, _owning_entity: Entity) -> Result<(), ()> {
let world = World::new(); 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))); 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); 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)>(); let view = world.view::<(Vec3, Vec4)>();
for pos in view { for pos in view {
let (p3, p4) = pos.unwrap(); let (p3, p4) = pos.unwrap();
@ -24,4 +34,4 @@ impl Guest for Component {
} }
} }
bindings::export!(Component with_types_in bindings); export!(Component);

View File

@ -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<u64, u64>,
}
record component-info {
host-name: option<string>,
/// 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<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();
/// 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;
//with_system: func(stage: string, component-infos: list<component-info>, system: func(components: list<u8>));
}
}

View File

@ -0,0 +1,5 @@
package lyra:api;
world imports {
import ecs;
}

View File

@ -11,7 +11,10 @@ interface math {
/// An example world for the component to target. /// An example world for the component to target.
world example { world example {
import math; import math;
use lyra:api/ecs.{ecs-world, entity};
import host-print: func(msg: string); import host-print: func(msg: string);
export on-init: func() -> result;
export on-init: func(game-world: borrow<ecs-world>, owning-entity: entity) -> result;
export on-update: func(game-world: borrow<ecs-world>, owning-entity: entity) -> result;
} }