Skip to content

Opinionated typescript API generator that integrates with bevy apps

Notifications You must be signed in to change notification settings

sanspointes/bevy-wasm-api

Repository files navigation


Image of typed wasm api returning an optional tuple asyncronously.

bevy-wasm-api

Opinionated plugin and proc macro for bevy to easily build typed APIs when running in a wasm instance.
View Demo · Report Bug · Request Feature

Getting Started

Installation

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

Add Plugin to app

use bevy_wasm_api::BevyWasmApiPlugin;

#[wasm_bindgen]
pub fn setup_app(canvas_selector: String) {
    let mut app = App::new();
    app.add_plugins(BevyWasmApiPlugin).run();
}

Define an Api

⚠️ The first argument must be a 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)
        })
    }
}

Use your api in typescript

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
}

(back to top)

How it works

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))
        }))
    }
}

(back to top)

Examples

vite-app

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).

wasm-app

This shows how to use the crate purely from the bevy side.
Showcasing the changes you'd make / dependencies you'd need in bevy.

(back to top)

Features

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 typescript number type
    • Infers bool as typescript bool type
    • Correctly handles custom struct returns (must implement From/IntoWasmAbi) (use tsify to generate typescript types).
    • Infers &str/String as typescript string
    • Infers Result<T, E> as typescript Promise<T>
      • Use a Result polyfill so the final return type is Result<JsResult<T, E>>
    • Infers Vec<T> as typescript typescript Array<T> type
      • Infers an Iter<T> as typescript Array<T>?
    • Infers Option<T> as typescript T | undefined type
    • Infers tuples (i.e. (f32, String)) as typescript [number, String] type
    • Infers &[i32], and other number arrays as typescript Int32Array
    • Infers i32[], and other number arrays as typescript Int32Array
    • Handle Future<T> as typescript Promise<T>?
  • 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)
  • 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> and Out<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.

(back to top)

Contributing

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.

(back to top)

Help me out?

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)

Compatibility

bevy-wasm-api version Bevy version
0.2 0.14
0.1 0.13

About

Opinionated typescript API generator that integrates with bevy apps

Resources

Stars

Watchers

Forks

Packages

No packages published