Skip to content

Commit 870c725

Browse files
authored
Merge pull request #1271 from godot-rust/qol/varia
Nightly rustfmt + internals (once-calls, robust refcounts)
2 parents 8d9a1ee + d6c77b0 commit 870c725

File tree

9 files changed

+128
-30
lines changed

9 files changed

+128
-30
lines changed

.github/workflows/full-ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ jobs:
4949
- name: "Install Rust"
5050
uses: ./.github/composite/rust
5151
with:
52+
rust: nightly
5253
components: rustfmt
5354

5455
- name: "Check rustfmt"

.github/workflows/minimal-ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ jobs:
4747
- name: "Install Rust"
4848
uses: ./.github/composite/rust
4949
with:
50+
rust: nightly
5051
components: rustfmt
5152

5253
- name: "Check rustfmt"

.github/workflows/release-version.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ jobs:
120120
ref: ${{ github.ref }}
121121

122122
- name: "Install Rust (uncached)"
123-
run: rustup update stable
123+
run: rustup update nightly && rustup default nightly
124124

125125
- name: "Check rustfmt"
126126
run: cargo fmt --all -- --check

godot-core/src/builtin/callable.rs

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ use godot_ffi as sys;
1111
use sys::{ffi_methods, ExtVariantType, GodotFfi};
1212

1313
use crate::builtin::{inner, GString, StringName, Variant, VariantArray};
14-
use crate::classes;
1514
use crate::meta::{GodotType, ToGodot};
1615
use crate::obj::bounds::DynMemory;
1716
use crate::obj::{Bounds, Gd, GodotClass, InstanceId};
17+
use crate::{classes, meta};
1818

1919
#[cfg(all(since_api = "4.2", before_api = "4.3"))]
2020
type CallableCustomInfo = sys::GDExtensionCallableCustomInfo;
@@ -181,6 +181,40 @@ impl Callable {
181181
})
182182
}
183183

184+
/// Create callable from **single-threaded** Rust function or closure that can only be called once.
185+
///
186+
/// `name` is used for the string representation of the closure, which helps debugging.
187+
///
188+
/// After the first invocation, subsequent calls will panic with a message indicating the callable has already been consumed. This is
189+
/// useful for deferred operations that should only execute once. For repeated execution, use [`from_local_fn()][Self::from_local_fn].
190+
#[cfg(since_api = "4.2")]
191+
pub(crate) fn from_once_fn<F, S>(name: S, rust_function: F) -> Self
192+
where
193+
F: 'static + FnOnce(&[&Variant]) -> Result<Variant, ()>,
194+
S: meta::AsArg<GString>,
195+
{
196+
meta::arg_into_owned!(name);
197+
198+
let mut rust_fn_once = Some(rust_function);
199+
Self::from_local_fn(&name, move |args| {
200+
let rust_fn_once = rust_fn_once
201+
.take()
202+
.expect("callable created with from_once_fn() has already been consumed");
203+
rust_fn_once(args)
204+
})
205+
}
206+
207+
#[cfg(feature = "trace")] // Test only.
208+
#[cfg(since_api = "4.2")]
209+
#[doc(hidden)]
210+
pub fn __once_fn<F, S>(name: S, rust_function: F) -> Self
211+
where
212+
F: 'static + FnOnce(&[&Variant]) -> Result<Variant, ()>,
213+
S: meta::AsArg<GString>,
214+
{
215+
Self::from_once_fn(name, rust_function)
216+
}
217+
184218
#[cfg(since_api = "4.2")]
185219
pub(crate) fn with_scoped_fn<S, F, Fc, R>(name: S, rust_function: F, callable_usage: Fc) -> R
186220
where
@@ -518,8 +552,6 @@ pub use custom_callable::RustCallable;
518552
#[cfg(since_api = "4.2")]
519553
use custom_callable::*;
520554

