Compare commits

..

3 commits
fibers ... main

Author SHA1 Message Date
Jarosław Konik
fa70abac23
Add annotations about Ruby only being supported on Linux (#64)
Some checks failed
Book / test (push) Has been cancelled
Deploy book / deploy (push) Has been cancelled
Rust / build (push) Has been cancelled
2025-06-01 18:28:22 +02:00
41d0fd57f3 Bump version
Some checks failed
Book / test (push) Has been cancelled
Deploy book / deploy (push) Has been cancelled
Rust / build (push) Has been cancelled
2025-06-01 14:21:40 +02:00
Curt Reyes
e430795dce
Compile on windows (#62)
* Don't check for rdynamic linking on Windows for now, fixes Windows builds

---------

Co-authored-by: Jaroslaw Konik <konikjar@gmail.com>
2025-06-01 14:19:43 +02:00
10 changed files with 43 additions and 163 deletions

View file

@ -1,7 +1,7 @@
[package] [package]
name = "bevy_scriptum" name = "bevy_scriptum"
authors = ["Jaroslaw Konik <konikjar@gmail.com>"] authors = ["Jaroslaw Konik <konikjar@gmail.com>"]
version = "0.9.0" version = "0.9.1"
edition = "2024" edition = "2024"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
readme = "README.md" readme = "README.md"
@ -35,7 +35,6 @@ magnus = { version = "0.7.1", optional = true }
rb-sys = { version = "0.9", default-features = false, features = ["link-ruby", "ruby-static"], optional = true } rb-sys = { version = "0.9", default-features = false, features = ["link-ruby", "ruby-static"], optional = true }
crossbeam-channel = "0.5.15" crossbeam-channel = "0.5.15"
libc = "0.2.172" libc = "0.2.172"
tempfile = "3.20.0"
[[example]] [[example]]
name = "call_function_from_rust_rhai" name = "call_function_from_rust_rhai"

View file

@ -3,21 +3,19 @@
![demo](demo.gif) ![demo](demo.gif)
bevy_scriptum is a a plugin for [Bevy](https://bevyengine.org/) that allows you to write some of your game or application logic in a scripting language. bevy_scriptum is a a plugin for [Bevy](https://bevyengine.org/) that allows you to write some of your game or application logic in a scripting language.
### Supported scripting languages/runtimes ### Supported scripting languages/runtimes
| language/runtime | cargo feature | documentation chapter | | language/runtime | cargo feature | documentation chapter |
| ---------------- | ------------- | --------------------------------------------------------------- | | ------------------------------------------ | ------------- | --------------------------------------------------------------- |
| 🌙 LuaJIT | `lua` | [link](https://jarkonik.github.io/bevy_scriptum/lua/lua.html) | | 🌙 LuaJIT | `lua` | [link](https://jarkonik.github.io/bevy_scriptum/lua/lua.html) |
| 🌾 Rhai | `rhai` | [link](https://jarkonik.github.io/bevy_scriptum/rhai/rhai.html) | | 🌾 Rhai | `rhai` | [link](https://jarkonik.github.io/bevy_scriptum/rhai/rhai.html) |
| 💎 Ruby | `ruby` | [link](https://jarkonik.github.io/bevy_scriptum/ruby/ruby.html) | | 💎 Ruby(currently only supported on Linux) | `ruby` | [link](https://jarkonik.github.io/bevy_scriptum/ruby/ruby.html) |
Documentation book is available [here](https://jarkonik.github.io/bevy_scriptum/) 📖 Documentation book is available [here](https://jarkonik.github.io/bevy_scriptum/) 📖
Full API docs are available at [docs.rs](https://docs.rs/bevy_scriptum/latest/bevy_scriptum/) 🧑‍💻 Full API docs are available at [docs.rs](https://docs.rs/bevy_scriptum/latest/bevy_scriptum/) 🧑‍💻
bevy_scriptum's main advantages include: bevy_scriptum's main advantages include:
- low-boilerplate - low-boilerplate
- easy to use - easy to use
- asynchronicity with a promise-based API - asynchronicity with a promise-based API
@ -27,7 +25,6 @@ bevy_scriptum's main advantages include:
Scripts are separate files that can be hot-reloaded at runtime. This allows you to quickly iterate on your game or application logic without having to recompile it. Scripts are separate files that can be hot-reloaded at runtime. This allows you to quickly iterate on your game or application logic without having to recompile it.
All you need to do is register callbacks on your Bevy app like this: All you need to do is register callbacks on your Bevy app like this:
```rust ```rust
use bevy::prelude::*; use bevy::prelude::*;
use bevy_scriptum::prelude::*; use bevy_scriptum::prelude::*;
@ -42,9 +39,7 @@ App::new()
}) })
.run(); .run();
``` ```
And you can call them in your scripts like this: And you can call them in your scripts like this:
```lua ```lua
hello_bevy() hello_bevy()
``` ```
@ -75,7 +70,6 @@ App::new()
``` ```
You can also pass arguments to your callback functions, just like you would in a regular Bevy system - using `In` structs with tuples: You can also pass arguments to your callback functions, just like you would in a regular Bevy system - using `In` structs with tuples:
```rust ```rust
use bevy::prelude::*; use bevy::prelude::*;
use bevy_scriptum::prelude::*; use bevy_scriptum::prelude::*;
@ -93,9 +87,7 @@ App::new()
}) })
.run(); .run();
``` ```
which you can then call in your script like this: which you can then call in your script like this:
```lua ```lua
fun_with_string_param("Hello world!") fun_with_string_param("Hello world!")
``` ```
@ -164,18 +156,17 @@ You should then see `my_print: 'Hello world!'` printed in your console.
### Provided examples ### Provided examples
You can also try running provided examples by cloning this repository and running `cargo run --example <example_name>_<language_name>`. For example: You can also try running provided examples by cloning this repository and running `cargo run --example <example_name>_<language_name>`. For example:
```bash ```bash
cargo run --example hello_world_lua cargo run --example hello_world_lua
``` ```
The examples live in `examples` directory and their corresponding scripts live in `assets/examples` directory within the repository. The examples live in `examples` directory and their corresponding scripts live in `assets/examples` directory within the repository.
### Bevy compatibility ### Bevy compatibility
| bevy version | bevy_scriptum version | | bevy version | bevy_scriptum version |
| ------------ | --------------------- | |--------------|-----------------------|
| 0.16 | 0.8-0.9 | | 0.16 | 0.8-0.9 |
| 0.15 | 0.7 | | 0.15 | 0.7 |
| 0.14 | 0.6 | | 0.14 | 0.6 |
@ -193,7 +184,6 @@ get_player_name():and_then(function(name)
print(name) print(name)
end) end)
``` ```
which will print out `John` when used with following exposed function: which will print out `John` when used with following exposed function:
```rust ```rust
@ -206,7 +196,7 @@ App::new()
.add_scripting::<LuaRuntime>(|runtime| { .add_scripting::<LuaRuntime>(|runtime| {
runtime.add_function(String::from("get_player_name"), || String::from("John")); runtime.add_function(String::from("get_player_name"), || String::from("John"));
}); });
``` ````
## Access entity from script ## Access entity from script
@ -214,7 +204,6 @@ A variable called `entity` is automatically available to all scripts - it repres
It exposes `index` property that returns bevy entity index. It exposes `index` property that returns bevy entity index.
It is useful for accessing entity's components from scripts. It is useful for accessing entity's components from scripts.
It can be used in the following way: It can be used in the following way:
```lua ```lua
print("Current entity index: " .. entity.index) print("Current entity index: " .. entity.index)
``` ```

View file

@ -1,21 +1,3 @@
def async_fun get_player_name.and_then do |name|
async do puts name
a = get_player_name
b = a
puts '0'
puts a.await
puts '1'
u = get_player_name
puts b.await
puts '2'
z = get_player_name
puts z
puts z.await
puts "end"
end
end end
async_fun.await
puts "after await"
quit

View file

@ -1,5 +1,7 @@
# Installation # Installation
Ruby is currently only supported on Linux.
## Ruby ## Ruby
To build `bevy_scriptum` with Ruby support a Ruby installation is needed to be To build `bevy_scriptum` with Ruby support a Ruby installation is needed to be

View file

@ -1,3 +1,4 @@
# Ruby # Ruby
This chapter demonstrates how to work with bevy_scriptum when using Ruby language runtime. This chapter demonstrates how to work with bevy_scriptum when using Ruby language runtime.
Ruby is currently only supported on Linux.

View file

@ -9,19 +9,15 @@ fn main() {
App::new() App::new()
.add_plugins(DefaultPlugins) .add_plugins(DefaultPlugins)
.add_scripting::<RubyRuntime>(|builder| { .add_scripting::<RubyRuntime>(|builder| {
builder builder.add_function(
.add_function( String::from("get_player_name"),
String::from("get_player_name"), |player_names: Query<&Name, With<Player>>| {
|player_names: Query<&Name, With<Player>>| { player_names
player_names .single()
.single() .expect("Missing player_names")
.expect("Missing player_names") .to_string()
.to_string() },
}, );
)
.add_function(String::from("quit"), |mut exit: EventWriter<AppExit>| {
exit.write(AppExit::Success);
});
}) })
.add_systems(Startup, startup) .add_systems(Startup, startup)
.run(); .run();

View file

@ -4,11 +4,11 @@
//! ## Supported scripting languages/runtimes //! ## Supported scripting languages/runtimes
//! //!
//! | language/runtime | cargo feature | documentation chapter | //! | language/runtime | cargo feature | documentation chapter |
//! | ----------------- | ------------- | --------------------------------------------------------------- | //! | ------------------------------------------ | ------------- | --------------------------------------------------------------- |
//! | 🌙 LuaJIT | `lua` | [link](https://jarkonik.github.io/bevy_scriptum/lua/lua.html) | //! | 🌙 LuaJIT | `lua` | [link](https://jarkonik.github.io/bevy_scriptum/lua/lua.html) |
//! | 🌾 Rhai | `rhai` | [link](https://jarkonik.github.io/bevy_scriptum/rhai/rhai.html) | //! | 🌾 Rhai | `rhai` | [link](https://jarkonik.github.io/bevy_scriptum/rhai/rhai.html) |
//! | 💎 Ruby | `ruby` | [link](https://jarkonik.github.io/bevy_scriptum/ruby/ruby.html) | //! | 💎 Ruby(currently only supported on Linux) | `ruby` | [link](https://jarkonik.github.io/bevy_scriptum/ruby/ruby.html) |
//! //!
//! Documentation book is available [here](https://jarkonik.github.io/bevy_scriptum/) 📖 //! Documentation book is available [here](https://jarkonik.github.io/bevy_scriptum/) 📖
//! //!
@ -375,8 +375,6 @@ pub trait Runtime: Resource + Default {
fn needs_rdynamic_linking() -> bool { fn needs_rdynamic_linking() -> bool {
false false
} }
fn resume(&self, fiber: &Self::Value, value: &Self::Value);
} }
pub trait FuncArgs<'a, V, R: Runtime> { pub trait FuncArgs<'a, V, R: Runtime> {
@ -512,7 +510,7 @@ impl<R: Runtime> Default for Callbacks<R> {
} }
} }
#[cfg(debug_assertions)] #[cfg(all(debug_assertions, unix))]
pub extern "C" fn is_rdynamic_linking() -> bool { pub extern "C" fn is_rdynamic_linking() -> bool {
unsafe { unsafe {
// Get a function pointer to itself // Get a function pointer to itself
@ -526,6 +524,12 @@ pub extern "C" fn is_rdynamic_linking() -> bool {
} }
} }
#[cfg(any(not(debug_assertions), not(unix)))]
pub extern "C" fn is_rdynamic_linking() -> bool {
// On Windows or in release builds, return a default value
true
}
pub mod prelude { pub mod prelude {
pub use crate::{BuildScriptingRuntime as _, Runtime as _, Script}; pub use crate::{BuildScriptingRuntime as _, Runtime as _, Script};
} }

View file

@ -13,8 +13,6 @@ pub(crate) struct PromiseInner<C: Send, V: Send> {
pub(crate) callbacks: Vec<PromiseCallback<C, V>>, pub(crate) callbacks: Vec<PromiseCallback<C, V>>,
#[allow(deprecated)] #[allow(deprecated)]
pub(crate) context: C, pub(crate) context: C,
pub(crate) resolved_value: Option<V>,
pub(crate) fibers: Vec<V>, // TODO: should htis be vec or option
} }
/// A struct that represents a Promise. /// A struct that represents a Promise.
@ -53,31 +51,12 @@ impl<C: Clone + Send + 'static, V: Send + Clone> Promise<C, V> {
where where
R: Runtime<Value = V, CallContext = C>, R: Runtime<Value = V, CallContext = C>,
{ {
let mut fibers: Vec<V> = vec![];
if let Ok(mut inner) = self.inner.lock() { if let Ok(mut inner) = self.inner.lock() {
inner.resolved_value = Some(val.clone()); inner.resolve(runtime, val)?;
inner.resolve(runtime, val.clone())?;
for fiber in inner.fibers.drain(..) {
fibers.push(fiber);
}
}
for fiber in fibers {
runtime.resume(&fiber, &val.clone());
} }
Ok(()) Ok(())
} }
/// Register a fiber that will be resumed when the [Promise] is resolved.
#[cfg(any(feature = "rhai", feature = "lua", feature = "ruby"))]
pub(crate) fn await_promise(&mut self, fiber: V) {
let mut inner = self
.inner
.lock()
.expect("Failed to lock inner promise mutex");
inner.fibers.push(fiber);
}
/// Register a callback that will be called when the [Promise] is resolved. /// 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"))]
pub(crate) fn then(&mut self, callback: V) -> Self { pub(crate) fn then(&mut self, callback: V) -> Self {
@ -86,10 +65,8 @@ impl<C: Clone + Send + 'static, V: Send + Clone> Promise<C, V> {
.lock() .lock()
.expect("Failed to lock inner promise mutex"); .expect("Failed to lock inner promise mutex");
let following_inner = Arc::new(Mutex::new(PromiseInner { let following_inner = Arc::new(Mutex::new(PromiseInner {
fibers: vec![],
callbacks: vec![], callbacks: vec![],
context: inner.context.clone(), context: inner.context.clone(),
resolved_value: None,
})); }));
inner.callbacks.push(PromiseCallback { inner.callbacks.push(PromiseCallback {

View file

@ -1,16 +1,11 @@
use std::{ use std::{
collections::HashMap, collections::HashMap,
ffi::CString, ffi::CString,
io::Write,
sync::{Arc, Condvar, LazyLock, Mutex}, sync::{Arc, Condvar, LazyLock, Mutex},
thread::{self, JoinHandle}, thread::{self, JoinHandle},
}; };
use ::magnus::{ use ::magnus::{typed_data::Inspect, value::Opaque};
Fiber,
typed_data::Inspect,
value::{self, Opaque},
};
use bevy::{ use bevy::{
asset::Asset, asset::Asset,
ecs::{component::Component, entity::Entity, resource::Resource, schedule::ScheduleLabel}, ecs::{component::Component, entity::Entity, resource::Resource, schedule::ScheduleLabel},
@ -24,7 +19,7 @@ use magnus::{
value::{Lazy, ReprValue}, value::{Lazy, ReprValue},
}; };
use magnus::{method, prelude::*}; use magnus::{method, prelude::*};
use rb_sys::{VALUE, rb_load, ruby_init_stack}; use rb_sys::{VALUE, ruby_init_stack};
use serde::Deserialize; use serde::Deserialize;
use crate::{ use crate::{
@ -179,19 +174,6 @@ fn then(r_self: magnus::Value) -> magnus::Value {
.into_value() .into_value()
} }
fn await_promise(r_self: magnus::Value) -> magnus::Value {
let promise: &Promise<(), RubyValue> =
TryConvert::try_convert(r_self).expect("Couldn't convert self to Promise");
let ruby =
Ruby::get().expect("Failed to get a handle to Ruby API when registering Promise callback");
let fiber = Opaque::from(ruby.fiber_current().as_value());
if let Some(value) = &promise.inner.try_lock().unwrap().resolved_value {
return ruby.get_inner(value.0);
}
promise.clone().await_promise(RubyValue(fiber)).into_value();
ruby.fiber_yield::<_, magnus::Value>(()).unwrap()
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
#[magnus::wrap(class = "Bevy::Entity")] #[magnus::wrap(class = "Bevy::Entity")]
pub struct BevyEntity(pub Entity); pub struct BevyEntity(pub Entity);
@ -213,19 +195,6 @@ impl TryConvert for BevyEntity {
#[magnus::wrap(class = "Bevy::Vec3")] #[magnus::wrap(class = "Bevy::Vec3")]
pub struct BevyVec3(pub Vec3); pub struct BevyVec3(pub Vec3);
pub fn async_function() {
let ruby = Ruby::get().unwrap();
let fiber = ruby
.fiber_new_from_fn(Default::default(), move |ruby, _args, _block| {
let p = ruby.block_proc().unwrap();
p.call::<_, value::Value>(()).unwrap();
Ok(())
})
.unwrap();
fiber.resume::<_, magnus::Value>(()).unwrap();
}
impl BevyVec3 { impl BevyVec3 {
pub fn new(x: f32, y: f32, z: f32) -> Self { pub fn new(x: f32, y: f32, z: f32) -> Self {
Self(Vec3::new(x, y, z)) Self(Vec3::new(x, y, z))
@ -297,15 +266,12 @@ impl Default for RubyRuntime {
let promise = module.define_class("Promise", ruby.class_object())?; let promise = module.define_class("Promise", ruby.class_object())?;
promise.define_method("and_then", magnus::method!(then, 0))?; promise.define_method("and_then", magnus::method!(then, 0))?;
promise.define_method("await", magnus::method!(await_promise, 0))?;
let vec3 = module.define_class("Vec3", ruby.class_object())?; let vec3 = module.define_class("Vec3", ruby.class_object())?;
vec3.define_singleton_method("new", function!(BevyVec3::new, 3))?; vec3.define_singleton_method("new", function!(BevyVec3::new, 3))?;
vec3.define_method("x", method!(BevyVec3::x, 0))?; vec3.define_method("x", method!(BevyVec3::x, 0))?;
vec3.define_method("y", method!(BevyVec3::y, 0))?; vec3.define_method("y", method!(BevyVec3::y, 0))?;
vec3.define_method("z", method!(BevyVec3::z, 0))?; vec3.define_method("z", method!(BevyVec3::z, 0))?;
ruby.define_global_function("async", function!(async_function, 0));
Ok::<(), ScriptingError>(()) Ok::<(), ScriptingError>(())
})) }))
.expect("Failed to define builtin types"); .expect("Failed to define builtin types");
@ -426,35 +392,10 @@ impl Runtime for RubyRuntime {
) -> Result<Self::ScriptData, crate::ScriptingError> { ) -> Result<Self::ScriptData, crate::ScriptingError> {
let script = script.0.clone(); let script = script.0.clone();
self.execute_in_thread(Box::new(move |ruby: &Ruby| { self.execute_in_thread(Box::new(move |ruby: &Ruby| {
let p = Opaque::from(ruby.proc_from_fn(move |ruby, _args, _block| { Self::with_current_entity(ruby, entity, || {
Self::with_current_entity(ruby, entity, || { ruby.eval::<magnus::value::Value>(&script)
let mut tmpfile = tempfile::NamedTempFile::new().unwrap(); .map_err(<magnus::Error as Into<ScriptingError>>::into)
tmpfile.write(script.as_bytes()).unwrap(); })?;
unsafe {
let file = rb_sys::rb_str_new_cstr(
CString::new(tmpfile.path().to_str().unwrap())
.unwrap()
.into_raw(),
);
rb_load(file, 0);
};
// ruby.eval::<magnus::value::Value>(&script)
// .map_err(<magnus::Error as Into<ScriptingError>>::into)
Ok::<(), ScriptingError>(())
})
.unwrap();
}));
let fiber = ruby
.fiber_new_from_fn(Default::default(), move |ruby, _args, _block| {
let p = ruby.get_inner(p);
p.call::<_, value::Value>(()).unwrap();
Ok(())
})
.unwrap();
fiber.resume::<_, value::Value>(()).unwrap();
Ok::<Self::ScriptData, ScriptingError>(RubyScriptData) Ok::<Self::ScriptData, ScriptingError>(RubyScriptData)
})) }))
} }
@ -568,15 +509,6 @@ impl Runtime for RubyRuntime {
fn needs_rdynamic_linking() -> bool { fn needs_rdynamic_linking() -> bool {
true true
} }
fn resume(&self, fiber: &Self::Value, value: &Self::Value) {
let fiber = fiber.clone();
let value = value.clone();
self.execute_in_thread(move |ruby| {
let fiber: Fiber = TryConvert::try_convert(ruby.get_inner(fiber.0)).unwrap();
fiber.resume::<_, magnus::Value>((value.0,));
});
}
} }
pub mod magnus { pub mod magnus {

View file

@ -89,8 +89,6 @@ pub(crate) fn init_callbacks<R: Runtime>(world: &mut World) -> Result<(), Script
move |context, params| { move |context, params| {
let promise = Promise { let promise = Promise {
inner: Arc::new(Mutex::new(PromiseInner { inner: Arc::new(Mutex::new(PromiseInner {
resolved_value: None,
fibers: vec![],
callbacks: vec![], callbacks: vec![],
context, context,
})), })),
@ -102,7 +100,7 @@ pub(crate) fn init_callbacks<R: Runtime>(world: &mut World) -> Result<(), Script
.expect("Failed to lock callback calls mutex"); .expect("Failed to lock callback calls mutex");
calls.push(FunctionCallEvent { calls.push(FunctionCallEvent {
promise: promise.clone(), // TODO: dont clone? promise: promise.clone(),
params, params,
}); });
Ok(promise) Ok(promise)