Implement JavaScript runtime
Some checks failed
Book / test (pull_request) Has been cancelled
Rust / build (3.4.4, map[os:ubuntu-latest]) (pull_request) Has been cancelled

This commit is contained in:
Jaroslaw Konik 2026-06-04 23:50:53 +02:00
parent 4fc2b47834
commit 36cc907f0e
60 changed files with 1944 additions and 20 deletions

View file

@ -14,6 +14,7 @@ keywords = ["bevy", "lua", "scripting", "game", "script"]
lua = ["dep:mlua", "mlua/luajit"]
rhai = ["dep:rhai"]
ruby = ["dep:magnus", "dep:rb-sys"]
javascript = ["dep:deno_core"]
[dependencies]
bevy = { default-features = false, version = "0.18", features = ["bevy_asset", "bevy_log"] }
@ -33,6 +34,7 @@ mlua = { version = "0.9.8", features = [
], optional = true }
magnus = { version = "0.8.2", optional = true }
rb-sys = { version = "0.9", default-features = false, features = ["link-ruby", "ruby-static"], optional = true }
deno_core = { version = "0.403", optional = true }
crossbeam-channel = "0.5.15"
libc = "0.2.172"
@ -211,6 +213,56 @@ name = "side_effects_ruby"
path = "examples/ruby/side_effects.rs"
required-features = ["ruby"]
[[example]]
name = "call_function_from_rust_js"
path = "examples/js/call_function_from_rust.rs"
required-features = ["javascript"]
[[example]]
name = "current_entity_js"
path = "examples/js/current_entity.rs"
required-features = ["javascript"]
[[example]]
name = "ecs_js"
path = "examples/js/ecs.rs"
required-features = ["javascript"]
[[example]]
name = "entity_variable_js"
path = "examples/js/entity_variable.rs"
required-features = ["javascript"]
[[example]]
name = "function_params_js"
path = "examples/js/function_params.rs"
required-features = ["javascript"]
[[example]]
name = "hello_world_js"
path = "examples/js/hello_world.rs"
required-features = ["javascript"]
[[example]]
name = "multiple_plugins_js"
path = "examples/js/multiple_plugins.rs"
required-features = ["javascript"]
[[example]]
name = "non_closure_system_js"
path = "examples/js/non_closure_system.rs"
required-features = ["javascript"]
[[example]]
name = "promises_js"
path = "examples/js/promises.rs"
required-features = ["javascript"]
[[example]]
name = "side_effects_js"
path = "examples/js/side_effects.rs"
required-features = ["javascript"]
[dev-dependencies]
tracing-subscriber = "0.3.18"
mlua = { version = "0.9.8", features = ["luajit", "vendored", "send"] }

View file

