diff --git a/Cargo.toml b/Cargo.toml index 71cc86f..9dbd741 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ magnus = { version = "0.7.1", optional = true } rb-sys = { version = "0.9", default-features = false, features = ["link-ruby", "ruby-static"], optional = true } crossbeam-channel = "0.5.15" libc = "0.2.172" +tempfile = "3.20.0" [[example]] name = "call_function_from_rust_rhai" diff --git a/assets/examples/ruby/promises.rb b/assets/examples/ruby/promises.rb index dd0ecbe..54560e3 100644 --- a/assets/examples/ruby/promises.rb +++ b/assets/examples/ruby/promises.rb @@ -1,3 +1,21 @@ -get_player_name.and_then do |name| - puts name +def async_fun + async do + 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 + +async_fun.await +puts "after await" + +quit diff --git a/examples/ruby/promises.rs b/examples/ruby/promises.rs index 1b3ed62..5afe115 100644 --- a/examples/ruby/promises.rs +++ b/examples/ruby/promises.rs @@ -9,15 +9,19 @@ fn main() { App::new() .add_plugins(DefaultPlugins) .add_scripting::(|builder| { - builder.add_function( - String::from("get_player_name"), - |player_names: Query<&Name, With>| { - player_names - .single() - .expect("Missing player_names") - .to_string() - }, - ); + builder + .add_function( + String::from("get_player_name"), + |player_names: Query<&Name, With>| { + player_names + .single() + .expect("Missing player_names") + .to_string() + }, + ) + .add_function(String::from("quit"), |mut exit: EventWriter| { + exit.write(AppExit::Success); + }); }) .add_systems(Startup, startup) .run(); diff --git a/src/lib.rs b/src/lib.rs index 0b6f097..5ee510c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -375,6 +375,8 @@ pub trait Runtime: Resource + Default { fn needs_rdynamic_linking() -> bool { false } + + fn resume(&self, fiber: &Self::Value, value: &Self::Value); } pub trait FuncArgs<'a, V, R: Runtime> { diff --git a/src/promise.rs b/src/promise.rs index 038cc0c..3425521 100644 --- a/src/promise.rs +++ b/src/promise.rs @@ -13,6 +13,8 @@ pub(crate) struct PromiseInner { pub(crate) callbacks: Vec>, #[allow(deprecated)] pub(crate) context: C, + pub(crate) resolved_value: Option, + pub(crate) fibers: Vec, // TODO: should htis be vec or option } /// A struct that represents a Promise. @@ -51,12 +53,31 @@ impl Promise { where R: Runtime, { + let mut fibers: Vec = vec![]; if let Ok(mut inner) = self.inner.lock() { - inner.resolve(runtime, val)?; + inner.resolved_value = Some(val.clone()); + inner.resolve(runtime, val.clone())?; + + for fiber in inner.fibers.drain(..) { + fibers.push(fiber); + } + } + for fiber in fibers { + runtime.resume(&fiber, &val.clone()); } 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. #[cfg(any(feature = "rhai", feature = "lua", feature = "ruby"))] pub(crate) fn then(&mut self, callback: V) -> Self { @@ -65,8 +86,10 @@ impl Promise { .lock() .expect("Failed to lock inner promise mutex"); let following_inner = Arc::new(Mutex::new(PromiseInner { + fibers: vec![], callbacks: vec![], context: inner.context.clone(), + resolved_value: None, })); inner.callbacks.push(PromiseCallback { diff --git a/src/runtimes/ruby.rs b/src/runtimes/ruby.rs index 1ccccdd..7477cb0 100644 --- a/src/runtimes/ruby.rs +++ b/src/runtimes/ruby.rs @@ -1,11 +1,16 @@ use std::{ collections::HashMap, ffi::CString, + io::Write, sync::{Arc, Condvar, LazyLock, Mutex}, thread::{self, JoinHandle}, }; -use ::magnus::{typed_data::Inspect, value::Opaque}; +use ::magnus::{ + Fiber, + typed_data::Inspect, + value::{self, Opaque}, +}; use bevy::{ asset::Asset, ecs::{component::Component, entity::Entity, resource::Resource, schedule::ScheduleLabel}, @@ -19,7 +24,7 @@ use magnus::{ value::{Lazy, ReprValue}, }; use magnus::{method, prelude::*}; -use rb_sys::{VALUE, ruby_init_stack}; +use rb_sys::{VALUE, rb_load, ruby_init_stack}; use serde::Deserialize; use crate::{ @@ -174,6 +179,19 @@ fn then(r_self: magnus::Value) -> magnus::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)] #[magnus::wrap(class = "Bevy::Entity")] pub struct BevyEntity(pub Entity); @@ -195,6 +213,19 @@ impl TryConvert for BevyEntity { #[magnus::wrap(class = "Bevy::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 { pub fn new(x: f32, y: f32, z: f32) -> Self { Self(Vec3::new(x, y, z)) @@ -266,12 +297,15 @@ impl Default for RubyRuntime { let promise = module.define_class("Promise", ruby.class_object())?; 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())?; vec3.define_singleton_method("new", function!(BevyVec3::new, 3))?; vec3.define_method("x", method!(BevyVec3::x, 0))?; vec3.define_method("y", method!(BevyVec3::y, 0))?; vec3.define_method("z", method!(BevyVec3::z, 0))?; + + ruby.define_global_function("async", function!(async_function, 0)); Ok::<(), ScriptingError>(()) })) .expect("Failed to define builtin types"); @@ -392,10 +426,35 @@ impl Runtime for RubyRuntime { ) -> Result { let script = script.0.clone(); self.execute_in_thread(Box::new(move |ruby: &Ruby| { - Self::with_current_entity(ruby, entity, || { - ruby.eval::(&script) - .map_err(>::into) - })?; + let p = Opaque::from(ruby.proc_from_fn(move |ruby, _args, _block| { + Self::with_current_entity(ruby, entity, || { + let mut tmpfile = tempfile::NamedTempFile::new().unwrap(); + 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::(&script) + // .map_err(>::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::(RubyScriptData) })) } @@ -509,6 +568,15 @@ impl Runtime for RubyRuntime { fn needs_rdynamic_linking() -> bool { 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 { diff --git a/src/systems.rs b/src/systems.rs index 7cbf81b..db2c3a5 100644 --- a/src/systems.rs +++ b/src/systems.rs @@ -89,6 +89,8 @@ pub(crate) fn init_callbacks(world: &mut World) -> Result<(), Script move |context, params| { let promise = Promise { inner: Arc::new(Mutex::new(PromiseInner { + resolved_value: None, + fibers: vec![], callbacks: vec![], context, })), @@ -100,7 +102,7 @@ pub(crate) fn init_callbacks(world: &mut World) -> Result<(), Script .expect("Failed to lock callback calls mutex"); calls.push(FunctionCallEvent { - promise: promise.clone(), + promise: promise.clone(), // TODO: dont clone? params, }); Ok(promise)