// TODO: use funcall_public? use std::{ collections::HashMap, ffi::CString, sync::{Arc, Condvar, LazyLock, Mutex}, thread::{self, JoinHandle}, }; use ::magnus::{error::IntoError, typed_data::Inspect, value::Opaque}; use anyhow::anyhow; use bevy::{ asset::Asset, ecs::{component::Component, entity::Entity, resource::Resource, schedule::ScheduleLabel}, math::Vec3, reflect::TypePath, }; use magnus::{ DataType, DataTypeFunctions, IntoValue, Object, RClass, RModule, Ruby, TryConvert, TypedData, block::Proc, data_type_builder, function, value::{Lazy, ReprValue}, }; use magnus::{method, prelude::*}; use rb_sys::{VALUE, rb_backtrace, rb_make_backtrace, ruby_init_stack}; use serde::Deserialize; use crate::{ FuncArgs, Runtime, ScriptingError, assets::GetExtensions, callback::{FromRuntimeValueWithEngine, IntoRuntimeValueWithEngine}, promise::Promise, }; #[derive(Resource)] pub struct RubyRuntime { ruby_thread: Option, } #[derive(ScheduleLabel, Clone, PartialEq, Eq, Debug, Hash, Default)] pub struct RubySchedule; #[derive(Asset, Debug, Deserialize, TypePath)] pub struct RubyScript(pub String); #[derive(Component)] pub struct RubyScriptData; impl GetExtensions for RubyScript { fn extensions() -> &'static [&'static str] { &["rb"] } } impl From for RubyScript { fn from(value: String) -> Self { Self(value) } } type RubyClosure = Box; struct RubyThread { sender: crossbeam_channel::Sender, handle: Option>, } static RUBY_THREAD: LazyLock>, Condvar)>> = LazyLock::new(|| Arc::new((Mutex::new(Some(RubyThread::spawn())), Condvar::new()))); impl RubyThread { fn build_ruby_process_argv() -> anyhow::Result> { Ok(vec![ CString::new("ruby")?.into_raw(), CString::new("-e")?.into_raw(), CString::new("")?.into_raw(), ]) } fn spawn() -> Self { let (sender, receiver) = crossbeam_channel::unbounded::>(); let handle = thread::spawn(move || { unsafe { let mut variable_in_this_stack_frame: VALUE = 0; ruby_init_stack(&mut variable_in_this_stack_frame as *mut VALUE as *mut _); rb_sys::ruby_init(); let mut argv = Self::build_ruby_process_argv().expect("Failed to build ruby process args"); rb_sys::ruby_options(argv.len() as i32, argv.as_mut_ptr()); }; while let Ok(f) = receiver.recv() { let ruby = Ruby::get().expect("Failed to get a handle to Ruby API"); f(ruby); } unsafe { rb_sys::ruby_finalize(); } }); RubyThread { sender, handle: Some(handle), } } fn execute(&self, f: Box T + Send>) -> T { let (return_sender, return_receiver) = crossbeam_channel::bounded(0); self.sender .send(Box::new(move |ruby| { return_sender .send(f(ruby)) .expect("Failed to send callback return value"); })) .expect("Faild to send execution unit to Ruby thread"); return_receiver .recv() .expect("Failed to receive callback return value") } } impl Drop for RubyThread { fn drop(&mut self) { let handle = self.handle.take().expect("No Ruby thread to join"); handle.join().expect("Failed to join Ruby thread"); } } impl DataTypeFunctions for Promise<(), RubyValue> {} unsafe impl TypedData for Promise<(), RubyValue> { fn class(ruby: &Ruby) -> magnus::RClass { static CLASS: Lazy = Lazy::new(|ruby| { let class = ruby .define_module("Bevy") .expect("Failed to define Bevy module") .define_class("Promise", ruby.class_object()) .expect("Failed to define Bevy::Promise class in Ruby"); class.undef_default_alloc_func(); class }); ruby.get_inner(&CLASS) } fn data_type() -> &'static magnus::DataType { static DATA_TYPE: DataType = data_type_builder!(Promise<(), RubyValue>, "Bevy::Promise").build(); &DATA_TYPE } } impl TryConvert for Promise<(), RubyValue> { fn try_convert(val: magnus::Value) -> Result { let result: Result<&Self, _> = TryConvert::try_convert(val); result.cloned() } } fn then(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"); promise .clone() .then(RubyValue::new( if ruby.block_given() { ruby.block_proc() .expect("Failed to create Proc for Promise") } else { ruby.proc_new(|ruby, _, _| ruby.qnil().as_value()) } .as_value(), )) .into_value() } #[derive(Clone)] #[magnus::wrap(class = "Bevy::Entity")] pub struct BevyEntity(pub Entity); impl BevyEntity { pub fn index(&self) -> u32 { self.0.index() } } impl TryConvert for BevyEntity { fn try_convert(val: magnus::Value) -> Result { let result: Result<&Self, _> = TryConvert::try_convert(val); result.cloned() } } #[derive(Clone)] #[magnus::wrap(class = "Bevy::Vec3")] 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 TryConvert for BevyVec3 { fn try_convert(val: magnus::Value) -> Result { let result: Result<&Self, _> = TryConvert::try_convert(val); result.cloned() } } impl From for ScriptingError { fn from(value: magnus::Error) -> Self { // TODO: DRY ScriptingError::RuntimeError( value.inspect(), value .value() .unwrap() .funcall::<_, _, magnus::RArray>("backtrace", ()) // TODO: is there an API for this // somehwere .unwrap() .to_vec::() .unwrap() .join("\n"), ) } } impl Default for RubyRuntime { fn default() -> Self { let (lock, cvar) = &*Arc::clone(&RUBY_THREAD); let mut ruby_thread = lock.lock().expect("Failed to acquire lock on Ruby thread"); while ruby_thread.is_none() { ruby_thread = cvar .wait(ruby_thread) .expect("Failed to acquire lock on Ruby thread after waiting"); } let ruby_thread = ruby_thread.take().expect("Ruby thread is not available"); cvar.notify_all(); ruby_thread .execute(Box::new(|ruby| { let module = ruby.define_module("Bevy")?; let entity = module.define_class("Entity", ruby.class_object())?; entity.class().define_method( "current", method!( |r_self: RClass| { r_self.ivar_get::<_, BevyEntity>("_current") }, 0 ), )?; entity.define_method("index", method!(BevyEntity::index, 0))?; let promise = module.define_class("Promise", ruby.class_object())?; promise.define_method("and_then", magnus::method!(then, 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))?; Ok::<(), ScriptingError>(()) })) .expect("Failed to define builtin types"); Self { ruby_thread: Some(ruby_thread), } } } impl Drop for RubyRuntime { fn drop(&mut self) { let (lock, cvar) = &*Arc::clone(&RUBY_THREAD); let mut ruby_thread = lock .lock() .expect("Failed to lock ruby thread while dropping the runtime"); *ruby_thread = self.ruby_thread.take(); cvar.notify_all(); } } #[derive(Clone)] pub struct RubyValue(pub magnus::value::Opaque); impl RubyValue { fn nil(ruby: &Ruby) -> Self { Self::new(ruby.qnil().as_value()) } fn new(value: magnus::Value) -> Self { Self(magnus::value::Opaque::from(value)) } } impl RubyRuntime { fn execute_in_thread( &self, f: impl FnOnce(&magnus::Ruby) -> T + Send + 'static, ) -> T { self.ruby_thread .as_ref() .expect("No Ruby thread") .execute(Box::new(move |ruby| f(&ruby))) } fn execute_in_thread_mut( &self, f: impl FnOnce(&mut magnus::Ruby) -> T + Send + 'static, ) -> T { self.ruby_thread .as_ref() .expect("No Ruby thread") .execute(Box::new(move |mut ruby| f(&mut ruby))) } } impl Runtime for RubyRuntime { type Schedule = RubySchedule; type ScriptAsset = RubyScript; type ScriptData = RubyScriptData; type CallContext = (); type Value = RubyValue; type RawEngine = magnus::Ruby; fn with_engine_send_mut( &mut self, f: impl FnOnce(&mut Self::RawEngine) -> T + Send + 'static, ) -> T { self.execute_in_thread_mut(f) } fn with_engine_send( &self, f: impl FnOnce(&Self::RawEngine) -> T + Send + 'static, ) -> T { self.execute_in_thread(f) } fn with_engine_mut(&mut self, _f: impl FnOnce(&mut Self::RawEngine) -> T) -> T { unimplemented!( "Ruby runtime requires sending execution to another thread, use `with_engine_mut_send`" ); } fn with_engine(&self, _f: impl FnOnce(&Self::RawEngine) -> T) -> T { unimplemented!( "Ruby runtime requires sending execution to another thread, use `with_engine_send`" ); } fn eval( &self, script: &Self::ScriptAsset, entity: bevy::prelude::Entity, ) -> Result { let script = script.0.clone(); self.execute_in_thread(Box::new(move |ruby: &Ruby| { // TODO: refactor let var = ruby .class_object() .const_get::<_, RModule>("Bevy") .expect("Failed to get Bevy module") .const_get::<_, RClass>("Entity") .expect("Failed to get Entity class"); var.ivar_set("_current", BevyEntity(entity)) .expect("Failed to set current entity handle"); unsafe { ruby.eval::(&script).map_err(|e| { ScriptingError::RuntimeError( e.inspect(), e.value() .unwrap() .funcall::<_, _, magnus::RArray>("backtrace", ()) .unwrap() .to_vec::() .unwrap() .join("\n"), ) })?; } var.ivar_set("_current", ruby.qnil().as_value()) .expect("Failed to unset current entity handle"); Ok::(RubyScriptData) })) } fn register_fn( &mut self, name: String, _arg_types: Vec, f: impl Fn( Self::CallContext, Vec, ) -> Result< crate::promise::Promise, crate::ScriptingError, > + Send + Sync + 'static, ) -> Result<(), crate::ScriptingError> { type CallbackClosure = Box< dyn Fn( (), Vec, ) -> Result, crate::ScriptingError> + Send, >; static RUBY_CALLBACKS: LazyLock>> = LazyLock::new(|| Mutex::new(HashMap::new())); let mut callbacks = RUBY_CALLBACKS .lock() .expect("Failed to lock callbacks static when registering a callback"); callbacks.insert(name.clone(), Box::new(f)); fn callback(args: &[magnus::Value]) -> magnus::Value { let ruby = magnus::Ruby::get() .expect("Failed to get a handle to Ruby API while processing callback"); let method_name: magnus::value::StaticSymbol = ruby.class_object().funcall("__method__", ()).unwrap(); let method_name = method_name.name().unwrap(); let callbacks = RUBY_CALLBACKS.lock().unwrap(); let f = callbacks.get(method_name).unwrap(); let result = f( (), args.iter() .map(|arg| RubyValue::new(arg.into_value())) .collect(), ) .expect("failed to call callback"); result.into_value() } self.execute_in_thread(Box::new(move |ruby: &Ruby| { ruby.define_global_function(&name, function!(callback, -1)); RubyValue::nil(ruby) })); Ok(()) } fn call_fn( &self, name: &str, _script_data: &mut Self::ScriptData, entity: bevy::prelude::Entity, args: impl for<'a> crate::FuncArgs<'a, Self::Value, Self> + Send + 'static, ) -> Result { let name = name.to_string(); self.execute_in_thread(Box::new(move |ruby: &Ruby| { // TOOD: refactor let var = ruby .class_object() .const_get::<_, RModule>("Bevy") .unwrap() .const_get::<_, RClass>("Entity") .unwrap(); var.ivar_set("_current", BevyEntity(entity)).unwrap(); let args: Vec<_> = args .parse(ruby) .into_iter() .map(|a| ruby.get_inner(a.0)) .collect(); let return_value: magnus::Value = ruby.class_object().funcall(name, args.as_slice())?; var.ivar_set("_current", ruby.qnil().as_value()).unwrap(); Ok(RubyValue::new(return_value)) })) } fn call_fn_from_value( &self, value: &Self::Value, _context: &Self::CallContext, args: Vec, ) -> Result { let value = value.clone(); self.ruby_thread .as_ref() .unwrap() .execute(Box::new(move |ruby| { let f: Proc = TryConvert::try_convert(ruby.get_inner(value.0)).unwrap(); let args: Vec<_> = args .into_iter() .map(|x| ruby.get_inner(x.0).as_value()) .collect(); let result: magnus::Value = f.funcall("call", args.as_slice())?; Ok(RubyValue::new(result)) })) } fn needs_own_thread() -> bool { true } } pub mod magnus { pub use magnus::*; } pub mod prelude { pub use super::{BevyEntity, BevyVec3, RubyRuntime, RubyScript, RubyScriptData}; } impl FromRuntimeValueWithEngine<'_, RubyRuntime> for T { fn from_runtime_value_with_engine(value: RubyValue, engine: &magnus::Ruby) -> Self { let inner = engine.get_inner(value.0); T::try_convert(inner).unwrap() } } impl IntoRuntimeValueWithEngine<'_, T, RubyRuntime> for T { fn into_runtime_value_with_engine(value: T, _engine: &magnus::Ruby) -> RubyValue { RubyValue::new(value.into_value()) } } impl FuncArgs<'_, RubyValue, RubyRuntime> for () { fn parse(self, _engine: &magnus::Ruby) -> Vec { Vec::new() } } impl FuncArgs<'_, RubyValue, RubyRuntime> for Vec { fn parse(self, _engine: &magnus::Ruby) -> Vec { self.into_iter() .map(|x| RubyValue::new(x.into_value())) .collect() } } pub struct RArray(pub Opaque); impl FromRuntimeValueWithEngine<'_, RubyRuntime> for RArray { fn from_runtime_value_with_engine(value: RubyValue, engine: &magnus::Ruby) -> Self { let inner = engine.get_inner(value.0); let array = magnus::RArray::try_convert(inner).unwrap(); RArray(Opaque::from(array)) } } macro_rules! impl_tuple { ($($idx:tt $t:tt),+) => { impl<'a, $($t: IntoValue,)+> FuncArgs<'a, RubyValue, RubyRuntime> for ($($t,)+) { fn parse(self, _engine: &'a magnus::Ruby) -> Vec { vec![ $(RubyValue::new(self.$idx.into_value()), )+ ] } } }; } 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);