Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Type-safe signals #1000

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion godot-core/src/builtin/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ pub use crate::{array, dict, real, reals, varray};

// Re-export generated enums.
pub use crate::gen::central::global_reexported_enums::{Corner, EulerOrder, Side, VariantOperator};
pub use crate::sys::VariantType;
// Not yet public.
pub(crate) use crate::gen::central::VariantDispatch;
pub use crate::sys::VariantType;

#[doc(hidden)]
pub mod __prelude_reexport {
Expand Down
2 changes: 1 addition & 1 deletion godot-core/src/classes/class_runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ pub(crate) fn display_string<T: GodotClass>(
obj: &Gd<T>,
f: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
let string: GString = obj.raw.as_object().to_string();
let string: GString = obj.raw.as_object_ref().to_string();
<GString as std::fmt::Display>::fmt(&string, f)
}

Expand Down
5 changes: 5 additions & 0 deletions godot-core/src/obj/gd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,11 @@ impl<T: GodotClass> Gd<T> {
.expect("Upcast to Object failed. This is a bug; please report it.")
}

/// Equivalent to [`upcast_mut::<Object>()`][Self::upcast_mut], but without bounds.
pub(crate) fn upcast_object_mut(&mut self) -> &mut classes::Object {
self.raw.as_object_mut()
}

/// **Upcast shared-ref:** access this object as a shared reference to a base class.
///
/// This is semantically equivalent to multiple applications of [`Self::deref()`]. Not really useful on its own, but combined with
Expand Down
7 changes: 6 additions & 1 deletion godot-core/src/obj/raw_gd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,11 +207,16 @@ impl<T: GodotClass> RawGd<T> {
// self.as_target_mut()
// }

pub(crate) fn as_object(&self) -> &classes::Object {
pub(crate) fn as_object_ref(&self) -> &classes::Object {
// SAFETY: Object is always a valid upcast target.
unsafe { self.as_upcast_ref() }
}

pub(crate) fn as_object_mut(&mut self) -> &mut classes::Object {
// SAFETY: Object is always a valid upcast target.
unsafe { self.as_upcast_mut() }
}

/// # Panics
/// If this `RawGd` is null. In Debug mode, sanity checks (valid upcast, ID comparisons) can also lead to panics.
///
Expand Down
19 changes: 17 additions & 2 deletions godot-core/src/obj/traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -261,8 +261,8 @@ pub trait IndexEnum: EngineEnum {
// Possible alternative for builder APIs, although even less ergonomic: Base<T> could be Base<T, Self> and return Gd<Self>.
#[diagnostic::on_unimplemented(
message = "Class `{Self}` requires a `Base<T>` field",
label = "missing field `_base: Base<...>`",
note = "A base field is required to access the base from within `self`, for script-virtual functions or #[rpc] methods",
label = "missing field `_base: Base<...>` in struct declaration",
note = "A base field is required to access the base from within `self`, as well as for #[signal], #[rpc] and #[func(virtual)]",
note = "see also: https://godot-rust.github.io/book/register/classes.html#the-base-field"
)]
pub trait WithBaseField: GodotClass + Bounds<Declarer = bounds::DeclUser> {
Expand Down Expand Up @@ -571,6 +571,21 @@ pub mod cap {
fn __godot_property_get_revert(&self, property: StringName) -> Option<Variant>;
}

// Move one level up, like WithBaseField?
pub trait WithFuncs {
type FuncCollection;
type StaticFuncCollection;

fn static_funcs() -> Self::StaticFuncCollection;
fn funcs(&self) -> Self::FuncCollection;
}

pub trait WithSignals: WithBaseField {
type SignalCollection<'a>;

fn signals(&mut self) -> Self::SignalCollection<'_>;
}

/// Auto-implemented for `#[godot_api] impl MyClass` blocks
pub trait ImplementsGodotApi: GodotClass {
#[doc(hidden)]
Expand Down
11 changes: 10 additions & 1 deletion godot-core/src/private.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ use std::sync::atomic;
#[cfg(debug_assertions)]
use std::sync::{Arc, Mutex};
use sys::Global;

// ----------------------------------------------------------------------------------------------------------------------------------------------
// Global variables

Expand Down Expand Up @@ -120,6 +119,16 @@ pub(crate) fn call_error_remove(in_error: &sys::GDExtensionCallError) -> Option<
call_error
}

// ----------------------------------------------------------------------------------------------------------------------------------------------
// Functional and signal APIs

// pub fn emit_signal<T>(obj: &mut BaseMut<T>, varargs: &[Variant])
// where
// T: GodotClass<Declarer = bounds::DeclEngine> + Inherits<crate::classes::Object>,
// {
// obj.upcast_mut().emit_signal(varargs);
// }

// ----------------------------------------------------------------------------------------------------------------------------------------------
// Plugin and global state handling

Expand Down
12 changes: 12 additions & 0 deletions godot-core/src/registry/functional/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* Copyright (c) godot-rust; Bromeon and contributors.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

mod typed_signal;
mod variadic;

pub use typed_signal::*;
pub use variadic::*;
215 changes: 215 additions & 0 deletions godot-core/src/registry/functional/typed_signal.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
/*
* Copyright (c) godot-rust; Bromeon and contributors.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

// Maybe move this to builtin::functional module?

use crate::builtin::{Callable, Variant};
use crate::obj::{Gd, GodotClass, WithBaseField};
use crate::registry::functional::{AsFunc, ParamTuple};
use crate::{classes, sys};
use std::borrow::Cow;
use std::fmt;

#[doc(hidden)]
pub enum ObjectRef<'a, C: GodotClass> {
/// Helpful for emit: reuse `&self` from within the `impl` block, goes through `base()` re-borrowing and thus allows re-entrant calls
/// through Godot.
Internal { obj_mut: &'a mut C },

/// From outside, based on `Gd` pointer.
External { gd: Gd<C> },
}

impl<C> ObjectRef<'_, C>
where
C: WithBaseField,
{
fn with_object_mut(&mut self, f: impl FnOnce(&mut classes::Object)) {
match self {
ObjectRef::Internal { obj_mut } => f(obj_mut.base_mut().upcast_object_mut()),
ObjectRef::External { gd } => f(gd.upcast_object_mut()),
}
}

fn to_owned(&self) -> Gd<C> {
match self {
ObjectRef::Internal { obj_mut } => WithBaseField::to_gd(*obj_mut),
ObjectRef::External { gd } => gd.clone(),
}
}
}

// ----------------------------------------------------------------------------------------------------------------------------------------------

pub struct TypedSignal<'a, C: GodotClass, Ps> {
//signal: Signal,
/// In Godot, valid signals (unlike funcs) are _always_ declared in a class and become part of each instance. So there's always an object.
owner: ObjectRef<'a, C>,
name: Cow<'static, str>,
_signature: std::marker::PhantomData<Ps>,
}

impl<'a, C: WithBaseField, Ps: ParamTuple> TypedSignal<'a, C, Ps> {
#[doc(hidden)]
pub fn new(owner: ObjectRef<'a, C>, name: &'static str) -> Self {
Self {
owner,
name: Cow::Borrowed(name),
_signature: std::marker::PhantomData,
}
}

pub fn emit(&mut self, params: Ps) {
let name = self.name.as_ref();

self.owner.with_object_mut(|obj| {
obj.emit_signal(name, &params.to_variant_array());
});
}

/// Connect a method (member function) with `&mut self` as the first parameter.
pub fn connect_self<F>(&mut self, mut function: F)
where
for<'c> F: AsFunc<&'c mut C, Ps> + 'static,
{
// When using sys::short_type_name() in the future, make sure global "func" and member "MyClass::func" are rendered as such.
// PascalCase heuristic should then be good enough.
let callable_name = std::any::type_name_of_val(&function);

let object = self.owner.to_owned();
let godot_fn = move |variant_args: &[&Variant]| -> Result<Variant, ()> {
let args = Ps::from_variant_array(variant_args);

// let mut function = function;
// function.call(instance, args);
let mut object = object.clone();

// TODO: how to avoid another bind, when emitting directly from Rust?
let mut instance = object.bind_mut();
let instance = &mut *instance;
function.call(instance, args);

Ok(Variant::nil())
};

let name = self.name.as_ref();
let callable = Callable::from_local_fn(callable_name, godot_fn);

self.owner.with_object_mut(|obj| {
obj.connect(name, &callable);
});
}

/// Connect a static function (global or associated function).
pub fn connect<F>(&mut self, mut function: F)
where
F: AsFunc<(), Ps> + 'static,
{
let callable_name = std::any::type_name_of_val(&function);

let godot_fn = move |variant_args: &[&Variant]| -> Result<Variant, ()> {
let args = Ps::from_variant_array(variant_args);
function.call((), args);

Ok(Variant::nil())
};

let name = self.name.as_ref();
let callable = Callable::from_local_fn(callable_name, godot_fn);

self.owner.with_object_mut(|obj| {
obj.connect(name, &callable);
});
}
}

// ----------------------------------------------------------------------------------------------------------------------------------------------

/// Type-safe `#[func]` reference that is readily callable.
///
/// Can be either a static function of a class, or a method which is bound to a concrete object.
///
/// This can be seen as a more type-safe variant of Godot's `Callable`, which can carry intermediate information about function signatures (e.g.
/// when connecting signals).
pub struct Func<R, Ps> {
godot_function_name: &'static str,
callable_kind: CallableKind,
_return_type: std::marker::PhantomData<R>,
_param_types: std::marker::PhantomData<Ps>,
}

enum CallableKind {
StaticFunction {
// Maybe class name can be moved out (and also be useful for methods), e.g. Debug impl or so.
class_godot_name: Cow<'static, str>,
},
Method {
bound_object: Gd<classes::Object>,
},
}

impl<R, Ps> Func<R, Ps> {
#[doc(hidden)]
pub fn from_instance_method(
bound_object: Gd<classes::Object>,
method_godot_name: &'static str,
) -> Self {
Self {
godot_function_name: method_godot_name,
callable_kind: CallableKind::Method { bound_object },
_return_type: std::marker::PhantomData,
_param_types: std::marker::PhantomData,
}
}

#[doc(hidden)]
pub fn from_static_function(
class_godot_name: Cow<'static, str>,
method_godot_name: &'static str,
) -> Self {
Self {
godot_function_name: method_godot_name,
callable_kind: CallableKind::StaticFunction { class_godot_name },
_return_type: std::marker::PhantomData,
_param_types: std::marker::PhantomData,
}
}

pub fn to_callable(&self) -> Callable {
match &self.callable_kind {
CallableKind::StaticFunction { class_godot_name } => {
let class_name = class_godot_name.as_ref();
Callable::from_local_static(class_name, self.godot_function_name)
}
CallableKind::Method { bound_object } => {
Callable::from_object_method(bound_object, self.godot_function_name)
}
}
}
}

impl<R, Ps> fmt::Debug for Func<R, Ps> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let r = sys::short_type_name::<R>();
let ps = sys::short_type_name::<Ps>();

let (obj_or_class, is_static);
match &self.callable_kind {
CallableKind::StaticFunction { class_godot_name } => {
obj_or_class = class_godot_name.to_string();
is_static = "; static";
}
CallableKind::Method { bound_object } => {
obj_or_class = format!("{bound_object:?}");
is_static = "";
}
};

let function = self.godot_function_name;
write!(f, "Func({obj_or_class}.{function}{is_static}; {ps} -> {r})")
}
}
Loading
Loading