@ -10,6 +10,7 @@ bevy_scriptum is a a plugin for [Bevy](https://bevyengine.org/) that allows you
| 🌙 LuaJIT | `lua` | [link](https://jarkonik.github.io/bevy_scriptum/lua/lua.html) |
| 🌾 Rhai | `rhai` | [link](https://jarkonik.github.io/bevy_scriptum/rhai/rhai.html) |
| 💎 Ruby(currently only supported on Linux and MacOS) | `ruby` | [link](https://jarkonik.github.io/bevy_scriptum/ruby/ruby.html) |
| 🟨 JavaScript (V8 via deno_core) | `javascript` | [link](https://jarkonik.github.io/bevy_scriptum/javascript/javascript.html) |
Documentation book is available [here](https://jarkonik.github.io/bevy_scriptum/) 📖

View file

@ -0,0 +1,13 @@
var my_state = {
iterations: 0,
};
function on_update() {
my_state.iterations = my_state.iterations + 1;
print("on_update called " + my_state.iterations + " times");
if (my_state.iterations >= 10) {
print("calling quit");
quit();
}
}

View file

@ -0,0 +1,7 @@
// entity is a global variable that is set to the entity that is currently being
// processed, it is automatically available in all scripts.
// get name of the entity
get_name(entity).and_then(function (name) {
print(name);
});

View file

@ -0,0 +1 @@
print_player_names();

View file

@ -0,0 +1,3 @@
// entity is a global variable that is set to the entity that is currently being
// processed, it is automatically available in all scripts.
print("Current entity index: " + entity.index);

View file

@ -0,0 +1,4 @@
fun_without_params();
fun_with_string_param("hello");
fun_with_i64_param(5);
fun_with_multiple_params(5, "hello");

View file

@ -0,0 +1 @@
hello_bevy();

View file

@ -0,0 +1 @@
hello_from_plugin_a();

View file

@ -0,0 +1 @@
hello_from_plugin_b_with_parameters("hello", 42);

View file

@ -0,0 +1,3 @@
get_player_name().and_then(function (name) {
print(name);
});

View file

@ -0,0 +1 @@
spawn_entity();

View file

@ -0,0 +1,3 @@
function test_func() {
throw new Error("intentional runtime error");
}

View file

@ -0,0 +1,7 @@
var State = {
called_with: null,
};
function test_func(x) {
State.called_with = x;
}

View file

@ -0,0 +1,3 @@
function test_func() {
rust_func(entity.index);
}

View file

@ -0,0 +1,5 @@
var index = entity.index;
function test_func() {
rust_func(index);
}

View file

@ -0,0 +1,2 @@
mark_called();
throw new Error("intentional runtime error");

View file

@ -0,0 +1,3 @@
function test_func() {
rust_func(entity);
}

View file

@ -0,0 +1,3 @@
function test_func() {
rust_func(Vec3(1.5, 2.5, -3.5));
}

View file

@ -0,0 +1,6 @@
function test_func(vec3) {
if (vec3.x !== 1.5 || vec3.y !== 2.5 || vec3.z !== -3.5) {
throw new Error("unexpected Vec3 components");
}
mark_success();
}

View file

@ -0,0 +1,5 @@
function test_func() {
rust_func().and_then(function (x) {
throw new Error("intentional runtime error");
});
}

View file

@ -0,0 +1,9 @@
var State = {
x: null,
};
function test_func() {
rust_func().and_then(function (x) {
State.x = x;
});
}

View file

@ -0,0 +1,3 @@
function test_func() {
rust_func();
}

View file

@ -0,0 +1,3 @@
function test_func() {
rust_func(5, "test");
}

View file

@ -0,0 +1,3 @@
function test_func() {
rust_func(5);
}

View file

@ -0,0 +1,7 @@
var State = {
times_called: 0,
};
function test_func() {
State.times_called = State.times_called + 1;
}

View file

@ -0,0 +1,9 @@
var State = {
a_value: null,
b_value: null,
};
function test_func(a, b) {
State.a_value = a;
State.b_value = b;
}

View file

@ -0,0 +1,7 @@
var State = {
a_value: null,
};
function test_func(a) {
State.a_value = a;
}

View file

@ -0,0 +1,3 @@
function test_func() {
spawn_entity();
}

View file

@ -19,6 +19,15 @@
- [Calling Ruby from Rust](./ruby/calling_script_from_rust.md)
- [Interacting with bevy in callbacks](./ruby/interacting_with_bevy.md)
- [Builtin types](./ruby/builtin_types.md)
- [JavaScript](./javascript/javascript.md)
- [Installation](./javascript/installation.md)
- [Hello World](./javascript/hello_world.md)
- [Spawning scripts](./javascript/spawning_scripts.md)
- [Calling Rust from JavaScript](./javascript/calling_rust_from_script.md)
- [Calling JavaScript from Rust](./javascript/calling_script_from_rust.md)
- [Interacting with bevy in callbacks](./javascript/interacting_with_bevy.md)
- [Builtin types](./javascript/builtin_types.md)
- [Builtin variables](./javascript/builtin_variables.md)
- [Rhai](./rhai/rhai.md)
- [Installation](./rhai/installation.md)
- [Hello World(TBD)]()

View file

@ -0,0 +1,85 @@
# Builtin types
bevy_scriptum provides the following types that can be used in JavaScript:
- `Vec3`
- `Entity`
## Vec3
A `Vec3` is represented as a plain object with `x`, `y` and `z` number
properties. A global `Vec3(x, y, z)` constructor function is available to all
scripts.
### Example JavaScript usage
```js
var my_vec = Vec3(1, 2, 3);
print(my_vec.x, my_vec.y, my_vec.z);
set_translation(entity, my_vec);
```
### Example Rust usage
```rust,no_run
# extern crate bevy;
# extern crate bevy_scriptum;
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::javascript::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_scripting::<JsRuntime>(|runtime| {
runtime.add_function(String::from("set_translation"), set_translation);
})
.run();
}
fn set_translation(
In((entity, translation)): In<(BevyEntity, BevyVec3)>,
mut entities: Query<&mut Transform>,
) {
let mut transform = entities.get_mut(entity.0).unwrap();
transform.translation = translation.0;
}
```
## Entity
The currently processed entity is available through the global `entity`
variable. It exposes an `index` property and can be passed back to Rust
functions that accept a `BevyEntity` argument.
`entity` is currently not available within promise callbacks.
### Example JavaScript usage
```js
print(entity.index);
pass_to_rust(entity);
```
### Example Rust usage
```rust,no_run
# extern crate bevy;
# extern crate bevy_scriptum;
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::javascript::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_scripting::<JsRuntime>(|runtime| {
runtime.add_function(String::from("pass_to_rust"), |In((entity,)): In<(BevyEntity,)>| {
println!("pass_to_rust called with entity: {:?}", entity);
});
})
.run();
}
```

View file

@ -0,0 +1,14 @@
# Builtin variables
## entity
A variable called `entity` is automatically available to all scripts - it
represents the bevy entity that the `Script` component is attached to.
It exposes an `index` property that returns the bevy entity index.
It is useful for accessing the entity's components from scripts.
It can be used in the following way:
```js
print("Current entity index: " + entity.index);
```
The `entity` variable is currently not available within promise callbacks.

View file

@ -0,0 +1,113 @@
# Calling Rust from JavaScript
To call a rust function from JavaScript first you need to register a function
within Rust using the builder pattern.
```rust,no_run
# extern crate bevy;
# extern crate bevy_scriptum;
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::javascript::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_scripting::<JsRuntime>(|runtime| {
// `runtime` is a builder that you can use to register functions
})
.run();
}
```
For example to register a function called `my_rust_func` you can do the following:
```rust,no_run
# extern crate bevy;
# extern crate bevy_scriptum;
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::javascript::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_scripting::<JsRuntime>(|runtime| {
runtime.add_function(String::from("my_rust_func"), || {
println!("my_rust_func has been called");
});
})
.run();
}
```
After you do that the function will be available to JavaScript code in your
spawned scripts.
```js
my_rust_func();
```
Since a registered callback function is a Bevy system, the parameters are passed
to it as an `In` struct with a tuple, which has to be the first parameter of the
closure.
```rust,no_run
# extern crate bevy;
# extern crate bevy_scriptum;
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::javascript::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_scripting::<JsRuntime>(|runtime| {
runtime.add_function(String::from("func_with_params"), |In((a, b)): In<(String, i64)>| {
println!("func_with_params has been called with string {} and i64 {}", a, b);
});
})
.run();
}
```
The above function can be called from JavaScript
```js
func_with_params("abc", 123);
```
## Return value via promise
Any registered rust function that returns a value will return a promise when
called within a script. By calling `and_then` on the promise you can register
a callback that will receive the value returned from the Rust function.
```rust,no_run
# extern crate bevy;
# extern crate bevy_scriptum;
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::javascript::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_scripting::<JsRuntime>(|runtime| {
runtime.add_function(String::from("returns_value"), || {
123
});
})
.run();
}
```
```js
returns_value().and_then(function (value) {
print(value); // 123
});
```

View file

@ -0,0 +1,66 @@
# Calling JavaScript from Rust
To call a function defined in JavaScript
```js
function on_update() {
}
```
We need to acquire the `JsRuntime` resource within a bevy system.
Then we will be able to call `call_fn` on it, providing the name
of the function to call, the `JsScriptData` that has been automatically
attached to the entity after an entity with a script attached has been spawned
and its script evaluated, the entity and optionally some arguments.
```rust,no_run
# extern crate bevy;
# extern crate bevy_scriptum;
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::javascript::prelude::*;
fn call_js_on_update_from_rust(
mut scripted_entities: Query<(Entity, &mut JsScriptData)>,
scripting_runtime: ResMut<JsRuntime>,
) {
for (entity, mut script_data) in &mut scripted_entities {
// calling function named `on_update` defined in JavaScript script
scripting_runtime
.call_fn("on_update", &mut script_data, entity, ())
.unwrap();
}
}
```
We can also pass some arguments by providing a tuple or `Vec` as the last
`call_fn` argument.
```rust,no_run
# extern crate bevy;
# extern crate bevy_scriptum;
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::javascript::prelude::*;
fn call_js_on_update_from_rust(
mut scripted_entities: Query<(Entity, &mut JsScriptData)>,
scripting_runtime: ResMut<JsRuntime>,
) {
for (entity, mut script_data) in &mut scripted_entities {
scripting_runtime
.call_fn("on_update", &mut script_data, entity, (123, String::from("hello")))
.unwrap();
}
}
```
They will be passed to the `on_update` JavaScript function
```js
function on_update(a, b) {
print(a); // 123
print(b); // hello
}
```

View file

@ -0,0 +1,75 @@
# Hello World
After you are done installing the required crates, you can start developing
your first game or application using bevy_scriptum.
To start using the library you need to first import some structs and traits
with Rust `use` statements.
For convenience there is a main "prelude" module provided called
`bevy_scriptum::prelude` and a prelude for each runtime you have enabled as
a crate feature.
You can now start exposing functions to the scripting language. For example, you
can expose a function that prints a message to the console:
```rust,no_run
# extern crate bevy;
# extern crate bevy_scriptum;
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::javascript::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_scripting::<JsRuntime>(|runtime| {
runtime.add_function(
String::from("my_print"),
|In((x,)): In<(String,)>| {
println!("my_print: '{}'", x);
},
);
})
.run();
}
```
Then you can create a script file in `assets` directory called `script.js` that
calls this function:
```js
my_print("Hello world!");
```
And spawn an entity with attached `Script` component with a handle to a script
source file:
```rust,no_run
# extern crate bevy;
# extern crate bevy_scriptum;
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::javascript::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_scripting::<JsRuntime>(|runtime| {
runtime.add_function(
String::from("my_print"),
|In((x,)): In<(String,)>| {
println!("my_print: '{}'", x);
},
);
})
.add_systems(Startup,|mut commands: Commands, asset_server: Res<AssetServer>| {
commands.spawn(Script::<JsScript>::new(asset_server.load("script.js")));
})
.run();
}
```
You should then see `my_print: 'Hello world!'` printed in your console.

View file

@ -0,0 +1,18 @@
# Installation
JavaScript support links against V8 which is downloaded as a prebuilt static
library by the `v8` crate during the build, so the first build may take a while
and requires network access.
## Main Library
Add the following to your `Cargo.toml`:
```toml
[dependencies]
bevy = "0.18"
bevy_scriptum = { version = "0.11", features = ["javascript"] }
```
If you need a different version of bevy you need to use a matching bevy_scriptum
version according to the [bevy support matrix](../bevy_support_matrix.md)

View file

@ -0,0 +1,83 @@
# Interacting with bevy in callbacks
Every registered function is also just a regular Bevy system.
That allows you to do anything you would do in a Bevy system.
You could for example create a callback system function that prints names
of all entities with a `Player` component.
```rust,no_run
# extern crate bevy;
# extern crate bevy_ecs;
# extern crate bevy_scriptum;
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::javascript::prelude::*;
#[derive(Component)]
struct Player;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_scripting::<JsRuntime>(|runtime| {
runtime.add_function(
String::from("print_player_names"),
|players: Query<&Name, With<Player>>| {
for player in &players {
println!("player name: {}", player);
}
},
);
})
.run();
}
```
In script:
```js
print_player_names();
```
You can use functions that interact with Bevy entities and resources and
take arguments at the same time. It could be used for example to mutate a
component.
```rust,no_run
# extern crate bevy;
# extern crate bevy_ecs;
# extern crate bevy_scriptum;
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::javascript::prelude::*;
#[derive(Component)]
struct Player {
health: i32
}
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_scripting::<JsRuntime>(|runtime| {
runtime.add_function(
String::from("hurt_player"),
|In((hit_value,)): In<(i32,)>, mut players: Query<&mut Player>| {
let mut player = players.single_mut().unwrap();
player.health -= hit_value;
},
);
})
.run();
}
```
And it could be called in script like:
```js
hurt_player(5);
```

View file

@ -0,0 +1,11 @@
# JavaScript
This chapter demonstrates how to work with bevy_scriptum when using the
JavaScript language runtime.
JavaScript support is powered by the [V8](https://v8.dev/) engine through
[`deno_core`](https://crates.io/crates/deno_core). Scripts run synchronously -
the JavaScript event loop is not pumped, so APIs such as `setTimeout` or native
`Promise`s are not available. Return values from registered Rust functions are
delivered through bevy_scriptum's own promise object (see
[Calling Rust from JavaScript](./calling_rust_from_script.md)).

View file

@ -0,0 +1,42 @@
# Spawning scripts
To spawn a JavaScript script you will need to get a handle to a script asset
using bevy's `AssetServer`.
```rust
# extern crate bevy;
# extern crate bevy_scriptum;
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::javascript::prelude::*;
fn my_spawner(mut commands: Commands, assets_server: Res<AssetServer>) {
commands.spawn(Script::<JsScript>::new(
assets_server.load("my_script.js"),
));
}
```
After the scripts have been evaled by bevy_scriptum, the entities that they've
been attached to will get the `Script::<JsScript>` component stripped and instead
a `JsScriptData` component will be attached.
So to query scripted entities you could do something like:
```rust
# extern crate bevy;
# extern crate bevy_scriptum;
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::javascript::prelude::*;
fn my_system(
mut scripted_entities: Query<(Entity, &mut JsScriptData)>,
) {
for (entity, mut script_data) in &mut scripted_entities {
// do something with scripted entities
}
}
```

View file

@ -0,0 +1,33 @@
use bevy::{app::AppExit, prelude::*};
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::javascript::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, startup)
.add_systems(Update, call_js_on_update_from_rust)
.add_scripting::<JsRuntime>(|runtime| {
runtime.add_function(String::from("quit"), |mut exit: MessageWriter<AppExit>| {
exit.write(AppExit::Success);
});
})
.run();
}
fn startup(mut commands: Commands, assets_server: Res<AssetServer>) {
commands.spawn(Script::<JsScript>::new(
assets_server.load("examples/js/call_function_from_rust.js"),
));
}
fn call_js_on_update_from_rust(
mut scripted_entities: Query<(Entity, &mut JsScriptData)>,
scripting_runtime: ResMut<JsRuntime>,
) {
for (entity, mut script_data) in &mut scripted_entities {
scripting_runtime
.call_fn("on_update", &mut script_data, entity, ())
.unwrap();
}
}

View file

@ -0,0 +1,25 @@
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::javascript::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_scripting::<JsRuntime>(|runtime| {
runtime.add_function(
String::from("get_name"),
|In((BevyEntity(entity),)): In<(BevyEntity,)>, names: Query<&Name>| {
names.get(entity).unwrap().to_string()
},
);
})
.add_systems(Startup, startup)
.run();
}
fn startup(mut commands: Commands, assets_server: Res<AssetServer>) {
commands.spawn((
Name::from("MyEntityName"),
Script::<JsScript>::new(assets_server.load("examples/js/current_entity.js")),
));
}

33
examples/js/ecs.rs Normal file
View file

@ -0,0 +1,33 @@
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::javascript::prelude::*;
#[derive(Component)]
struct Player;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_scripting::<JsRuntime>(|runtime| {
runtime.add_function(
String::from("print_player_names"),
|players: Query<&Name, With<Player>>| {
for player in &players {
println!("player name: {}", player);
}
},
);
})
.add_systems(Startup, startup)
.run();
}
fn startup(mut commands: Commands, assets_server: Res<AssetServer>) {
commands.spawn((Player, Name::new("John")));
commands.spawn((Player, Name::new("Mary")));
commands.spawn((Player, Name::new("Alice")));
commands.spawn(Script::<JsScript>::new(
assets_server.load("examples/js/ecs.js"),
));
}

View file

@ -0,0 +1,17 @@
use bevy::prelude::*;
use bevy_scriptum::runtimes::javascript::prelude::*;
use bevy_scriptum::{BuildScriptingRuntime, prelude::*};
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_scripting::<JsRuntime>(|_| {})
.add_systems(Startup, startup)
.run();
}
fn startup(mut commands: Commands, assets_server: Res<AssetServer>) {
commands.spawn(Script::<JsScript>::new(
assets_server.load("examples/js/entity_variable.js"),
));
}

View file

@ -0,0 +1,40 @@
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::javascript::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_scripting::<JsRuntime>(|runtime| {
runtime
.add_function(String::from("fun_without_params"), || {
println!("called without params");
})
.add_function(
String::from("fun_with_string_param"),
|In((x,)): In<(String,)>| {
println!("called with string: '{}'", x);
},
)
.add_function(
String::from("fun_with_i64_param"),
|In((x,)): In<(i64,)>| {
println!("called with i64: {}", x);
},
)
.add_function(
String::from("fun_with_multiple_params"),
|In((x, y)): In<(i64, String)>| {
println!("called with i64: {} and string: '{}'", x, y);
},
);
})
.add_systems(Startup, startup)
.run();
}
fn startup(mut commands: Commands, assets_server: Res<AssetServer>) {
commands.spawn(Script::<JsScript>::new(
assets_server.load("examples/js/function_params.js"),
));
}

View file

@ -0,0 +1,21 @@
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::javascript::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_scripting::<JsRuntime>(|runtime| {
runtime.add_function(String::from("hello_bevy"), || {
println!("hello bevy, called from script");
});
})
.add_systems(Startup, startup)
.run();
}
fn startup(mut commands: Commands, assets_server: Res<AssetServer>) {
commands.spawn(Script::<JsScript>::new(
assets_server.load("examples/js/hello_world.js"),
));
}

