Opinionated plugin and proc macro for bevy to easily build typed APIs when running in a wasm instance.
View Demo
·
Report Bug
·
Request Feature
Install the bevy-wasm-api
crate.
cargo add --git https://github.com/sanspointes/bevy-wasm-api
cargo add wasm-bindgen
Install optional dependencies to help your development
# Required if you want to return custom structs from your api
cargo add serde --features derive
# Helpful crate for generating typescript types for custom structs
cargo add tsify --features js --no-default-features
use bevy_wasm_api::BevyWasmApiPlugin;
#[wasm_bindgen]
pub fn setup_app(canvas_selector: String) {
let mut app = App::new();
app.add_plugins(BevyWasmApiPlugin).run();
}
world: &mut World
.
#[wasm_bindgen(skip_typescript)] // Let bevy-wasm-api generate the types
struct MyApi;
#[bevy_wasm_api]
impl MyApi {
pub fn spawn_entity(world: &mut World, x: f32, y: f32, z: f32) -> Entity {
world.spawn(
TransformBundle {
transform: Transform {
translation: Vec3::new(x, y, z),
..Default::default(),
},
..Default::default(),
}
).id()
}
pub fn set_entity_position(world: &mut World, entity: u32, x: f32, y: f32, z: f32) -> Result<(), String> {
let entity = Entity::from_raw(entity);
let mut transform = world.get_mut::<Transform>(entity).ok_or("Could not find entity".to_string())?;
transform.translation.x = x;
transform.translation.y = y;
transform.translation.z = z;
Ok(())
}
pub fn get_entity_position(world: &mut World, entity: u32) -> Option<(f32, f32, f32)> {
let transform = world.get::<Transform>(Entity::from_raw(entity));
transform.map(|transform| {
let pos = transform.translation;
(pos.x, pos.y, pos.z)
})
}
}
import { setup_app, MyApi } from 'bevy-app';
async function start() {
try {
setup_app('#canvas-element');
} catch (error) {
// Ignore, Bevy apps for wasm error for control flow.
}
const api = new MyApi();
const id = await api.spawn_entity(0, 0, 0);
await api.set_entity_position(id, 10, 0, 0);
const pos = await api.get_entity_position(id)
console.log(pos) // [10, 0, 0]
const otherPos = await api.get_entity_position(1000) // (Made up entity)
console.log(pos) // undefined
}
The crate uses a similar approach to the deferred promise
by parking the function that we want to execute (See Task
in sync.rs
),
executing all the parked tasks, and then converts the result back to a JsValue.
The real complexity is in the effort to support typed returns in typescript which is handled in the bevy-wasm-api-macro-core` crate.
Given the following input
#[bevy_wasm_api]
impl MyApi {
pub fn my_function(world: &mut World, x:f32, y: f32) -> bool {
// Do anything with your &mut World
true
}
}
The output will look something like this.
// Exposes `MyApiWasmApi` as `MyApi` in javascript
#[wasm_bindgen(js_class = "MyApi")]
impl MyApiWasmApi {
// Skips wasm_bindgen typescript types so we can generate better typescript types.
#[wasm_bindgen(skip_typescript)]
pub fn my_function(x: f32, y: f32) -> js_sys::Promise {
// Uses execute_in_world to get a `world: &mut World`, converts the future to a Js Promise
wasm_bindgen_futures::future_to_promise(bevy_wasm_api::execute_in_world(bevy_wasm_api::ExecutionChannel::FrameStart, |world| {
// Calls the original method
let ret_val = MyApi::my_function(world, x, y);
// Return the original return type as a JsValue
// The real code that's generated here is actually dependent on the return type but I'll keep it simple in this example.
Ok(JsValue::from(ret_val))
}))
}
}
This is your "kitchen sink" example showcasing a lot of the features of the crate. This is how I am personally using the package to develop my app (a CAD/design program).
This shows how to use the crate purely from the bevy side.
Showcasing the changes you'd make / dependencies you'd need in bevy.
Here's an outline of the currently supported feature set + features that I'd like to implement.
- Type inference / handling of return types
- Infers any number (
i32
, ...) as typescriptnumber
type - Infers
bool
as typescriptbool
type - Correctly handles custom struct returns (must implement From/IntoWasmAbi) (use tsify to generate typescript types).
- Infers
&str
/String
as typescriptstring
- Infers
Result<T, E>
as typescriptPromise<T>
- Use a Result polyfill so the final return type is
Result<JsResult<T, E>>
- Use a Result polyfill so the final return type is
- Infers
Vec<T>
as typescript typescriptArray<T>
type- Infers an
Iter<T>
as typescriptArray<T>
?
- Infers an
- Infers
Option<T>
as typescriptT | undefined
type - Infers tuples (i.e.
(f32, String)
) as typescript[number, String]
type - Infers
&[i32]
, and other number arrays as typescriptInt32Array
- Infers
i32[]
, and other number arrays as typescriptInt32Array
- Handle
Future<T>
as typescriptPromise<T>
?
- Infers any number (
- Type inference / handling of argument types
- Input parameters handled entirely by
wasm_bindgen
. tsify is good for making this more ergonomic. - Implement custom handling supporting the same typed parameters as return types (above)
- Input parameters handled entirely by
- Targets:
- Exposes an api in JS that communicates directly with the bevy wasm app. (For use in browser contexts)
- Exposes an api in JS that communicates with a desktop app over a HTTP endpoint + RPC layer. (For use in desktop contexts with ui in bevy_wry_webview)
- Support systems as the Api handler. Make use of
In<T>
andOut<T>
for args / return value. - Support multiple bevy apps
- Less restrictive dependency versions
- Adding proc macro attributes to declare when in the frame lifecycle we want to execute the api method.
This crate is an ends to a means for developing an app so I am not sure what level of support I will be able to provide and I might not be able to support a lot of additional features. That being said, if you run into bugs or have ideas for improvements/features feel free to create an issue or, even better, submit a PR.
⚠️ If the PR is fairly large and complex it could be worth submitting an issue introducing the desired changes + the usecase so I can verify if it's something that belongs in this crate.
This is also my first proc_macro and I am not that experience with the "bevy" way of doing things so if you know have some technical ideas on how this crate can be improved (improve modularity/adaptability, performance, simplify code) I would be very grateful to hear it in an issue.
Some things I'd love feedback on is:
- Making the dependency versions lest restrictive.
- Adding proc macro attributes on each function to declare when the ApiMethod should run.
- Making better use of bevy paradigms
- Making better use of wasm_bindgen type inference (currently duplicating logic converting
str
(rust) ->string
(typescript)) - All of this is only tested with my depenencies, anything that makes it more versatile (I might be a bit too dumb to make it fully generic)
- Generalising the type inference improvements into its own crate (could be useful outside of the bevy ecosystem)
bevy-wasm-api version | Bevy version |
---|---|
0.2 | 0.14 |
0.1 | 0.13 |