-
-
Notifications
You must be signed in to change notification settings - Fork 217
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
Add implementation of Callable #231
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,201 @@ | ||
/* | ||
* 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/. | ||
*/ | ||
|
||
use godot_ffi as sys; | ||
|
||
use crate::builtin::{inner, ToVariant, Variant}; | ||
use crate::engine::Object; | ||
use crate::obj::mem::Memory; | ||
use crate::obj::{Gd, GodotClass, InstanceId}; | ||
use std::fmt; | ||
use sys::{ffi_methods, GodotFfi}; | ||
|
||
use super::{StringName, VariantArray}; | ||
|
||
/// A `Callable` represents a function in Godot. | ||
/// | ||
/// Usually a callable is a reference to an `Object` and a method name, this is a standard callable. But can | ||
/// also be a custom callable, which is usually created from `bind`, `unbind`, or a GDScript lambda. See | ||
/// [`Callable::is_custom`]. | ||
/// | ||
/// Currently it is impossible to use `bind` and `unbind` in GDExtension, see [godot-cpp#802]. | ||
/// | ||
/// [godot-cpp#802]: https://github.com/godotengine/godot-cpp/issues/802 | ||
#[repr(C, align(8))] | ||
pub struct Callable { | ||
opaque: sys::types::OpaqueCallable, | ||
} | ||
|
||
impl Callable { | ||
fn from_opaque(opaque: sys::types::OpaqueCallable) -> Self { | ||
Self { opaque } | ||
} | ||
|
||
/// Create a callable for the method `object::method_name`. | ||
/// | ||
/// _Godot equivalent: `Callable(Object object, StringName method)`_ | ||
pub fn from_object_method<T, S>(object: Gd<T>, method_name: S) -> Self | ||
where | ||
T: GodotClass, // + Inherits<Object>, | ||
S: Into<StringName>, | ||
{ | ||
// upcast not needed | ||
let method = method_name.into(); | ||
unsafe { | ||
Self::from_sys_init_default(|self_ptr| { | ||
let ctor = sys::builtin_fn!(callable_from_object_method); | ||
let args = [object.sys_const(), method.sys_const()]; | ||
ctor(self_ptr, args.as_ptr()); | ||
}) | ||
} | ||
} | ||
|
||
/// Calls the method represented by this callable. | ||
/// | ||
/// Arguments passed should match the method's signature. | ||
/// | ||
/// - If called with more arguments than expected by the method, the extra arguments will be ignored and | ||
/// the call continues as normal. | ||
/// - If called with fewer arguments than expected it will crash Godot, without triggering UB. | ||
/// - If called with arguments of the wrong type then an error will be printed and the call will return | ||
/// `NIL`. | ||
/// - If called on an invalid Callable then no error is printed, and `NIL` is returned. | ||
/// | ||
/// _Godot equivalent: `callv`_ | ||
pub fn callv(&self, arguments: VariantArray) -> Variant { | ||
self.as_inner().callv(arguments) | ||
} | ||
|
||
/// Returns the name of the method represented by this callable. If the callable is a lambda function, | ||
/// returns the function's name. | ||
/// | ||
/// ## Known Bugs | ||
/// | ||
/// Getting the name of a lambda errors instead of returning its name, see [godot#73052]. | ||
/// | ||
/// _Godot equivalent: `get_method`_ | ||
/// | ||
/// [godot#73052]: https://github.com/godotengine/godot/issues/73052 | ||
pub fn method_name(&self) -> Option<StringName> { | ||
let method_name = self.as_inner().get_method(); | ||
if method_name.is_empty() { | ||
None | ||
} else { | ||
Some(method_name) | ||
} | ||
} | ||
|
||
/// Returns the object on which this callable is called. | ||
/// | ||
/// Returns `None` when this callable doesn't have any target object to call a method on, regardless of | ||
/// if the method exists for that target or not. | ||
/// | ||
/// _Godot equivalent: `get_object`_ | ||
pub fn object(&self) -> Option<Gd<Object>> { | ||
// Increment refcount because we're getting a reference, and `InnerCallable::get_object` doesn't | ||
// increment the refcount. | ||
self.as_inner().get_object().map(|object| { | ||
<Object as GodotClass>::Mem::maybe_inc_ref(&object); | ||
object | ||
}) | ||
} | ||
|
||
/// Returns the ID of this callable's object, see also [`Gd::instance_id`]. | ||
/// | ||
/// Returns `None` when this callable doesn't have any target to call a method on. | ||
/// | ||
/// _Godot equivalent: `get_object_id`_ | ||
Comment on lines
+106
to
+110
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no |
||
pub fn object_id(&self) -> Option<InstanceId> { | ||
let id = self.as_inner().get_object_id(); | ||
InstanceId::try_from_i64(id) | ||
} | ||
|
||
/// Returns the 32-bit hash value of this callable's object. | ||
/// | ||
/// _Godot equivalent: `hash`_ | ||
pub fn hash(&self) -> u32 { | ||
self.as_inner().hash().try_into().unwrap() | ||
} | ||
Comment on lines
+116
to
+121
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this guaranteed not to panic? The values returned by Godot are unsigned? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. according to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. also if this ever breaks/changes then it's likely gonna break for every call to |
||
|
||
/// Returns true if this callable is a custom callable. | ||
/// | ||
/// Custom callables are mainly created from bind or unbind. In GDScript, lambda functions are also | ||
/// custom callables. | ||
/// | ||
/// If a callable is not a custom callable, then it is considered a standard callable, this function is | ||
/// the opposite of [`Callable.is_standard`]. | ||
/// | ||
/// _Godot equivalent: `is_custom`_ | ||
/// | ||
/// [`Callable.is_standard`]: https://docs.godotengine.org/en/stable/classes/class_callable.html#class-callable-method-is-standard | ||
#[doc(alias = "is_standard")] | ||
pub fn is_custom(&self) -> bool { | ||
self.as_inner().is_custom() | ||
} | ||
|
||
/// Returns true if this callable has no target to call the method on. | ||
/// | ||
/// This is not the negated form of [`is_valid`], as `is_valid` will return `false` if the callable has a | ||
/// target but the method does not exist. | ||
/// | ||
/// _Godot equivalent: `is_null`_ | ||
pub fn is_null(&self) -> bool { | ||
self.as_inner().is_null() | ||
} | ||
|
||
/// Returns true if the callable's object exists and has a valid method name assigned, or is a custom | ||
/// callable. | ||
/// | ||
/// _Godot equivalent: `is_valid`_ | ||
pub fn is_valid(&self) -> bool { | ||
self.as_inner().is_valid() | ||
} | ||
|
||
#[doc(hidden)] | ||
pub fn as_inner(&self) -> inner::InnerCallable { | ||
inner::InnerCallable::from_outer(self) | ||
} | ||
} | ||
|
||
impl_builtin_traits! { | ||
for Callable { | ||
Default => callable_construct_default; | ||
// Equality for custom callables depend on the equality implementation of that custom callable. This | ||
// is from what i can tell currently implemented as total equality in all cases, but i dont believe | ||
// there are any guarantees that all implementations of equality for custom callables will be. | ||
// | ||
// So we cannot implement `Eq` here and be confident equality will be total for all future custom | ||
// callables. | ||
PartialEq => callable_operator_equal; | ||
Clone => callable_construct_copy; | ||
Drop => callable_destroy; | ||
} | ||
} | ||
|
||
// SAFETY: | ||
// The `opaque` in `Callable` is just a pair of pointers, and requires no special initialization or cleanup | ||
// beyond what is done in `from_opaque` and `drop`. So using `*mut Opaque` is safe. | ||
unsafe impl GodotFfi for Callable { | ||
ffi_methods! { type sys::GDExtensionTypePtr = *mut Opaque; .. } | ||
} | ||
|
||
impl std::fmt::Debug for Callable { | ||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
let method = self.method_name(); | ||
let object = self.object(); | ||
|
||
f.debug_struct("Callable") | ||
.field("method", &method) | ||
.field("object", &object) | ||
.finish() | ||
} | ||
} | ||
|
||
impl std::fmt::Display for Callable { | ||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
write!(f, "{}", self.to_variant()) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,7 +15,9 @@ use sys::types::OpaqueObject; | |
use sys::{ffi_methods, interface_fn, static_assert_eq_size, GodotFfi, PtrcallType}; | ||
|
||
use crate::builtin::meta::{ClassName, VariantMetadata}; | ||
use crate::builtin::{FromVariant, ToVariant, Variant, VariantConversionError}; | ||
use crate::builtin::{ | ||
Callable, FromVariant, StringName, ToVariant, Variant, VariantConversionError, | ||
}; | ||
use crate::obj::dom::Domain as _; | ||
use crate::obj::mem::Memory as _; | ||
use crate::obj::{cap, dom, mem, Export, GodotClass, Inherits, Share}; | ||
|
@@ -416,6 +418,11 @@ impl<T: GodotClass> Gd<T> { | |
T::Mem::maybe_init_ref(&self); | ||
self | ||
} | ||
|
||
/// Returns a callable referencing a method from this object named `method_name`. | ||
pub fn callable<S: Into<StringName>>(&self, method_name: S) -> Callable { | ||
Callable::from_object_method(self.share(), method_name) | ||
} | ||
Comment on lines
+421
to
+425
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see the convenience of this method; though over time, as we're adding more and more interactions between It's fine for now, but this may still change in the future. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, it's convenient for now but we should have a proper discussion of what the API for using callable should be. |
||
} | ||
|
||
/// _The methods in this impl block are only available for objects `T` that are manually managed, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does that mean? Lambdas have no name, at least not from the user perspective.
Since we have
Option
return type, would it be possible to returnNone
in cases where the function name is not meaningful for the user?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lambdas can in fact have names in GDScript, which is kinda counter-intuitive since that's not what lambda means. Here is an example of a lambda with a name:
this will print
Though i realize now that the godot docs are actually wrong and trying to use
get_method
on a lambda does not in fact return the function's name but rather just errors.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
godotengine/godot#73052 this is a known bug apparently, not sure how we should handle that in our case
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I see! Regarding the bug, it's probably OK to rely on it working, hopefully it will be fixed at some point. We could maybe add a mention in the doc.