View file

@ -0,0 +1,67 @@
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::javascript::prelude::*;
// Plugin A
struct PluginA;
impl Plugin for PluginA {
fn build(&self, app: &mut App) {
app.add_scripting_api::<JsRuntime>(|runtime| {
runtime.add_function(String::from("hello_from_plugin_a"), || {
info!("Hello from Plugin A");
});
})
.add_systems(Startup, plugin_a_startup);
}
}
fn plugin_a_startup(mut commands: Commands, assets_server: Res<AssetServer>) {
commands.spawn(Script::<JsScript>::new(
assets_server.load("examples/js/multiple_plugins_plugin_a.js"),
));
}
// Plugin B
struct PluginB;
impl Plugin for PluginB {
fn build(&self, app: &mut App) {
app.add_scripting_api::<JsRuntime>(|runtime| {
runtime.add_function(
String::from("hello_from_plugin_b_with_parameters"),
hello_from_b,
);
})
.add_systems(Startup, plugin_b_startup);
}
}
fn plugin_b_startup(mut commands: Commands, assets_server: Res<AssetServer>) {
commands.spawn(Script::<JsScript>::new(
assets_server.load("examples/js/multiple_plugins_plugin_b.js"),
));
}
fn hello_from_b(In((text, x)): In<(String, i32)>) {
info!("{} from Plugin B: {}", text, x);
}
// Main
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_scripting::<JsRuntime>(|runtime| {
runtime.add_function(String::from("hello_bevy"), || {
info!("hello bevy, called from script");
});
})
.add_systems(Startup, main_startup)
.add_plugins(PluginA)
.add_plugins(PluginB)
.run();
}
fn main_startup(mut commands: Commands, assets_server: Res<AssetServer>) {
commands.spawn(Script::<JsScript>::new(
assets_server.load("examples/js/hello_world.js"),
));
}

