diff --git a/Cargo.toml b/Cargo.toml index 73fa84073..8ffeb6e9b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "gdnative", + "gdnative-async", "gdnative-bindings", "gdnative-core", "gdnative-derive", diff --git a/gdnative-async/Cargo.toml b/gdnative-async/Cargo.toml new file mode 100644 index 000000000..ae628d911 --- /dev/null +++ b/gdnative-async/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "gdnative-async" +authors = ["The godot-rust developers"] +description = "Runtime async support for godot-rust." +documentation = "https://docs.rs/crate/gdnative-async" +repository = "https://github.com/godot-rust/godot-rust" +homepage = "https://godot-rust.github.io/" +version = "0.9.3" +license = "MIT" +workspace = ".." +edition = "2018" + +[features] + +[dependencies] +gdnative-derive = { path = "../gdnative-derive", version = "=0.9.3" } +gdnative-core = { path = "../gdnative-core", version = "=0.9.3" } +gdnative-bindings = { path = "../gdnative-bindings", version = "=0.9.3" } +futures-task = "0.3.17" +atomic-waker = "1.0.0" +once_cell = "1.8.0" +thiserror = "1.0" +parking_lot = "0.11.2" +crossbeam-channel = "0.5.1" +crossbeam-utils = "0.8.5" + +[build-dependencies] diff --git a/gdnative-async/src/executor.rs b/gdnative-async/src/executor.rs new file mode 100644 index 000000000..df51e3a9a --- /dev/null +++ b/gdnative-async/src/executor.rs @@ -0,0 +1,21 @@ +use std::cell::Cell; + +use futures_task::LocalSpawn; + +thread_local!( + static LOCAL_SPAWN: Cell> = Cell::new(None); +); + +pub(crate) fn local_spawn() -> Option<&'static dyn LocalSpawn> { + LOCAL_SPAWN.with(|cell| cell.get()) +} + +/// Sets the global executor for the current thread to a `Box`. This value is leaked. +pub fn set_boxed_executor(sp: Box) { + set_executor(Box::leak(sp)) +} + +/// Sets the global executor for the current thread to a `&'static dyn LocalSpawn`. +pub fn set_executor(sp: &'static dyn LocalSpawn) { + LOCAL_SPAWN.with(|cell| cell.set(Some(sp))) +} diff --git a/gdnative-async/src/future.rs b/gdnative-async/src/future.rs new file mode 100644 index 000000000..d51a1b10f --- /dev/null +++ b/gdnative-async/src/future.rs @@ -0,0 +1,57 @@ +use std::future::Future; +use std::pin::Pin; +use std::sync::Arc; +use std::task::{Context, Poll}; + +use atomic_waker::AtomicWaker; +use crossbeam_channel::{Receiver, Sender}; + +pub(crate) fn make() -> (Yield, Resume) { + let (arg_send, arg_recv) = crossbeam_channel::bounded(1); + let waker = Arc::default(); + + let future = Yield { + waker: Arc::clone(&waker), + arg_recv, + }; + + let resume = Resume { waker, arg_send }; + + (future, resume) +} + +/// Future that can be `await`ed for a signal or a `resume` call from Godot. See +/// [`Context`](crate::Context) for methods that return this future. +pub struct Yield { + waker: Arc, + arg_recv: Receiver, +} + +impl Future for Yield { + type Output = T; + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + match self.arg_recv.try_recv() { + Ok(arg) => Poll::Ready(arg), + Err(_) => { + self.waker.register(cx.waker()); + Poll::Pending + } + } + } +} + +pub(crate) struct Resume { + waker: Arc, + arg_send: Sender, +} + +impl Resume { + /// Resume the task with a given argument from GDScript. + pub fn resume(self, arg: T) { + self.arg_send + .send(arg) + .expect("sender should not become disconnected"); + + self.waker.wake(); + } +} diff --git a/gdnative-async/src/lib.rs b/gdnative-async/src/lib.rs new file mode 100644 index 000000000..cfdb2c77b --- /dev/null +++ b/gdnative-async/src/lib.rs @@ -0,0 +1,20 @@ +//! Runtime async support for godot-rust. +//! +//! This crate contains types and functions that enable using async code with godot-rust. +//! +//! # Safety assumptions +//! +//! This crate assumes that all user non-Rust code follow the official threading guidelines. + +// Workaround for macros that expect the `gdnative` crate. +extern crate gdnative_core as gdnative; + +mod executor; +mod future; +mod method; +mod rt; + +pub use executor::{set_boxed_executor, set_executor}; +pub use future::Yield; +pub use method::{Async, AsyncMethod, Spawner}; +pub use rt::{register_runtime, terminate_runtime, Context}; diff --git a/gdnative-async/src/method.rs b/gdnative-async/src/method.rs new file mode 100644 index 000000000..27585b18a --- /dev/null +++ b/gdnative-async/src/method.rs @@ -0,0 +1,127 @@ +use std::future::Future; +use std::marker::PhantomData; +use std::sync::Arc; + +use futures_task::{LocalFutureObj, LocalSpawn, SpawnError}; + +use gdnative_core::core_types::{ToVariant, Variant}; +use gdnative_core::log::{self, Site}; +use gdnative_core::nativescript::export::{Method, Varargs}; +use gdnative_core::nativescript::{NativeClass, RefInstance}; +use gdnative_core::object::ownership::Shared; + +use crate::rt::Context; + +/// Trait for async methods. When exported, such methods return `FunctionState`-like +/// objects that can be manually resumed or yielded to completion. +/// +/// Async methods are always spawned locally on the thread where they were created, +/// and never sent to another thread. This is so that we can ensure the safety of +/// emitting signals from the `FunctionState`-like object. If you need to off-load +/// some task to another thread, consider using something like +/// `futures::future::Remote` to spawn it remotely on a thread pool. +pub trait AsyncMethod: Send + Sync + 'static { + /// Spawns the future for result of this method with `spawner`. This is done so + /// that implementors of this trait do not have to name their future types. + /// + /// If the `spawner` object is not used, the method call will fail, output an error, + /// and return a `Nil` variant. + fn spawn_with(&self, spawner: Spawner<'_, C>); + + /// Returns an optional site where this method is defined. Used for logging errors in FFI wrappers. + /// + /// Default implementation returns `None`. + #[inline] + fn site() -> Option> { + None + } +} + +/// A helper structure for working around naming future types. See [`Spawner::spawn`]. +pub struct Spawner<'a, C: NativeClass> { + sp: &'static dyn LocalSpawn, + ctx: Context, + this: RefInstance<'a, C, Shared>, + args: Varargs<'a>, + result: &'a mut Option>, + /// Remove Send and Sync + _marker: PhantomData<*const ()>, +} + +impl<'a, C: NativeClass> Spawner<'a, C> { + /// Consumes this `Spawner` and spawns a future returned by the closure. This indirection + /// is necessary so that implementors of the `AsyncMethod` trait do not have to name their + /// future types. + pub fn spawn(self, f: F) + where + F: FnOnce(Arc, RefInstance<'_, C, Shared>, Varargs<'_>) -> R, + R: Future + 'static, + { + let ctx = Arc::new(self.ctx); + let future = f(Arc::clone(&ctx), self.this, self.args); + *self.result = Some( + self.sp + .spawn_local_obj(LocalFutureObj::new(Box::new(async move { + let value = future.await; + ctx.resolve(value); + }))), + ); + } +} + +/// Adapter for async methods that implements `Method` and can be registered. +#[derive(Clone, Copy, Default, Debug)] +pub struct Async { + f: F, +} + +impl Async { + /// Wrap `f` in an adapter that implements `Method`. + #[inline] + pub fn new(f: F) -> Self { + Async { f } + } +} + +impl> Method for Async { + fn call(&self, this: RefInstance<'_, C, Shared>, args: Varargs<'_>) -> Variant { + if let Some(sp) = crate::executor::local_spawn() { + let ctx = Context::new(); + let func_state = ctx.func_state(); + + let mut result = None; + self.f.spawn_with(Spawner { + sp, + ctx, + this, + args, + result: &mut result, + _marker: PhantomData, + }); + + match result { + Some(Ok(())) => func_state.to_variant(), + Some(Err(err)) => { + log::error( + Self::site().unwrap_or_default(), + format_args!("unable to spawn future: {}", err), + ); + Variant::new() + } + None => { + log::error( + Self::site().unwrap_or_default(), + format_args!("implementation did not spawn a future"), + ); + Variant::new() + } + } + } else { + log::error( + Self::site().unwrap_or_default(), + "a global executor must be set before any async methods can be called on this thread", + ); + Variant::new() + } + } +} diff --git a/gdnative-async/src/rt.rs b/gdnative-async/src/rt.rs new file mode 100644 index 000000000..21dfeb9de --- /dev/null +++ b/gdnative-async/src/rt.rs @@ -0,0 +1,106 @@ +use std::marker::PhantomData; + +use gdnative_bindings::Object; +use gdnative_core::object::SubClass; + +use gdnative_core::core_types::{GodotError, Variant}; +use gdnative_core::nativescript::export::InitHandle; +use gdnative_core::nativescript::{Instance, RefInstance}; +use gdnative_core::object::ownership::Shared; +use gdnative_core::object::TRef; + +use crate::future; + +mod bridge; +mod func_state; + +use func_state::FuncState; + +/// Context for creating `yield`-like futures in async methods. +pub struct Context { + func_state: Instance, + /// Remove Send and Sync + _marker: PhantomData<*const ()>, +} + +impl Context { + pub(crate) fn new() -> Self { + Context { + func_state: FuncState::new().into_shared(), + _marker: PhantomData, + } + } + + pub(crate) fn func_state(&self) -> Instance { + self.func_state.clone() + } + + fn safe_func_state(&self) -> RefInstance<'_, FuncState, Shared> { + // SAFETY: FuncState objects are bound to their origin threads in Rust, and + // Context is !Send, so this is safe to call within this type. + // Non-Rust code is expected to be following the official guidelines as per + // the global safety assumptions. Since a reference of `FuncState` is held by + // Rust, it voids the assumption to send the reference to any thread aside from + // the one where it's created. + unsafe { self.func_state.assume_safe() } + } + + pub(crate) fn resolve(&self, value: Variant) { + func_state::resolve(self.safe_func_state(), value); + } + + /// Returns a future that waits until the corresponding `FunctionState` object + /// is manually resumed from GDScript, and yields the argument to `resume` or `Nil` + /// if nothing is passed. + /// + /// Calling this function will put the associated `FunctionState`-like object in + /// resumable state, and will make it emit a `resumable` signal if it isn't in that + /// state already. + /// + /// Only the most recent future created from this `Context` is guaranteed to resolve + /// upon a `resume` call. If any previous futures weren't `await`ed to completion, they + /// are no longer guaranteed to resolve, and have unspecified, but safe behavior + /// when polled. + pub fn until_resume(&self) -> future::Yield { + let (future, resume) = future::make(); + func_state::make_resumable(self.safe_func_state(), resume); + future + } + + /// Returns a future that waits until the specified signal is emitted, if connection succeeds. + /// Yields any arguments emitted with the signal. + /// + /// Only the most recent future created from this `Context` is guaranteed to resolve + /// when the signal is emitted. If any previous futures weren't `await`ed to completion, they + /// are no longer guaranteed to resolve, and have unspecified, but safe behavior + /// when polled. + /// + /// # Errors + /// + /// If connection to the signal failed. + pub fn signal( + &self, + obj: TRef<'_, C>, + signal: &str, + ) -> Result>, GodotError> + where + C: SubClass, + { + let (future, resume) = future::make(); + bridge::SignalBridge::connect(obj.upcast(), signal, resume)?; + Ok(future) + } +} + +/// Adds required supporting NativeScript classes to `handle`. This must be called once and +/// only once per initialization. +pub fn register_runtime(handle: &InitHandle) { + handle.add_class::(); + handle.add_class::(); +} + +/// Releases all observers still in use. This should be called in the +/// `godot_gdnative_terminate` callback. +pub fn terminate_runtime() { + bridge::terminate(); +} diff --git a/gdnative-async/src/rt/bridge.rs b/gdnative-async/src/rt/bridge.rs new file mode 100644 index 000000000..a1648bc7a --- /dev/null +++ b/gdnative-async/src/rt/bridge.rs @@ -0,0 +1,144 @@ +use std::collections::HashMap; + +use once_cell::sync::OnceCell; +use parking_lot::Mutex; + +use gdnative_bindings::{Object, Reference}; +use gdnative_core::core_types::{GodotError, Variant, VariantArray}; +use gdnative_core::godot_site; +use gdnative_core::nativescript::export::method::{Method, Varargs}; +use gdnative_core::nativescript::export::ClassBuilder; +use gdnative_core::nativescript::user_data::{ArcData, Map}; +use gdnative_core::nativescript::{Instance, NativeClass, NativeClassMethods, RefInstance}; +use gdnative_core::object::{ownership::Shared, TRef}; + +use crate::future::Resume; + +// We need to keep our observers alive since `Object::connect` won't +static BRIDGES: OnceCell> = OnceCell::new(); + +pub(super) fn terminate() { + if let Some(pool) = BRIDGES.get() { + let mut pool = pool.lock(); + std::mem::take(&mut *pool); + } +} + +#[derive(Default)] +struct Pool { + busy: HashMap, + free: Vec<(i64, Instance)>, + next_id: i64, +} + +impl Pool { + fn next_id(&mut self) -> i64 { + let id = self.next_id; + self.next_id += 1; + id + } +} + +struct Entry { + resume: Resume>, + + // Just need to keep this alive. + #[allow(dead_code)] + obj: Instance, +} + +pub(super) struct SignalBridge { + id: i64, +} + +impl NativeClass for SignalBridge { + type Base = Reference; + type UserData = ArcData; + + fn class_name() -> &'static str { + // Sort of just praying that there will be no duplicates of this. + "__GDNATIVE_ASYNC_INTERNAL__SignalBridge" + } + + fn register_properties(_builder: &ClassBuilder) {} +} + +impl SignalBridge { + pub(crate) fn connect( + source: TRef, + signal: &str, + resume: Resume>, + ) -> Result<(), GodotError> { + let mut pool = BRIDGES.get_or_init(Mutex::default).lock(); + let (id, bridge) = pool.free.pop().unwrap_or_else(|| { + let id = pool.next_id(); + let bridge = Instance::emplace(SignalBridge { id }).into_shared(); + (id, bridge) + }); + + source.connect( + signal, + bridge.base(), + "_on_signal", + VariantArray::new_shared(), + Object::CONNECT_ONESHOT, + )?; + + let entry = Entry { + obj: bridge, + resume, + }; + + assert!(pool.busy.insert(id, entry).is_none()); + + Ok(()) + } +} + +#[derive(Clone, Copy, Debug, Default)] +struct OnSignalFn; + +impl Method for OnSignalFn { + fn call(&self, this: RefInstance<'_, SignalBridge, Shared>, args: Varargs<'_>) -> Variant { + let args = args.cloned().collect(); + + let this_persist = this.clone().claim(); + + this.script() + .map(|s| { + let (resume, args) = { + let mut pool = BRIDGES.get().unwrap().lock(); + match pool.busy.remove(&s.id) { + Some(entry) => { + pool.free.push((s.id, this_persist)); + (entry.resume, args) + } + None => { + gdnative_core::log::warn( + Self::site().unwrap(), + "`_on_signal` should only be called once per bridge object", + ); + return; + } + } + }; + + resume.resume(args); + }) + .unwrap(); + + Variant::new() + } + + fn site() -> Option> { + Some(godot_site!(SignalBridge::_on_signal)) + } +} + +impl NativeClassMethods for SignalBridge { + fn register(builder: &ClassBuilder) { + builder + .build_method("_on_signal", OnSignalFn) + .done_stateless(); + } +} diff --git a/gdnative-async/src/rt/func_state.rs b/gdnative-async/src/rt/func_state.rs new file mode 100644 index 000000000..5e5edb939 --- /dev/null +++ b/gdnative-async/src/rt/func_state.rs @@ -0,0 +1,180 @@ +use gdnative_bindings::Reference; +use gdnative_core::core_types::{ToVariant, Variant, VariantType}; +use gdnative_core::godot_site; +use gdnative_core::nativescript::export::method::StaticArgs; +use gdnative_core::nativescript::export::method::StaticArgsMethod; +use gdnative_core::nativescript::export::{ + ClassBuilder, ExportInfo, PropertyUsage, Signal, SignalArgument, +}; +use gdnative_core::nativescript::user_data::LocalCellData; +use gdnative_core::nativescript::user_data::{Map, MapMut}; +use gdnative_core::nativescript::{Instance, NativeClass, NativeClassMethods, RefInstance}; +use gdnative_core::object::ownership::{Shared, Unique}; +use gdnative_derive::FromVarargs; + +use crate::future::Resume; + +pub(crate) struct FuncState { + kind: Kind, +} + +enum Kind { + Resolved(Variant), + Resumable(Resume), + Pending, +} + +impl NativeClass for FuncState { + type Base = Reference; + type UserData = LocalCellData; + + fn class_name() -> &'static str { + // Sort of just praying that there will be no duplicates of this. + "__GDNATIVE_ASYNC_INTERNAL__FuncState" + } + + fn register_properties(builder: &ClassBuilder) { + builder.add_signal(Signal { + name: "completed", + args: &[SignalArgument { + name: "value", + default: Variant::new(), + export_info: ExportInfo::new(VariantType::Nil), + usage: PropertyUsage::DEFAULT, + }], + }); + + builder.add_signal(Signal { + name: "resumable", + args: &[], + }); + } +} + +impl FuncState { + pub fn new() -> Instance { + Instance::emplace(FuncState { + kind: Kind::Pending, + }) + } +} + +pub(super) fn resolve(this: RefInstance<'_, FuncState, Shared>, value: Variant) { + this.script() + .map_mut(|s| { + match s.kind { + Kind::Resolved(_) => { + panic!("`resolve` should only be called once for each FuncState") + } + Kind::Pending => {} + Kind::Resumable(_) => { + gdnative_core::log::warn( + Default::default(), + "async function resolved while waiting for a `resume` call", + ); + } + } + + s.kind = Kind::Resolved(value.clone()); + }) + .expect("no reentrancy"); + + this.base().emit_signal("completed", &[value]); +} + +pub(super) fn make_resumable(this: RefInstance<'_, FuncState, Shared>, resume: Resume) { + let kind = this + .script() + .map_mut(|s| std::mem::replace(&mut s.kind, Kind::Resumable(resume))) + .expect("no reentrancy"); + + match kind { + Kind::Resolved(_) => { + panic!("`make_resumable` should not be called after resolution") + } + Kind::Resumable(_) => { + gdnative_core::log::warn( + Default::default(), + "`make_resumable` called when there is a previous pending future", + ); + } + Kind::Pending => { + this.base().emit_signal("resumable", &[]); + } + } +} + +#[derive(Clone, Copy, Debug, Default)] +struct IsValidFn; + +#[derive(FromVarargs)] +struct IsValidArgs { + #[opt] + extended_check: Option, +} + +impl StaticArgsMethod for IsValidFn { + type Args = IsValidArgs; + fn call(&self, this: RefInstance<'_, FuncState, Shared>, args: Self::Args) -> Variant { + if args.extended_check.is_some() { + gdnative_core::log::warn( + Self::site().unwrap(), + "`extended_check` is set, but it has no effect on Rust function state objects", + ) + } + + this.script() + .map(|s| match &s.kind { + Kind::Resumable(_) => true, + Kind::Resolved(_) | Kind::Pending => false, + }) + .unwrap() + .to_variant() + } + fn site() -> Option> { + Some(godot_site!(FunctionState::is_valid)) + } +} + +#[derive(Clone, Copy, Debug, Default)] +struct ResumeFn; + +#[derive(FromVarargs)] +struct ResumeArgs { + #[opt] + arg: Variant, +} + +impl StaticArgsMethod for ResumeFn { + type Args = ResumeArgs; + fn call(&self, this: RefInstance<'_, FuncState, Shared>, args: Self::Args) -> Variant { + this.map_mut( + |s, owner| match std::mem::replace(&mut s.kind, Kind::Pending) { + Kind::Resumable(resume) => { + resume.resume(args.arg); + owner.to_variant() + } + Kind::Pending => owner.to_variant(), + Kind::Resolved(result) => { + s.kind = Kind::Resolved(result.clone()); + result + } + }, + ) + .expect("no reentrancy") + } + fn site() -> Option> { + Some(godot_site!(FunctionState::is_valid)) + } +} + +impl NativeClassMethods for FuncState { + fn register(builder: &ClassBuilder) { + builder + .build_method("is_valid", StaticArgs::new(IsValidFn)) + .done_stateless(); + builder + .build_method("resume", StaticArgs::new(ResumeFn)) + .done_stateless(); + } +} diff --git a/gdnative-core/src/lib.rs b/gdnative-core/src/lib.rs index 6fa302aea..2adc34764 100644 --- a/gdnative-core/src/lib.rs +++ b/gdnative-core/src/lib.rs @@ -20,11 +20,7 @@ //! [thread-safety]: https://docs.godotengine.org/en/stable/tutorials/threads/thread_safe_apis.html #![deny(clippy::missing_inline_in_public_items)] -#![allow( - clippy::transmute_ptr_to_ptr, - clippy::missing_safety_doc, - clippy::if_then_panic -)] +#![allow(clippy::transmute_ptr_to_ptr, clippy::missing_safety_doc)] #![cfg_attr(feature = "gd_test", allow(clippy::blacklisted_name))] #[doc(hidden)] @@ -51,3 +47,97 @@ pub mod object; /// Internal low-level API for use by macros and generated bindings. Not a part of the public API. #[doc(hidden)] pub mod private; + +/// Context for the [`godot_gdnative_terminate`] callback. +pub struct TerminateInfo { + in_editor: bool, +} + +impl TerminateInfo { + #[inline] + #[doc(hidden)] // avoids clippy warning: unsafe function's docs miss `# Safety` section + pub unsafe fn new(options: *mut crate::sys::godot_gdnative_terminate_options) -> Self { + assert!(!options.is_null(), "options were NULL"); + + let crate::sys::godot_gdnative_terminate_options { in_editor } = *options; + + Self { in_editor } + } + + /// Returns `true` if the library is loaded in the Godot Editor. + #[inline] + pub fn in_editor(&self) -> bool { + self.in_editor + } +} + +/// Context for the [`godot_gdnative_init`] callback. +pub struct InitializeInfo { + in_editor: bool, + active_library_path: core_types::GodotString, + options: *mut crate::sys::godot_gdnative_init_options, +} + +impl InitializeInfo { + /// Returns true if the library is loaded in the Godot Editor. + #[inline] + pub fn in_editor(&self) -> bool { + self.in_editor + } + + /// Returns a path to the library relative to the project. + /// + /// Example: `res://../../target/debug/libhello_world.dylib` + #[inline] + pub fn active_library_path(&self) -> &core_types::GodotString { + &self.active_library_path + } + + /// # Safety + /// + /// Will `panic!()` if options is NULL or invalid. + #[inline] + #[doc(hidden)] + pub unsafe fn new(options: *mut crate::sys::godot_gdnative_init_options) -> Self { + assert!(!options.is_null(), "options were NULL"); + let crate::sys::godot_gdnative_init_options { + in_editor, + active_library_path, + .. + } = *options; + + let active_library_path = + crate::core_types::GodotString::clone_from_sys(*active_library_path); + + Self { + in_editor, + active_library_path, + options, + } + } + + #[inline] + pub fn report_loading_error(&self, message: T) + where + T: std::fmt::Display, + { + let crate::sys::godot_gdnative_init_options { + report_loading_error, + gd_native_library, + .. + } = unsafe { *self.options }; + + if let Some(report_loading_error_fn) = report_loading_error { + // Add the trailing zero and convert Display => String + let message = format!("{}\0", message); + + // Convert to FFI compatible string + let message = std::ffi::CStr::from_bytes_with_nul(message.as_bytes()) + .expect("message should not have a NULL"); + + unsafe { + report_loading_error_fn(gd_native_library, message.as_ptr()); + } + } + } +} diff --git a/gdnative-core/src/macros.rs b/gdnative-core/src/macros.rs index 68f112421..cac24ce63 100644 --- a/gdnative-core/src/macros.rs +++ b/gdnative-core/src/macros.rs @@ -16,11 +16,11 @@ #[macro_export] macro_rules! godot_gdnative_init { () => { - fn godot_gdnative_init_empty(_options: &$crate::private::InitializeInfo) {} + fn godot_gdnative_init_empty(_options: &$crate::InitializeInfo) {} $crate::godot_gdnative_init!(godot_gdnative_init_empty); }; (_ as $fn_name:ident) => { - fn godot_gdnative_init_empty(_options: &$crate::private::InitializeInfo) {} + fn godot_gdnative_init_empty(_options: &$crate::InitializeInfo) {} $crate::godot_gdnative_init!(godot_gdnative_init_empty as $fn_name); }; ($callback:ident) => { @@ -38,7 +38,7 @@ macro_rules! godot_gdnative_init { } let __result = ::std::panic::catch_unwind(|| { - let callback_options = $crate::private::InitializeInfo::new(options); + let callback_options = $crate::InitializeInfo::new(options); $callback(&callback_options) }); if __result.is_err() { @@ -64,14 +64,14 @@ macro_rules! godot_gdnative_init { #[macro_export] macro_rules! godot_gdnative_terminate { () => { - fn godot_gdnative_terminate_empty(_term_info: &$crate::private::TerminateInfo) {} + fn godot_gdnative_terminate_empty(_term_info: &$crate::TerminateInfo) {} $crate::godot_gdnative_terminate!(godot_gdnative_terminate_empty); }; ($callback:ident) => { $crate::godot_gdnative_terminate!($callback as godot_gdnative_terminate); }; (_ as $fn_name:ident) => { - fn godot_gdnative_terminate_empty(_term_info: &$crate::private::TerminateInfo) {} + fn godot_gdnative_terminate_empty(_term_info: &$crate::TerminateInfo) {} $crate::godot_gdnative_terminate!(godot_gdnative_terminate_empty as $fn_name); }; ($callback:ident as $fn_name:ident) => { @@ -86,7 +86,7 @@ macro_rules! godot_gdnative_terminate { } let __result = ::std::panic::catch_unwind(|| { - let term_info = $crate::private::TerminateInfo::new(options); + let term_info = $crate::TerminateInfo::new(options); $callback(&term_info) }); if __result.is_err() { diff --git a/gdnative-core/src/private.rs b/gdnative-core/src/private.rs index 188e4d80c..0bce4d0ad 100644 --- a/gdnative-core/src/private.rs +++ b/gdnative-core/src/private.rs @@ -1,6 +1,5 @@ use std::ffi::CString; -use crate::core_types::GodotString; use crate::sys; // ---------------------------------------------------------------------------------------------------------------------------------------------- @@ -234,98 +233,3 @@ make_method_table!(struct NativeScriptMethodTable for NativeScript { set_library, new, }); - -// ---------------------------------------------------------------------------------------------------------------------------------------------- -// Helper structs for init/terminate - -pub struct TerminateInfo { - in_editor: bool, -} - -impl TerminateInfo { - #[inline] - #[doc(hidden)] // avoids clippy warning: unsafe function's docs miss `# Safety` section - pub unsafe fn new(options: *mut crate::sys::godot_gdnative_terminate_options) -> Self { - assert!(!options.is_null(), "options were NULL"); - - let crate::sys::godot_gdnative_terminate_options { in_editor } = *options; - - Self { in_editor } - } - - /// Returns `true` if the library is loaded in the Godot Editor. - #[inline] - pub fn in_editor(&self) -> bool { - self.in_editor - } -} - -pub struct InitializeInfo { - in_editor: bool, - active_library_path: GodotString, - options: *mut crate::sys::godot_gdnative_init_options, -} - -impl InitializeInfo { - /// Returns true if the library is loaded in the Godot Editor. - #[inline] - pub fn in_editor(&self) -> bool { - self.in_editor - } - - /// Returns a path to the library relative to the project. - /// - /// Example: `res://../../target/debug/libhello_world.dylib` - #[inline] - pub fn active_library_path(&self) -> &GodotString { - &self.active_library_path - } - - /// # Safety - /// - /// Will `panic!()` if options is NULL or invalid. - #[inline] - #[doc(hidden)] - pub unsafe fn new(options: *mut crate::sys::godot_gdnative_init_options) -> Self { - assert!(!options.is_null(), "options were NULL"); - let crate::sys::godot_gdnative_init_options { - in_editor, - active_library_path, - .. - } = *options; - - let active_library_path = - crate::core_types::GodotString::clone_from_sys(*active_library_path); - - Self { - in_editor, - active_library_path, - options, - } - } - - #[inline] - pub fn report_loading_error(&self, message: T) - where - T: std::fmt::Display, - { - let crate::sys::godot_gdnative_init_options { - report_loading_error, - gd_native_library, - .. - } = unsafe { *self.options }; - - if let Some(report_loading_error_fn) = report_loading_error { - // Add the trailing zero and convert Display => String - let message = format!("{}\0", message); - - // Convert to FFI compatible string - let message = std::ffi::CStr::from_bytes_with_nul(message.as_bytes()) - .expect("message should not have a NULL"); - - unsafe { - report_loading_error_fn(gd_native_library, message.as_ptr()); - } - } - } -} diff --git a/gdnative/Cargo.toml b/gdnative/Cargo.toml index 6bc9a5b67..ac20f1bf3 100644 --- a/gdnative/Cargo.toml +++ b/gdnative/Cargo.toml @@ -12,18 +12,20 @@ readme = "../README.md" edition = "2018" [features] -default = ["bindings"] +default = ["bindings", "async"] formatted = ["gdnative-bindings/formatted", "gdnative-bindings/one_class_one_file"] serde = ["gdnative-core/serde"] gd_test = ["gdnative-core/gd_test"] type_tag_fallback = ["gdnative-core/type_tag_fallback"] bindings = ["gdnative-bindings"] +async = ["gdnative-async", "bindings"] [dependencies] gdnative-derive = { path = "../gdnative-derive", version = "=0.9.3" } gdnative-core = { path = "../gdnative-core", version = "=0.9.3" } gdnative-bindings = { optional = true, path = "../gdnative-bindings", version = "=0.9.3" } +gdnative-async = { optional = true, path = "../gdnative-async", version = "=0.9.3" } [dev-dependencies] trybuild = "1.0.50" diff --git a/gdnative/src/lib.rs b/gdnative/src/lib.rs index 33d74d6ed..373ffa2a8 100644 --- a/gdnative/src/lib.rs +++ b/gdnative/src/lib.rs @@ -58,7 +58,7 @@ // Items, which are #[doc(hidden)] in their original crate and re-exported with a wildcard, lose // their hidden status. Re-exporting them manually and hiding the wildcard solves this. #[doc(inline)] -pub use gdnative_core::{core_types, log, nativescript, object}; +pub use gdnative_core::{core_types, log, nativescript, object, InitializeInfo, TerminateInfo}; /// Collection of declarative `godot_*` macros, mostly for GDNative registration and output. pub mod macros { @@ -84,3 +84,8 @@ pub mod prelude; #[doc(inline)] #[cfg(feature = "bindings")] pub use gdnative_bindings as api; + +#[doc(inline)] +#[cfg(feature = "async")] +/// Support for async code +pub use gdnative_async as tasks; diff --git a/test/Cargo.toml b/test/Cargo.toml index 4d0be74ec..e92708239 100644 --- a/test/Cargo.toml +++ b/test/Cargo.toml @@ -23,3 +23,5 @@ bincode = "1.3.3" serde_cbor = "0.11.2" serde_yaml = "0.8.21" rmp-serde = "0.15.5" +futures = "0.3.17" +once_cell = "1.8.0" diff --git a/test/project/Scene.tscn b/test/project/Scene.tscn index 80884ab52..3552839d0 100644 --- a/test/project/Scene.tscn +++ b/test/project/Scene.tscn @@ -2,8 +2,5 @@ [ext_resource path="res://main.gd" type="Script" id=1] -[node name="Node" type="Node" index="0"] - +[node name="Node" type="Node"] script = ExtResource( 1 ) - - diff --git a/test/project/main.gd b/test/project/main.gd index 1c224f2ec..0018a620a 100644 --- a/test/project/main.gd +++ b/test/project/main.gd @@ -3,95 +3,165 @@ extends Node var gdn func _ready(): - print(" -- Rust gdnative test suite:") - gdn = GDNative.new() - var status = false; + print(" -- Rust gdnative test suite:") + _timeout() - gdn.library = load("res://gdnative.gdnlib") + gdn = GDNative.new() + var status = false; - if gdn.initialize(): - status = gdn.call_native("standard_varcall", "run_tests", []) + gdn.library = load("res://gdnative.gdnlib") - status = status && _test_argument_passing_sanity() - status = status && _test_optional_args() + if gdn.initialize(): + status = gdn.call_native("standard_varcall", "run_tests", []) - gdn.terminate() - else: - print(" -- Could not load the gdnative library.") + status = status && _test_argument_passing_sanity() + status = status && _test_optional_args() + status = status && yield(_test_async_resume(), "completed") - if status: - print(" -- Test run completed successfully.") - else: - print(" -- Test run completed with errors.") - OS.exit_code = 1 + # Godot needs another frame to dispose the executor driver node. Otherwise the process + # aborts due to `_process` being called after `terminate` (`get_api` fail, not UB). + yield(get_tree().create_timer(0.1), "timeout") - print(" -- exiting.") - get_tree().quit() + gdn.terminate() + else: + print(" -- Could not load the gdnative library.") + + if status: + print(" -- Test run completed successfully.") + else: + print(" -- Test run completed with errors.") + OS.exit_code = 1 + + print(" -- exiting.") + get_tree().quit() + +func _timeout(): + yield(get_tree().create_timer(10.0), "timeout") + print(" -- Test run is taking too long.") + OS.exit_code = 1 + + print(" -- exiting.") + get_tree().quit() func _test_argument_passing_sanity(): - print(" -- test_argument_passing_sanity") + print(" -- test_argument_passing_sanity") - var script = NativeScript.new() - script.set_library(gdn.library) - script.set_class_name("Foo") - var foo = Reference.new() - foo.set_script(script) - - var status = true + var script = NativeScript.new() + script.set_library(gdn.library) + script.set_class_name("Foo") + var foo = Reference.new() + foo.set_script(script) + + var status = true - status = status && _assert_choose("foo", foo, "choose", "foo", true, "bar") - status = status && _assert_choose("night", foo, "choose", "day", false, "night") - status = status && _assert_choose(42, foo, "choose_variant", 42, "int", 54.0) - status = status && _assert_choose(9.0, foo, "choose_variant", 6, "float", 9.0) + status = status && _assert_choose("foo", foo, "choose", "foo", true, "bar") + status = status && _assert_choose("night", foo, "choose", "day", false, "night") + status = status && _assert_choose(42, foo, "choose_variant", 42, "int", 54.0) + status = status && _assert_choose(9.0, foo, "choose_variant", 6, "float", 9.0) - if status: - assert("foo" == foo.choose("foo", true, "bar")) - assert("night" == foo.choose("day", false, "night")) - assert(42 == foo.choose_variant(42, "int", 54.0)) - assert(9.0 == foo.choose_variant(6, "float", 9.0)) + if status: + assert("foo" == foo.choose("foo", true, "bar")) + assert("night" == foo.choose("day", false, "night")) + assert(42 == foo.choose_variant(42, "int", 54.0)) + assert(9.0 == foo.choose_variant(6, "float", 9.0)) - if !status: - printerr(" !! test_argument_passing_sanity failed") + if !status: + printerr(" !! test_argument_passing_sanity failed") - return status + return status func _assert_choose(expected, foo, fun, a, which, b): - var got_value = foo.call(fun, a, which, b) - if got_value == expected: - return true - printerr(" !! expected ", expected, ", got ", got_value) - return false + var got_value = foo.call(fun, a, which, b) + if got_value == expected: + return true + printerr(" !! expected ", expected, ", got ", got_value) + return false func _test_optional_args(): - print(" -- _test_optional_args") - print(" -- expected error messages for edge cases:") - print(" -- missing non-optional parameter `b` (#1)") - print(" -- 1 excessive argument is given: [I64(6)]") - print(" -- the test is successful when and only when these errors are shown") + print(" -- _test_optional_args") + print(" -- expected error messages for edge cases:") + print(" -- missing non-optional parameter `b` (#1)") + print(" -- 1 excessive argument is given: [I64(6)]") + print(" -- the test is successful when and only when these errors are shown") - var script = NativeScript.new() - script.set_library(gdn.library) - script.set_class_name("OptionalArgs") - var opt_args = Reference.new() - opt_args.set_script(script) + var script = NativeScript.new() + script.set_library(gdn.library) + script.set_class_name("OptionalArgs") + var opt_args = Reference.new() + opt_args.set_script(script) - var status = true + var status = true - status = status && _assert_opt_args(null, opt_args, [1]) - status = status && _assert_opt_args(2, opt_args, [1, 1]) - status = status && _assert_opt_args(6, opt_args, [1, 3, 2]) - status = status && _assert_opt_args(13, opt_args, [5, 1, 3, 4]) - status = status && _assert_opt_args(42, opt_args, [4, 1, 20, 4, 13]) - status = status && _assert_opt_args(null, opt_args, [1, 2, 3, 4, 5, 6]) + status = status && _assert_opt_args(null, opt_args, [1]) + status = status && _assert_opt_args(2, opt_args, [1, 1]) + status = status && _assert_opt_args(6, opt_args, [1, 3, 2]) + status = status && _assert_opt_args(13, opt_args, [5, 1, 3, 4]) + status = status && _assert_opt_args(42, opt_args, [4, 1, 20, 4, 13]) + status = status && _assert_opt_args(null, opt_args, [1, 2, 3, 4, 5, 6]) - if !status: - printerr(" !! _test_optional_args failed") + if !status: + printerr(" !! _test_optional_args failed") - return status + return status func _assert_opt_args(expected, opt_args, args): - var got_value = opt_args.callv("opt_sum", args); - if got_value == expected: - return true - printerr(" !! expected ", expected, ", got ", got_value) - return false + var got_value = opt_args.callv("opt_sum", args); + if got_value == expected: + return true + printerr(" !! expected ", expected, ", got ", got_value) + return false + + +func _test_async_resume(): + print(" -- _test_async_resume") + + var driver_script = NativeScript.new() + driver_script.set_library(gdn.library) + driver_script.set_class_name("AsyncExecutorDriver") + var driver = Node.new() + driver.set_script(driver_script) + add_child(driver) + + var script = NativeScript.new() + script.set_library(gdn.library) + script.set_class_name("AsyncMethods") + var resume = Reference.new() + resume.set_script(script) + + var status = true + + # Force this to return a FunctionState for convenience + yield(get_tree().create_timer(0.1), "timeout") + + var fn_state = resume.resume_add(1, self, "_get_async_number") + if !fn_state: + printerr(" !! _test_async_resume failed") + remove_child(driver) + driver.queue_free() + return false + + yield(fn_state, "resumable") + status = status && fn_state.is_valid() + + fn_state = fn_state.resume(2) + if !fn_state: + printerr(" !! _test_async_resume failed") + remove_child(driver) + driver.queue_free() + return false + + var result = yield(fn_state, "completed") + + status = status && (result == 42) + + if !status: + printerr(" !! _test_async_resume failed") + + remove_child(driver) + driver.queue_free() + + return status + +func _get_async_number(): + yield(get_tree().create_timer(0.1), "timeout") + return 39 diff --git a/test/src/lib.rs b/test/src/lib.rs index 712f39e78..319ef9eba 100644 --- a/test/src/lib.rs +++ b/test/src/lib.rs @@ -1,7 +1,8 @@ -#![allow(clippy::blacklisted_name, clippy::if_then_panic)] +#![allow(clippy::blacklisted_name)] use gdnative::prelude::*; +mod test_async; mod test_constructor; mod test_derive; mod test_free_ub; @@ -61,6 +62,7 @@ pub extern "C" fn run_tests( status &= test_rust_class_construction(); status &= test_from_instance_id(); + status &= test_async::run_tests(); status &= test_derive::run_tests(); status &= test_free_ub::run_tests(); status &= test_constructor::run_tests(); @@ -257,6 +259,7 @@ fn init(handle: InitHandle) { handle.add_class::(); handle.add_class::(); + test_async::register(handle); test_derive::register(handle); test_free_ub::register(handle); test_constructor::register(handle); @@ -268,4 +271,10 @@ fn init(handle: InitHandle) { test_vararray_return::register(handle); } -godot_init!(init); +fn terminate(_term_info: &gdnative::TerminateInfo) { + gdnative::tasks::terminate_runtime(); +} + +gdnative::macros::godot_gdnative_init!(); +gdnative::macros::godot_nativescript_init!(init); +gdnative::macros::godot_gdnative_terminate!(terminate); diff --git a/test/src/test_async.rs b/test/src/test_async.rs new file mode 100644 index 000000000..c2bf792ad --- /dev/null +++ b/test/src/test_async.rs @@ -0,0 +1,99 @@ +use std::cell::RefCell; + +use gdnative::prelude::*; +use gdnative::tasks::{Async, AsyncMethod, Spawner}; + +pub(crate) fn run_tests() -> bool { + // Relevant tests in GDScript + true +} + +thread_local! { + static EXECUTOR: &'static SharedLocalPool = { + Box::leak(Box::new(SharedLocalPool::default())) + }; +} + +pub(crate) fn register(handle: InitHandle) { + gdnative::tasks::register_runtime(&handle); + gdnative::tasks::set_executor(EXECUTOR.with(|e| *e)); + + handle.add_class::(); + handle.add_class::(); +} + +#[derive(Default)] +struct SharedLocalPool { + pool: RefCell, +} + +impl futures::task::LocalSpawn for SharedLocalPool { + fn spawn_local_obj( + &self, + future: futures::task::LocalFutureObj<'static, ()>, + ) -> Result<(), futures::task::SpawnError> { + self.pool.borrow_mut().spawner().spawn_local_obj(future) + } +} + +#[derive(NativeClass)] +#[inherit(Node)] +struct AsyncExecutorDriver; + +impl AsyncExecutorDriver { + fn new(_owner: &Node) -> Self { + AsyncExecutorDriver + } +} + +#[methods] +impl AsyncExecutorDriver { + #[export] + fn _process(&self, _owner: &Node, _delta: f64) { + EXECUTOR.with(|e| e.pool.borrow_mut().run_until_stalled()); + } +} + +#[derive(NativeClass)] +#[inherit(Reference)] +#[register_with(register_methods)] +struct AsyncMethods; + +#[methods] +impl AsyncMethods { + fn new(_owner: TRef) -> Self { + AsyncMethods + } +} + +struct ResumeAddFn; + +impl AsyncMethod for ResumeAddFn { + fn spawn_with(&self, spawner: Spawner<'_, AsyncMethods>) { + spawner.spawn(|ctx, _this, mut args| { + let a = args.read::().get().unwrap(); + let obj = args.read::>().get().unwrap(); + let name = args.read::().get().unwrap(); + + async move { + let b = ctx.until_resume().await; + let b = i32::from_variant(&b).unwrap(); + + let c = unsafe { obj.assume_safe().call(name, &[]) }; + let c = Ref::::from_variant(&c).unwrap(); + let c = unsafe { c.assume_safe() }; + let c = ctx.signal(c, "completed").unwrap().await; + assert_eq!(1, c.len()); + let c = i32::from_variant(&c[0]).unwrap(); + + (a + b + c).to_variant() + } + }); + } +} + +fn register_methods(builder: &ClassBuilder) { + builder + .build_method("resume_add", Async::new(ResumeAddFn)) + .done(); +}