521-
use crate::meta;
522-
523555
#[cfg(since_api = "4.2")]
524556
mod custom_callable {
525557
use std::hash::Hash;

godot-core/src/classes/class_runtime.rs

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,12 @@ pub(crate) fn debug_string_variant(
4949
.expect("get_class() must be compatible with StringName");
5050

5151
let refcount = id.is_ref_counted().then(|| {
52-
obj.call("get_reference_count", &[])
52+
let count = obj
53+
.call("get_reference_count", &[])
5354
.try_to_relaxed::<i32>()
54-
.expect("get_reference_count() must return integer") as usize
55+
.expect("get_reference_count() must return integer");
56+
57+
Ok(count as usize)
5558
});
5659

5760
debug_string_parts(f, ty, id, class, refcount, None)
@@ -75,7 +78,11 @@ pub(crate) fn debug_string_variant(
7578
let class = obj.dynamic_class_string();
7679

7780
// Refcount is off-by-one due to now-created Gd<T> from conversion; correct by -1.
78-
let refcount = obj.maybe_refcount().map(|rc| rc.saturating_sub(1));
81+
let refcount = match obj.maybe_refcount() {
82+
Some(Ok(rc)) => Some(Ok(rc.saturating_sub(1))),
83+
Some(Err(e)) => Some(Err(e)),
84+
None => None,
85+
};
7986

8087
debug_string_parts(f, ty, id, class, refcount, None)
8188
}
@@ -121,7 +128,7 @@ fn debug_string_parts(
121128
ty: &str,
122129
id: InstanceId,
123130
class: StringName,
124-
refcount: Option<usize>,
131+
refcount: Option<Result<usize, ()>>,
125132
trait_name: Option<&str>,
126133
) -> std::fmt::Result {
127134
let mut builder = f.debug_struct(ty);
@@ -133,8 +140,14 @@ fn debug_string_parts(
133140
builder.field("trait", &format_args!("{trait_name}"));
134141
}
135142

136-
if let Some(refcount) = refcount {
137-
builder.field("refc", &refcount);
143+
match refcount {
144+
Some(Ok(refcount)) => {
145+
builder.field("refc", &refcount);
146+
}
147+
Some(Err(_)) => {
148+
builder.field("refc", &"(N/A during init or drop)");
149+
}
150+
None => {}
138151
}
139152

140153
builder.finish()

godot-core/src/obj/call_deferred.rs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -61,14 +61,11 @@ where
6161
is_main_thread(),
6262
"`apply_deferred` must be called on the main thread"
6363
);
64-
let mut rust_fn_once = Some(rust_function);
64+
6565
let mut this = self.to_signal_obj().clone();
66-
let callable = Callable::from_local_fn("apply_deferred", move |_| {
67-
let rust_fn_once = rust_fn_once
68-
.take()
69-
.expect("rust_fn_once was already consumed");
66+
let callable = Callable::from_once_fn("apply_deferred", move |_| {
7067
let mut this_mut = T::object_as_mut(&mut this);
71-
rust_fn_once(this_mut.deref_mut());
68+
rust_function(this_mut.deref_mut());
7269
Ok(Variant::nil())
7370
});
7471
callable.call_deferred(&[]);

godot-core/src/obj/gd.rs

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -315,18 +315,32 @@ impl<T: GodotClass> Gd<T> {
315315
}
316316

317317
/// Returns the reference count, if the dynamic object inherits `RefCounted`; and `None` otherwise.
318-
pub(crate) fn maybe_refcount(&self) -> Option<usize> {
318+
///
319+
/// Returns `Err(())` if obtaining reference count failed, due to being called during init/drop.
320+
pub(crate) fn maybe_refcount(&self) -> Option<Result<usize, ()>> {
321+
// May become infallible if implemented via call() on Object, if ref-count bit of instance ID is set.
322+
// This would likely be more efficient, too.
323+
319324
// Fast check if ref-counted without downcast.
320-
self.instance_id().is_ref_counted().then(|| {
321-
let rc = self.raw.with_ref_counted(|refc| refc.get_reference_count());
322-
rc as usize
323-
})
325+
if !self.instance_id().is_ref_counted() {
326+
return None;
327+
}
328+
329+
// Optimization: call `get_reference_count()` directly. Might also increase reliability and obviate the need for Result.
330+
331+
let rc = self
332+
.raw
333+
.try_with_ref_counted(|refc| refc.get_reference_count());
334+
335+
Some(rc.map(|i| i as usize))
324336
}
325337

326338
#[cfg(feature = "trace")] // itest only.
327339
#[doc(hidden)]
328340
pub fn test_refcount(&self) -> Option<usize> {
329341
self.maybe_refcount()
342+
.transpose()
343+
.expect("failed to obtain refcount")
330344
}
331345

332346
/// **Upcast:** convert into a smart pointer to a base class. Always succeeds.
@@ -359,11 +373,20 @@ impl<T: GodotClass> Gd<T> {
359373
.expect("Upcast to Object failed. This is a bug; please report it.")
360374
}
361375

376+
// /// Equivalent to [`upcast_mut::<Object>()`][Self::upcast_mut], but without bounds.
377+
// pub(crate) fn upcast_object_ref(&self) -> &classes::Object {
378+
// self.raw.as_object_ref()
379+
// }
380+
362381
/// Equivalent to [`upcast_mut::<Object>()`][Self::upcast_mut], but without bounds.
363382
pub(crate) fn upcast_object_mut(&mut self) -> &mut classes::Object {
364383
self.raw.as_object_mut()
365384
}
366385

386+
// pub(crate) fn upcast_object_mut_from_ref(&self) -> &mut classes::Object {
387+
// self.raw.as_object_mut()
388+
// }
389+
367390
/// **Upcast shared-ref:** access this object as a shared reference to a base class.
368391
///
369392
/// This is semantically equivalent to multiple applications of [`Self::deref()`]. Not really useful on its own, but combined with
@@ -554,7 +577,10 @@ impl<T: GodotClass> Gd<T> {
554577
pub(crate) unsafe fn from_obj_sys_or_none(
555578
ptr: sys::GDExtensionObjectPtr,
556579
) -> Result<Self, ConvertError> {
557-
Self::try_from_ffi(RawGd::from_obj_sys(ptr))
580+
// Used to have a flag to select RawGd::from_obj_sys_weak(ptr) for Base::to_init_gd(), but solved differently in the end.
581+
let obj = RawGd::from_obj_sys(ptr);
582+
583+
Self::try_from_ffi(obj)
558584
}
559585

560586
/// Initializes this `Gd<T>` from the object pointer as a **strong ref**, meaning

godot-core/src/obj/raw_gd.rs

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ impl<T: GodotClass> RawGd<T> {
195195
///
196196
/// # Panics
197197
/// If `self` does not inherit `RefCounted` or is null.
198-
pub(crate) fn with_ref_counted<R>(&self, apply: impl Fn(&mut classes::RefCounted) -> R) -> R {
198+
pub fn with_ref_counted<R>(&self, apply: impl Fn(&mut classes::RefCounted) -> R) -> R {
199199
// Note: this previously called Declarer::scoped_mut() - however, no need to go through bind() for changes in base RefCounted.
200200
// Any accesses to user objects (e.g. destruction if refc=0) would bind anyway.
201201
//
@@ -204,8 +204,8 @@ impl<T: GodotClass> RawGd<T> {
204204
// self.as_target_mut()
205205
// }
206206

207-
let mut ref_counted = match self.ffi_cast::<classes::RefCounted>() {
208-
Ok(cast_success) => cast_success,
207+
match self.try_with_ref_counted(apply) {
208+
Ok(result) => result,
209209
Err(()) if self.is_null() => {
210210
panic!("RawGd::with_ref_counted(): expected to inherit RefCounted, encountered null pointer");
211211
}
@@ -216,14 +216,25 @@ impl<T: GodotClass> RawGd<T> {
216216

217217
// One way how this may panic is when invoked during destruction of a RefCounted object. The C++ `Object::object_cast_to()`
218218
// function is virtual but cannot be dynamically dispatched in a C++ destructor.
219-
panic!("RawGd::with_ref_counted(): expected to inherit RefCounted, but encountered {class}");
219+
panic!(
220+
"Operation not permitted for object of class {class}:\n\
221+
class is either not RefCounted, or currently in construction/destruction phase"
222+
);
220223
}
221-
};
224+
}
225+
}
222226

227+
/// Fallible version of [`with_ref_counted()`](Self::with_ref_counted), for situations during init/drop when downcast no longer works.
228+
#[expect(clippy::result_unit_err)]
229+
pub fn try_with_ref_counted<R>(
230+
&self,
231+
apply: impl Fn(&mut classes::RefCounted) -> R,
232+
) -> Result<R, ()> {
233+
let mut ref_counted = self.ffi_cast::<classes::RefCounted>()?;
223234
let return_val = apply(ref_counted.as_dest_mut().as_target_mut());
224235

225236
// CastSuccess is forgotten when dropped, so no ownership transfer.
226-
return_val
237+
Ok(return_val)
227238
}
228239

229240
/// Enables outer `Gd` APIs or bypasses additional null checks, in cases where `RawGd` is guaranteed non-null.
@@ -716,7 +727,7 @@ impl<T: GodotClass> Clone for RawGd<T> {
716727
fn clone(&self) -> Self {
717728
out!("RawGd::clone: {self:?} (before clone)");
718729

719-
if self.is_null() {
730+
let cloned = if self.is_null() {
720731
Self::null()
721732
} else {
722733
self.check_rtti("clone");
@@ -728,7 +739,10 @@ impl<T: GodotClass> Clone for RawGd<T> {
728739
cached_storage_ptr: self.cached_storage_ptr.clone(),
729740
};
730741
copy.with_inc_refcount()
731-
}
742+
};
743+
744+
out!(" {self:?} (after clone)");
745+
cloned
732746
}
733747
}
734748

itest/rust/src/builtin_tests/containers/callable_test.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -648,6 +648,20 @@ pub mod custom_callable {
648648
assert!(signal.is_connected(&identical_callable));
649649
}
650650

651+
#[cfg(since_api = "4.2")]
652+
#[itest]
653+
fn callable_from_once_fn() {
654+
let callable = Callable::__once_fn("once_test", move |_| Ok(42.to_variant()));
655+
656+
// First call should succeed.
657+
let result = callable.call(&[]);
658+
assert_eq!(result.to::<i32>(), 42);
659+
660+
// Second call should fail (panic currently isn't propagated, see other tests).
661+
let result = callable.call(&[]);
662+
assert!(result.is_nil());
663+
}
664+
651665
// ------------------------------------------------------------------------------------------------------------------------------------------
652666
// Helper structs and functions for custom callables
653667

0 commit comments

Comments
 (0)