View file

@ -0,0 +1,23 @@
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::javascript::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_scripting::<JsRuntime>(|runtime| {
runtime.add_function(String::from("hello_bevy"), hello_bevy_callback_system);
})
.add_systems(Startup, startup)
.run();
}
fn startup(mut commands: Commands, assets_server: Res<AssetServer>) {
commands.spawn(Script::<JsScript>::new(
assets_server.load("examples/js/hello_world.js"),
));
}
fn hello_bevy_callback_system() {
println!("hello bevy, called from script");
}

31
examples/js/promises.rs Normal file
View file

@ -0,0 +1,31 @@
use bevy::prelude::*;
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::javascript::prelude::*;
#[derive(Component)]
struct Player;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_scripting::<JsRuntime>(|builder| {
builder.add_function(
String::from("get_player_name"),
|player_names: Query<&Name, With<Player>>| {
player_names
.single()
.expect("Missing player_names")
.to_string()
},
);
})
.add_systems(Startup, startup)
.run();
}
fn startup(mut commands: Commands, assets_server: Res<AssetServer>) {
commands.spawn((Player, Name::new("John")));
commands.spawn(Script::<JsScript>::new(
assets_server.load("examples/js/promises.js"),
));
}

View file

@ -0,0 +1,43 @@
use bevy::{app::AppExit, prelude::*};
use bevy_scriptum::prelude::*;
use bevy_scriptum::runtimes::javascript::prelude::*;
fn main() {
App::new()
// This is just needed for headless console app, not needed for a regular bevy application
// that uses a winit window
.set_runner(move |mut app: App| {
loop {
app.update();
if let Some(exit) = app.should_exit() {
return exit;
}
}
})
.add_plugins(DefaultPlugins)
.add_systems(Startup, startup)
.add_systems(Update, print_entity_names_and_quit)
.add_scripting::<JsRuntime>(|runtime| {
runtime.add_function(String::from("spawn_entity"), spawn_entity);
})
.run();
}
fn spawn_entity(mut commands: Commands) {
commands.spawn(Name::new("SpawnedEntity"));
}
fn startup(mut commands: Commands, assets_server: Res<AssetServer>) {
commands.spawn((Script::<JsScript>::new(
assets_server.load("examples/js/side_effects.js"),
),));
}
fn print_entity_names_and_quit(query: Query<&Name>, mut exit: MessageWriter<AppExit>) {
if !query.is_empty() {
for e in &query {
println!("{}", e);
}
exit.write(AppExit::Success);
}
}

View file

@ -1,6 +1,6 @@
use bevy::prelude::*;
use bevy_scriptum::runtimes::lua::prelude::*;
use bevy_scriptum::{prelude::*, BuildScriptingRuntime};
use bevy_scriptum::{BuildScriptingRuntime, prelude::*};
fn main() {
App::new()

View file

@ -11,7 +11,12 @@ fn main() {
.add_scripting::<LuaRuntime>(|builder| {
builder.add_function(
String::from("get_player_name"),
|player_names: Query<&Name, With<Player>>| player_names.single().expect("Missing player_names").to_string(),
|player_names: Query<&Name, With<Player>>| {
player_names
.single()
.expect("Missing player_names")
.to_string()
},
);
})
.add_systems(Startup, startup)

View file

@ -1,6 +1,6 @@
use bevy::prelude::*;
use bevy_scriptum::runtimes::rhai::prelude::*;
use bevy_scriptum::{prelude::*, BuildScriptingRuntime};
use bevy_scriptum::{BuildScriptingRuntime, prelude::*};
fn main() {
App::new()

View file

@ -11,7 +11,12 @@ fn main() {
.add_scripting::<RhaiRuntime>(|builder| {
builder.add_function(
String::from("get_player_name"),
|player_names: Query<&Name, With<Player>>| player_names.single().expect("Missing player_names").to_string(),
|player_names: Query<&Name, With<Player>>| {
player_names
.single()
.expect("Missing player_names")
.to_string()
},
);
})
.add_systems(Startup, startup)

View file

@ -1,6 +1,6 @@
use bevy::prelude::*;
use bevy_scriptum::runtimes::ruby::prelude::*;
use bevy_scriptum::{prelude::*, BuildScriptingRuntime};
use bevy_scriptum::{BuildScriptingRuntime, prelude::*};
fn main() {
App::new()

View file

@ -1,9 +1,9 @@
use std::marker::PhantomData;
use bevy::{
asset::{io::Reader, Asset, AssetLoader, LoadContext},
tasks::ConditionalSendFuture,
asset::{Asset, AssetLoader, LoadContext, io::Reader},
reflect::TypePath,
tasks::ConditionalSendFuture,
};
/// A loader for script assets.

View file

@ -9,6 +9,7 @@
//! | 🌙 LuaJIT | `lua` | [link](https://jarkonik.github.io/bevy_scriptum/lua/lua.html) |
//! | 🌾 Rhai | `rhai` | [link](https://jarkonik.github.io/bevy_scriptum/rhai/rhai.html) |
//! | 💎 Ruby(currently only supported on Linux and MacOS) | `ruby` | [link](https://jarkonik.github.io/bevy_scriptum/ruby/ruby.html) |
//! | 🟨 JavaScript (V8 via deno_core) | `javascript` | [link](https://jarkonik.github.io/bevy_scriptum/javascript/javascript.html) |
//!
//! Documentation book is available [here](https://jarkonik.github.io/bevy_scriptum/) 📖
//!
@ -266,7 +267,7 @@ use self::{
systems::{process_new_scripts, reload_scripts},
};
#[cfg(any(feature = "rhai", feature = "lua"))]
#[cfg(any(feature = "rhai", feature = "lua", feature = "javascript"))]
const ENTITY_VAR_NAME: &str = "entity";
/// An error that can occur when internal [ScriptingPlugin] systems are being executed

View file

@ -58,7 +58,12 @@ impl<C: Clone + Send + 'static, V: Send + Clone> Promise<C, V> {
}
/// Register a callback that will be called when the [Promise] is resolved.
#[cfg(any(feature = "rhai", feature = "lua", feature = "ruby"))]
#[cfg(any(
feature = "rhai",
feature = "lua",
feature = "ruby",
feature = "javascript"
))]
pub(crate) fn then(&mut self, callback: V) -> Self {
let mut inner = self
.inner

776
src/runtimes/javascript.rs Normal file
View file

@ -0,0 +1,776 @@
use std::{
cell::{Cell, RefCell},
collections::HashMap,
rc::Rc,
sync::{Arc, Mutex},
thread::{self, JoinHandle},
};
use bevy::{
asset::Asset,
ecs::{component::Component, entity::Entity, resource::Resource, schedule::ScheduleLabel},
math::Vec3,
reflect::TypePath,
};
use deno_core::{JsRuntime as DenoRuntime, RuntimeOptions};
pub use deno_core::v8;
use serde::Deserialize;
use crate::{
ENTITY_VAR_NAME, FuncArgs, Runtime, ScriptingError,
assets::GetExtensions,
callback::{FromRuntimeValueWithEngine, IntoRuntimeValueWithEngine},
promise::Promise,
};
/// Boxed registered callback function as stored on the JavaScript thread.
type CallbackFn =
dyn Fn((), Vec<JsValue>) -> Result<Promise<(), JsValue>, ScriptingError> + 'static;
/// A queue of value ids that have been dropped on another thread and need to be
/// removed from the value registry the next time the JavaScript thread runs.
type FreeQueue = Arc<Mutex<Vec<u64>>>;
thread_local! {
/// Registry of live `v8::Global` values, keyed by the id carried in a [JsValue].
static REGISTRY: RefCell<HashMap<u64, v8::Global<v8::Value>>> = RefCell::new(HashMap::new());
/// Registry of pending promises, keyed by the id stored in the script-side
/// promise object.
static PROMISES: RefCell<HashMap<u64, Promise<(), JsValue>>> = RefCell::new(HashMap::new());
/// Registered Rust callback functions, keyed by their script-visible name.
static CALLBACKS: RefCell<HashMap<String, Rc<CallbackFn>>> = RefCell::new(HashMap::new());
static NEXT_VALUE_ID: Cell<u64> = const { Cell::new(0) };
static NEXT_PROMISE_ID: Cell<u64> = const { Cell::new(0) };
static FREE_QUEUE: FreeQueue = Arc::new(Mutex::new(Vec::new()));
}
/// A handle to a value living inside the JavaScript engine.
///
/// The actual `v8::Global` is kept in a thread-local registry on the JavaScript
/// thread - this handle only carries an id (plus a shared free-list so it can be
/// cleaned up when dropped from any thread), which makes it cheap to `Clone` and
/// safe to `Send`.
#[derive(Clone)]
pub struct JsValue(Arc<JsValueInner>);
struct JsValueInner {
id: u64,
free: FreeQueue,
}
impl Drop for JsValueInner {
fn drop(&mut self) {
if let Ok(mut queue) = self.free.lock() {
queue.push(self.id);
}
}
}
/// Removes any values that were dropped on other threads from the registry.
/// Must be called on the JavaScript thread while the isolate is entered.
fn drain_free_queue() {
let ids: Vec<u64> = FREE_QUEUE.with(|queue| {
queue
.lock()
.map(|mut queue| queue.drain(..).collect())
.unwrap_or_default()
});
if !ids.is_empty() {
REGISTRY.with(|registry| {
let mut registry = registry.borrow_mut();
for id in ids {
registry.remove(&id);
}
});
}
}
/// Stores a `v8::Local` value in the registry and returns a [JsValue] handle for it.
fn store_value(scope: &mut v8::PinScope, local: v8::Local<v8::Value>) -> JsValue {
let global = v8::Global::new(scope, local);
let id = NEXT_VALUE_ID.with(|next| {
let id = next.get();
next.set(id + 1);
id
});
REGISTRY.with(|registry| registry.borrow_mut().insert(id, global));
let free = FREE_QUEUE.with(|queue| queue.clone());
JsValue(Arc::new(JsValueInner { id, free }))
}
/// Loads the `v8::Local` value referenced by a [JsValue] handle.
fn load_value<'s>(scope: &mut v8::PinScope<'s, '_>, value: &JsValue) -> v8::Local<'s, v8::Value> {
let global = REGISTRY
.with(|registry| registry.borrow().get(&value.0.id).cloned())
.expect("JsValue referenced a value that is no longer in the registry");
v8::Local::new(scope, global)
}
fn v8_string<'s>(scope: &mut v8::PinScope<'s, '_>, value: &str) -> v8::Local<'s, v8::Value> {
v8::String::new(scope, value)
.expect("Failed to allocate JavaScript string")
.into()
}
fn global_object<'s>(scope: &mut v8::PinScope<'s, '_>) -> v8::Local<'s, v8::Object> {
scope.get_current_context().global(scope)
}
fn set_entity(scope: &mut v8::PinScope, entity: Entity) {
let global = global_object(scope);
let key = v8_string(scope, ENTITY_VAR_NAME);
let value = BevyEntity(entity).to_v8(scope);
global.set(scope, key, value);
}
fn clear_entity(scope: &mut v8::PinScope) {
let global = global_object(scope);
let key = v8_string(scope, ENTITY_VAR_NAME);
let undefined: v8::Local<v8::Value> = v8::undefined(scope).into();
global.set(scope, key, undefined);
}
/// Wraps a bevy_scriptum [Promise] into a script-side object exposing `and_then`.
fn promise_to_v8<'s>(
scope: &mut v8::PinScope<'s, '_>,
promise: Promise<(), JsValue>,
) -> v8::Local<'s, v8::Value> {
let id = NEXT_PROMISE_ID.with(|next| {
let id = next.get();
next.set(id + 1);
id
});
PROMISES.with(|promises| promises.borrow_mut().insert(id, promise));
let object = v8::Object::new(scope);
let key = v8_string(scope, "and_then");
let data: v8::Local<v8::Value> = v8::Number::new(scope, id as f64).into();
let function = v8::Function::builder(and_then_callback)
.data(data)
.build(scope)
.expect("Failed to create and_then function");
object.set(scope, key, function.into());
object.into()
}
/// `and_then` method on script-side promise objects.
fn and_then_callback(
scope: &mut v8::PinScope,
args: v8::FunctionCallbackArguments,
mut rv: v8::ReturnValue<v8::Value>,
) {
let id = args
.data()
.number_value(scope)
.expect("Promise object is missing its id") as u64;
let callback = store_value(scope, args.get(0));
let following = PROMISES.with(|promises| {
let mut promises = promises.borrow_mut();
let promise = promises
.get_mut(&id)
.expect("Promise referenced by script no longer exists");
promise.then(callback)
});
let object = promise_to_v8(scope, following);
rv.set(object);
}
/// Callback invoked by V8 for every registered Rust function.
fn native_callback(
scope: &mut v8::PinScope,
args: v8::FunctionCallbackArguments,
mut rv: v8::ReturnValue<v8::Value>,
) {
let name = args.data().to_rust_string_lossy(scope);
let mut params = Vec::with_capacity(args.length() as usize);
for i in 0..args.length() {
params.push(store_value(scope, args.get(i)));
}
let callback = CALLBACKS.with(|callbacks| callbacks.borrow().get(&name).cloned());
if let Some(callback) = callback {
let promise = callback((), params).expect("Failed to call registered function");
let object = promise_to_v8(scope, promise);
rv.set(object);
}
}
fn print_callback(
scope: &mut v8::PinScope,
args: v8::FunctionCallbackArguments,
_rv: v8::ReturnValue<v8::Value>,
) {
let mut parts = Vec::with_capacity(args.length() as usize);
for i in 0..args.length() {
parts.push(args.get(i).to_rust_string_lossy(scope));
}
println!("{}", parts.join(" "));
}
/// `Vec3(x, y, z)` global constructor available to all scripts.
fn vec3_callback(
scope: &mut v8::PinScope,
args: v8::FunctionCallbackArguments,
mut rv: v8::ReturnValue<v8::Value>,
) {
let component = |scope: &mut v8::PinScope, index: i32| {
args.get(index).number_value(scope).unwrap_or(0.0) as f32
};
let x = component(scope, 0);
let y = component(scope, 1);
let z = component(scope, 2);
let value = BevyVec3::new(x, y, z).to_v8(scope);
rv.set(value);
}
/// Registers a global function on the engine's global object.
fn register_global_function(
scope: &mut v8::PinScope,
name: &str,
callback: impl v8::MapFnTo<v8::FunctionCallback>,
) {
let global = global_object(scope);
let key = v8_string(scope, name);
let function = v8::Function::new(scope, callback)
.unwrap_or_else(|| panic!("Failed to create `{name}` function"));
global.set(scope, key, function.into());
}
/// Calls a global script function by name with the provided arguments.
fn call_global_fn(
scope: &mut v8::PinScope,
name: &str,
args: &[JsValue],
) -> Result<JsValue, ScriptingError> {
let global = global_object(scope);
let key = v8_string(scope, name);
let value = global
.get(scope, key)
.ok_or_else(|| ScriptingError::RuntimeError(format!("function `{name}` not found")))?;
let function = value
.try_cast::<v8::Function>()
.map_err(|_| ScriptingError::RuntimeError(format!("`{name}` is not a function")))?;
let arguments: Vec<v8::Local<v8::Value>> =
args.iter().map(|arg| load_value(scope, arg)).collect();
call_function(scope, function, &arguments)
}
/// Calls a `v8::Function` with the provided arguments, capturing runtime errors.
fn call_function(
scope: &mut v8::PinScope,
function: v8::Local<v8::Function>,
arguments: &[v8::Local<v8::Value>],
) -> Result<JsValue, ScriptingError> {
let recv: v8::Local<v8::Value> = v8::undefined(scope).into();
v8::tc_scope!(let scope, scope);
match function.call(scope, recv, arguments) {
Some(result) => Ok(store_value(scope, result)),
None => {
let message = scope
.exception()
.map(|exception| exception.to_rust_string_lossy(scope))
.unwrap_or_else(|| String::from("unknown JavaScript error"));
Err(ScriptingError::RuntimeError(message))
}
}
}
/// The JavaScript engine - a thin wrapper around a deno_core [JsRuntime] that
/// allows obtaining a V8 scope through a shared reference (the rest of
/// bevy_scriptum only hands out `&RawEngine`).
pub struct JsEngine {
runtime: RefCell<DenoRuntime>,
}
impl JsEngine {
fn new() -> Self {
let engine = Self {
runtime: RefCell::new(DenoRuntime::new(RuntimeOptions::default())),
};
engine.with_scope(|scope| {
register_global_function(scope, "print", print_callback);
register_global_function(scope, "Vec3", vec3_callback);
});
engine
}
/// Runs `f` with a V8 handle/context scope ready for use.
pub fn with_scope<R>(&self, f: impl FnOnce(&mut v8::PinScope) -> R) -> R {
let mut runtime = self.runtime.borrow_mut();
let context = runtime.main_context();
let isolate = &mut *runtime.v8_isolate();
v8::scope!(let scope, isolate);
let context = v8::Local::new(scope, context);
let scope = &mut v8::ContextScope::new(scope, context);
drain_free_queue();
f(scope)
}
}
#[derive(Resource)]
pub struct JsRuntime {
thread: Option<JsThread>,
}
impl Default for JsRuntime {
fn default() -> Self {
Self {
thread: Some(JsThread::spawn()),
}
}
}
impl JsRuntime {
fn execute_in_thread<T: Send + 'static>(
&self,
f: impl FnOnce(&mut JsEngine) -> T + Send + 'static,
) -> T {
self.thread
.as_ref()
.expect("JavaScript thread is gone")
.execute(Box::new(f))
}
/// Runs `f` with a V8 scope on the JavaScript thread. Useful for directly
/// interacting with the engine, e.g. from tests.
pub fn with_scope<T: Send + 'static>(
&self,
f: impl FnOnce(&mut v8::PinScope) -> T + Send + 'static,
) -> T {
self.execute_in_thread(move |engine| engine.with_scope(f))
}
}
type JsClosure = Box<dyn FnOnce(&mut JsEngine) + Send>;
/// A dedicated thread owning the (non-`Send`) V8 isolate.
struct JsThread {
sender: Option<crossbeam_channel::Sender<JsClosure>>,
handle: Option<JoinHandle<()>>,
}
impl JsThread {
fn spawn() -> Self {
let (sender, receiver) = crossbeam_channel::unbounded::<JsClosure>();
let handle = thread::spawn(move || {
let mut engine = JsEngine::new();
while let Ok(f) = receiver.recv() {
f(&mut engine);
}
});
JsThread {
sender: Some(sender),
handle: Some(handle),
}
}
fn execute<T: Send + 'static>(&self, f: Box<dyn FnOnce(&mut JsEngine) -> T + Send>) -> T {
let (return_sender, return_receiver) = crossbeam_channel::bounded(1);
self.sender
.as_ref()
.expect("JavaScript thread sender is gone")
.send(Box::new(move |engine| {
let _ = return_sender.send(f(engine));
}))
.expect("Failed to send execution unit to JavaScript thread");
return_receiver
.recv()
.expect("Failed to receive callback return value from JavaScript thread")
}
}
impl Drop for JsThread {
fn drop(&mut self) {
drop(self.sender.take());
if let Some(handle) = self.handle.take() {
let _ = handle.join();
}
}
}
#[derive(ScheduleLabel, Clone, PartialEq, Eq, Debug, Hash, Default)]
pub struct JsSchedule;
#[derive(Asset, Debug, Deserialize, TypePath)]
pub struct JsScript(pub String);
impl GetExtensions for JsScript {
fn extensions() -> &'static [&'static str] {
&["js"]
}
}
impl From<String> for JsScript {
fn from(value: String) -> Self {
Self(value)
}
}
#[derive(Component)]
pub struct JsScriptData;
#[derive(Debug, Clone, Copy)]
pub struct BevyEntity(pub Entity);
impl BevyEntity {
pub fn index(&self) -> u32 {
self.0.index_u32()
}
}
#[derive(Debug, Clone, Copy)]
pub struct BevyVec3(pub Vec3);
impl BevyVec3 {
pub fn new(x: f32, y: f32, z: f32) -> Self {
Self(Vec3::new(x, y, z))
}
pub fn x(&self) -> f32 {
self.0.x
}
pub fn y(&self) -> f32 {
self.0.y
}
pub fn z(&self) -> f32 {
self.0.z
}
}
impl Runtime for JsRuntime {
type Schedule = JsSchedule;
type ScriptAsset = JsScript;
type ScriptData = JsScriptData;
type CallContext = ();
type Value = JsValue;
type RawEngine = JsEngine;
fn with_engine_send_mut<T: Send + 'static>(
&mut self,
f: impl FnOnce(&mut Self::RawEngine) -> T + Send + 'static,
) -> T {
self.execute_in_thread(f)
}
fn with_engine_send<T: Send + 'static>(
&self,
f: impl FnOnce(&Self::RawEngine) -> T + Send + 'static,
) -> T {
self.execute_in_thread(move |engine| f(engine))
}
fn with_engine_mut<T>(&mut self, _f: impl FnOnce(&mut Self::RawEngine) -> T) -> T {
unimplemented!(
"JavaScript runtime requires sending execution to another thread, use `with_engine_send_mut`"
);
}
fn with_engine<T>(&self, _f: impl FnOnce(&Self::RawEngine) -> T) -> T {
unimplemented!(
"JavaScript runtime requires sending execution to another thread, use `with_engine_send`"
);
}
fn eval(
&self,
script: &Self::ScriptAsset,
entity: Entity,
) -> Result<Self::ScriptData, ScriptingError> {
let source = script.0.clone();
self.execute_in_thread(move |engine| {
engine.with_scope(|scope| set_entity(scope, entity));
let result = engine
.runtime
.borrow_mut()
.execute_script("[bevy_scriptum]", source);
engine.with_scope(clear_entity);
result
.map(|_| JsScriptData)
.map_err(|e| ScriptingError::RuntimeError(e.to_string()))
})
}
fn register_fn(
&mut self,
name: String,
_arg_types: Vec<std::any::TypeId>,
f: impl Fn(
Self::CallContext,
Vec<Self::Value>,
) -> Result<Promise<Self::CallContext, Self::Value>, ScriptingError>
+ Send
+ Sync
+ 'static,
) -> Result<(), ScriptingError> {
self.execute_in_thread(move |engine| {
CALLBACKS.with(|callbacks| {
callbacks.borrow_mut().insert(name.clone(), Rc::new(f));
});
engine.with_scope(|scope| {
let global = global_object(scope);
let key = v8_string(scope, &name);
let data = v8_string(scope, &name);
let function = v8::Function::builder(native_callback)
.data(data)
.build(scope)
.expect("Failed to create registered function");
global.set(scope, key, function.into());
});
});
Ok(())
}
fn call_fn(
&self,
name: &str,
_script_data: &mut Self::ScriptData,
entity: Entity,
args: impl for<'a> FuncArgs<'a, Self::Value, Self> + Send + 'static,
) -> Result<Self::Value, ScriptingError> {
let name = name.to_string();
self.execute_in_thread(move |engine| {
let args = args.parse(engine);
engine.with_scope(|scope| {
set_entity(scope, entity);
let result = call_global_fn(scope, &name, &args);
clear_entity(scope);
result
})
})
}
fn call_fn_from_value(
&self,
value: &Self::Value,
_context: &Self::CallContext,
args: Vec<Self::Value>,
) -> Result<Self::Value, ScriptingError> {
let value = value.clone();
self.execute_in_thread(move |engine| {
engine.with_scope(|scope| {
let local = load_value(scope, &value);
let function = local.try_cast::<v8::Function>().map_err(|_| {
ScriptingError::RuntimeError(String::from("value is not a function"))
})?;
let arguments: Vec<v8::Local<v8::Value>> =
args.iter().map(|arg| load_value(scope, arg)).collect();
call_function(scope, function, &arguments)
})
})
}
}
/// Conversion from a Rust value into a V8 value.
trait ToV8 {
fn to_v8<'s>(self, scope: &mut v8::PinScope<'s, '_>) -> v8::Local<'s, v8::Value>;
}
/// Conversion from a V8 value into a Rust value.
trait FromV8 {
fn from_v8(scope: &mut v8::PinScope, value: v8::Local<v8::Value>) -> Self;
}
macro_rules! impl_number {
($($t:ty),*) => {$(
impl ToV8 for $t {
fn to_v8<'s>(self, scope: &mut v8::PinScope<'s, '_>) -> v8::Local<'s, v8::Value> {
v8::Number::new(scope, self as f64).into()
}
}
impl FromV8 for $t {
fn from_v8(scope: &mut v8::PinScope, value: v8::Local<v8::Value>) -> Self {
value.number_value(scope).unwrap_or(0.0) as $t
}
}
)*};
}
impl_number!(i8, i16, i32, i64, isize, u8, u16, u32, u64, usize, f32, f64);
impl ToV8 for String {
fn to_v8<'s>(self, scope: &mut v8::PinScope<'s, '_>) -> v8::Local<'s, v8::Value> {
v8_string(scope, &self)
}
}
impl FromV8 for String {
fn from_v8(scope: &mut v8::PinScope, value: v8::Local<v8::Value>) -> Self {
value.to_rust_string_lossy(scope)
}
}
impl ToV8 for bool {
fn to_v8<'s>(self, scope: &mut v8::PinScope<'s, '_>) -> v8::Local<'s, v8::Value> {
v8::Boolean::new(scope, self).into()
}
}
impl FromV8 for bool {
fn from_v8(scope: &mut v8::PinScope, value: v8::Local<v8::Value>) -> Self {
value.boolean_value(scope)
}
}
impl ToV8 for () {
fn to_v8<'s>(self, scope: &mut v8::PinScope<'s, '_>) -> v8::Local<'s, v8::Value> {
v8::undefined(scope).into()
}
}
impl FromV8 for () {
fn from_v8(_scope: &mut v8::PinScope, _value: v8::Local<v8::Value>) -> Self {}
}
impl ToV8 for BevyEntity {
fn to_v8<'s>(self, scope: &mut v8::PinScope<'s, '_>) -> v8::Local<'s, v8::Value> {
let object = v8::Object::new(scope);
let key = v8_string(scope, "index");
let value: v8::Local<v8::Value> =
v8::Integer::new_from_unsigned(scope, self.index()).into();
object.set(scope, key, value);
let key = v8_string(scope, "__entity_bits");
let value: v8::Local<v8::Value> = v8::BigInt::new_from_u64(scope, self.0.to_bits()).into();
object.set(scope, key, value);
object.into()
}
}
impl FromV8 for BevyEntity {
fn from_v8(scope: &mut v8::PinScope, value: v8::Local<v8::Value>) -> Self {
let object = value.to_object(scope).expect("entity is not an object");
let key = v8_string(scope, "__entity_bits");
let bits = object
.get(scope, key)
.expect("entity object is missing `__entity_bits`")
.try_cast::<v8::BigInt>()
.expect("`__entity_bits` is not a BigInt");
let (bits, _) = bits.u64_value();
BevyEntity(Entity::from_bits(bits))
}
}
impl ToV8 for BevyVec3 {
fn to_v8<'s>(self, scope: &mut v8::PinScope<'s, '_>) -> v8::Local<'s, v8::Value> {
let object = v8::Object::new(scope);
for (name, component) in [("x", self.0.x), ("y", self.0.y), ("z", self.0.z)] {
let key = v8_string(scope, name);
let value: v8::Local<v8::Value> = v8::Number::new(scope, component as f64).into();
object.set(scope, key, value);
}
object.into()
}
}
impl FromV8 for BevyVec3 {
fn from_v8(scope: &mut v8::PinScope, value: v8::Local<v8::Value>) -> Self {
let object = value.to_object(scope).expect("Vec3 is not an object");
let read = |scope: &mut v8::PinScope, name: &str| {
let key = v8_string(scope, name);
object
.get(scope, key)
.and_then(|value| value.number_value(scope))
.unwrap_or(0.0) as f32
};
let x = read(scope, "x");
let y = read(scope, "y");
let z = read(scope, "z");
BevyVec3(Vec3::new(x, y, z))
}
}
impl<'a, T: ToV8> IntoRuntimeValueWithEngine<'a, T, JsRuntime> for T {
fn into_runtime_value_with_engine(value: T, engine: &'a JsEngine) -> JsValue {
engine.with_scope(|scope| {
let local = value.to_v8(scope);
store_value(scope, local)
})
}
}
impl<'a, T: FromV8> FromRuntimeValueWithEngine<'a, JsRuntime> for T {
fn from_runtime_value_with_engine(value: JsValue, engine: &'a JsEngine) -> Self {
engine.with_scope(|scope| {
let local = load_value(scope, &value);
T::from_v8(scope, local)
})
}
}
impl FuncArgs<'_, JsValue, JsRuntime> for () {
fn parse(self, _engine: &JsEngine) -> Vec<JsValue> {
Vec::new()
}
}
impl<T: ToV8> FuncArgs<'_, JsValue, JsRuntime> for Vec<T> {
fn parse(self, engine: &JsEngine) -> Vec<JsValue> {
engine.with_scope(|scope| {
self.into_iter()
.map(|value| {
let local = value.to_v8(scope);
store_value(scope, local)
})
.collect()
})
}
}
pub mod prelude {
pub use super::{BevyEntity, BevyVec3, JsRuntime, JsScript, JsScriptData};
}
macro_rules! impl_tuple {
($($idx:tt $t:tt),+) => {
impl<$($t: ToV8,)+> FuncArgs<'_, JsValue, JsRuntime> for ($($t,)+) {
fn parse(self, engine: &JsEngine) -> Vec<JsValue> {
engine.with_scope(|scope| {
vec![
$({
let local = self.$idx.to_v8(scope);
store_value(scope, local)
},)+
]
})
}
}
};
}
impl_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H, 8 I, 9 J, 10 K, 11 L, 12 M, 13 N, 14 O, 15 P, 16 Q, 17 R, 18 S, 19 T, 20 U, 21 V, 22 W, 23 X, 24 Y, 25 Z);
impl_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H, 8 I, 9 J, 10 K, 11 L, 12 M, 13 N, 14 O, 15 P, 16 Q, 17 R, 18 S, 19 T, 20 U, 21 V, 22 W, 23 X, 24 Y);
impl_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H, 8 I, 9 J, 10 K, 11 L, 12 M, 13 N, 14 O, 15 P, 16 Q, 17 R, 18 S, 19 T, 20 U, 21 V, 22 W, 23 X);
impl_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H, 8 I, 9 J, 10 K, 11 L, 12 M, 13 N, 14 O, 15 P, 16 Q, 17 R, 18 S, 19 T, 20 U, 21 V, 22 W);
impl_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H, 8 I, 9 J, 10 K, 11 L, 12 M, 13 N, 14 O, 15 P, 16 Q, 17 R, 18 S, 19 T, 20 U, 21 V);
impl_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H, 8 I, 9 J, 10 K, 11 L, 12 M, 13 N, 14 O, 15 P, 16 Q, 17 R, 18 S, 19 T, 20 U);
impl_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H, 8 I, 9 J, 10 K, 11 L, 12 M, 13 N, 14 O, 15 P, 16 Q, 17 R, 18 S, 19 T);
impl_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H, 8 I, 9 J, 10 K, 11 L, 12 M, 13 N, 14 O, 15 P, 16 Q, 17 R, 18 S);
impl_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H, 8 I, 9 J, 10 K, 11 L, 12 M, 13 N, 14 O, 15 P, 16 Q, 17 R);
impl_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H, 8 I, 9 J, 10 K, 11 L, 12 M, 13 N, 14 O, 15 P, 16 Q);
impl_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H, 8 I, 9 J, 10 K, 11 L, 12 M, 13 N, 14 O, 15 P);
impl_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H, 8 I, 9 J, 10 K, 11 L, 12 M, 13 N, 14 O);
impl_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H, 8 I, 9 J, 10 K, 11 L, 12 M, 13 N);
impl_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H, 8 I, 9 J, 10 K, 11 L, 12 M);
impl_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H, 8 I, 9 J, 10 K, 11 L);
impl_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H, 8 I, 9 J, 10 K);
impl_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H, 8 I, 9 J);
impl_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H, 8 I);
impl_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H);
impl_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G);
impl_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F);
impl_tuple!(0 A, 1 B, 2 C, 3 D, 4 E);
impl_tuple!(0 A, 1 B, 2 C, 3 D);
impl_tuple!(0 A, 1 B, 2 C);
impl_tuple!(0 A, 1 B);
impl_tuple!(0 A);

View file

@ -1,3 +1,5 @@
#[cfg(feature = "javascript")]
pub mod javascript;
#[cfg(feature = "lua")]
pub mod lua;
#[cfg(feature = "rhai")]

View file

@ -1,17 +1,47 @@
#[cfg(any(feature = "rhai", feature = "lua", feature = "ruby"))]
#[cfg(any(
feature = "rhai",
feature = "lua",
feature = "ruby",
feature = "javascript"
))]
use std::sync::OnceLock;
#[cfg(any(feature = "rhai", feature = "lua", feature = "ruby"))]
#[cfg(any(
feature = "rhai",
feature = "lua",
feature = "ruby",
feature = "javascript"
))]
use bevy::ecs::system::RunSystemOnce as _;
#[cfg(any(feature = "rhai", feature = "lua", feature = "ruby"))]
#[cfg(any(
feature = "rhai",
feature = "lua",
feature = "ruby",
feature = "javascript"
))]
use bevy::prelude::*;
#[cfg(any(feature = "rhai", feature = "lua", feature = "ruby"))]
#[cfg(any(
feature = "rhai",
feature = "lua",
feature = "ruby",
feature = "javascript"
))]
use bevy_scriptum::{FuncArgs, Runtime, prelude::*};
#[cfg(any(feature = "rhai", feature = "lua", feature = "ruby"))]
#[cfg(any(
feature = "rhai",
feature = "lua",
feature = "ruby",
feature = "javascript"
))]
static TRACING_SUBSCRIBER: OnceLock<()> = OnceLock::new();
#[cfg(any(feature = "rhai", feature = "lua", feature = "ruby"))]
#[cfg(any(
feature = "rhai",
feature = "lua",
feature = "ruby",
feature = "javascript"
))]
#[derive(Default, Resource)]
struct TimesCalled {
times_called: u8,
@ -29,7 +59,12 @@ macro_rules! assert_n_times_called {
};
}
#[cfg(any(feature = "rhai", feature = "lua", feature = "ruby"))]
#[cfg(any(
feature = "rhai",
feature = "lua",
feature = "ruby",
feature = "javascript"
))]
fn build_test_app() -> App {
let mut app = App::new();
@ -43,7 +78,12 @@ fn build_test_app() -> App {
app
}
#[cfg(any(feature = "rhai", feature = "lua", feature = "ruby"))]
#[cfg(any(
feature = "rhai",
feature = "lua",
feature = "ruby",
feature = "javascript"
))]
fn run_script<R: Runtime, Out, Marker>(
app: &mut App,
path: String,
@ -60,7 +100,12 @@ fn run_script<R: Runtime, Out, Marker>(
entity_id
}
#[cfg(any(feature = "rhai", feature = "lua", feature = "ruby"))]
#[cfg(any(
feature = "rhai",
feature = "lua",
feature = "ruby",
feature = "javascript"
))]
fn call_script_on_update_from_rust<R: Runtime>(
mut scripted_entities: Query<(Entity, &mut R::ScriptData)>,
scripting_runtime: ResMut<R>,
@ -73,7 +118,12 @@ fn call_script_on_update_from_rust<R: Runtime>(
.unwrap();
}
#[cfg(any(feature = "rhai", feature = "lua", feature = "ruby"))]
#[cfg(any(
feature = "rhai",
feature = "lua",
feature = "ruby",
feature = "javascript"
))]
trait AssertStateKeyValue {
type ScriptData;
fn assert_state_key_value_i64(world: &World, entity_id: Entity, key: &str, value: i64);
@ -81,7 +131,12 @@ trait AssertStateKeyValue {
fn assert_state_key_value_string(world: &World, entity_id: Entity, key: &str, value: &str);
}
#[cfg(any(feature = "rhai", feature = "lua", feature = "ruby"))]
#[cfg(any(
feature = "rhai",
feature = "lua",
feature = "ruby",
feature = "javascript"
))]
macro_rules! scripting_tests {
($runtime:ty, $script:literal, $extension:literal, $entity_type: ty, $vec_type: ty) => {
use super::*;
@ -712,3 +767,60 @@ mod ruby_tests {
scripting_tests!(RubyRuntime, "ruby", "rb", BevyEntity, BevyVec3);
}
#[cfg(feature = "javascript")]
mod javascript_tests {
use bevy::prelude::*;
use bevy_scriptum::runtimes::javascript::{prelude::*, v8};
fn with_state_value<T: Send + 'static>(
world: &World,
key: &str,
f: impl FnOnce(&mut v8::PinScope, v8::Local<v8::Value>) -> T + Send + 'static,
) -> T {
let runtime = world.get_resource::<JsRuntime>().unwrap();
let key = key.to_string();
runtime.with_scope(move |scope| {
let global = scope.get_current_context().global(scope);
let state_key: v8::Local<v8::Value> = v8::String::new(scope, "State").unwrap().into();
let state = global
.get(scope, state_key)
.unwrap()
.to_object(scope)
.unwrap();
let value_key: v8::Local<v8::Value> = v8::String::new(scope, &key).unwrap().into();
let value = state.get(scope, value_key).unwrap();
f(scope, value)
})
}
impl AssertStateKeyValue for JsRuntime {
type ScriptData = JsScriptData;
fn assert_state_key_value_i64(world: &World, _entity_id: Entity, key: &str, value: i64) {
let actual = with_state_value(world, key, |scope, val| {
val.number_value(scope).unwrap() as i64
});
assert_eq!(actual, value);
}
fn assert_state_key_value_i32(world: &World, _entity_id: Entity, key: &str, value: i32) {
let actual = with_state_value(world, key, |scope, val| {
val.number_value(scope).unwrap() as i32
});
assert_eq!(actual, value);
}
fn assert_state_key_value_string(
world: &World,
_entity_id: Entity,
key: &str,
value: &str,
) {
let actual = with_state_value(world, key, |scope, val| val.to_rust_string_lossy(scope));
assert_eq!(actual, value);
}
}
scripting_tests!(JsRuntime, "js", "js", BevyEntity, BevyVec3);
}