Skip to content

Commit e01d2ea

Browse files
authored
Merge pull request #1406 from godot-rust/qol/opt-constrained-types
Add `GodotImmutable` to constrain `#[opt(default)]` types
2 parents b2a2a58 + f5eff0d commit e01d2ea

File tree

5 files changed

+137
-5
lines changed

5 files changed

+137
-5
lines changed

godot-core/src/meta/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ pub use signature::trace;
7676
#[doc(hidden)]
7777
pub use signature::*;
7878
pub use signed_range::{wrapped, SignedRange};
79-
pub use traits::{ArrayElement, GodotType, PackedArrayElement};
79+
pub use traits::{ArrayElement, GodotImmutable, GodotType, PackedArrayElement};
8080
pub use uniform_object_deref::UniformObjectDeref;
8181

8282
// Public due to signals emit() needing it. Should be made pub(crate) again if that changes.

godot-core/src/meta/traits.rs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,3 +236,88 @@ pub(crate) fn element_godot_type_name<T: ArrayElement>() -> String {
236236
// pub(crate) fn element_godot_type_name<T: ArrayElement>() -> String {
237237
// <T::Via as GodotType>::godot_type_name()
238238
// }
239+
240+
// ----------------------------------------------------------------------------------------------------------------------------------------------
241+
242+
/// Implemented for types that can be used as immutable default parameters in `#[func]` methods.
243+
///
244+
/// This trait ensures that default parameter values cannot be mutated by callers, preventing the Python "mutable default argument" problem
245+
/// where a single default value is shared across multiple calls.
246+
///
247+
/// Post-processes the default value in some cases, e.g. makes `Array<T>` read-only via `into_read_only()`.
248+
///
249+
/// At the moment, this trait is conservatively implemented for types where immutability can be statically guaranteed.
250+
/// Depending on usage, the API might be expanded in the future to allow defaults whose immutability is only determined
251+
/// at runtime (e.g. untyped arrays/dictionaries where all element types are immutable).
252+
///
253+
/// # Safety
254+
/// Allows to use the implementors in a limited `Sync` context. Implementing this trait asserts that `Self` is either:
255+
/// - `Copy`, i.e. each instance is truly independent.
256+
/// - Thread-safe in the sense that `clone()` is thread-safe. Individual clones must not offer a way to mutate the value or cause race conditions.
257+
#[diagnostic::on_unimplemented(
258+
message = "#[opt(default = ...)] only supports a set of truly immutable types",
259+
label = "this type is not immutable and thus not eligible for a default value"
260+
)]
261+
pub unsafe trait GodotImmutable: GodotConvert + Sized {
262+
fn into_runtime_immutable(self) -> Self {
263+
self
264+
}
265+
}
266+
267+
mod godot_immutable_impls {
268+
use super::GodotImmutable;
269+
use crate::builtin::*;
270+
use crate::meta::ArrayElement;
271+
272+
unsafe impl GodotImmutable for bool {}
273+
unsafe impl GodotImmutable for i8 {}
274+
unsafe impl GodotImmutable for u8 {}
275+
unsafe impl GodotImmutable for i16 {}
276+
unsafe impl GodotImmutable for u16 {}
277+
unsafe impl GodotImmutable for i32 {}
278+
unsafe impl GodotImmutable for u32 {}
279+
unsafe impl GodotImmutable for i64 {}
280+
unsafe impl GodotImmutable for f32 {}
281+
unsafe impl GodotImmutable for f64 {}
282+
283+
// No NodePath, Callable, Signal, Rid, Variant.
284+
unsafe impl GodotImmutable for Aabb {}
285+
unsafe impl GodotImmutable for Basis {}
286+
unsafe impl GodotImmutable for Color {}
287+
unsafe impl GodotImmutable for GString {}
288+
unsafe impl GodotImmutable for Plane {}
289+
unsafe impl GodotImmutable for Projection {}
290+
unsafe impl GodotImmutable for Quaternion {}
291+
unsafe impl GodotImmutable for Rect2 {}
292+
unsafe impl GodotImmutable for Rect2i {}
293+
unsafe impl GodotImmutable for StringName {}
294+
unsafe impl GodotImmutable for Transform2D {}
295+
unsafe impl GodotImmutable for Transform3D {}
296+
unsafe impl GodotImmutable for Vector2 {}
297+
unsafe impl GodotImmutable for Vector2i {}
298+
unsafe impl GodotImmutable for Vector3 {}
299+
unsafe impl GodotImmutable for Vector3i {}
300+
unsafe impl GodotImmutable for Vector4 {}
301+
unsafe impl GodotImmutable for Vector4i {}
302+
303+
unsafe impl GodotImmutable for PackedByteArray {}
304+
unsafe impl GodotImmutable for PackedColorArray {}
305+
unsafe impl GodotImmutable for PackedFloat32Array {}
306+
unsafe impl GodotImmutable for PackedFloat64Array {}
307+
unsafe impl GodotImmutable for PackedInt32Array {}
308+
unsafe impl GodotImmutable for PackedInt64Array {}
309+
unsafe impl GodotImmutable for PackedStringArray {}
310+
unsafe impl GodotImmutable for PackedVector2Array {}
311+
unsafe impl GodotImmutable for PackedVector3Array {}
312+
#[cfg(since_api = "4.3")]
313+
unsafe impl GodotImmutable for PackedVector4Array {}
314+
315+
unsafe impl<T> GodotImmutable for Array<T>
316+
where
317+
T: GodotImmutable + ArrayElement,
318+
{
319+
fn into_runtime_immutable(self) -> Self {
320+
self.into_read_only()
321+
}
322+
}
323+
}

godot-core/src/private.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,25 @@ pub fn is_class_runtime(is_tool: bool) -> bool {
238238
global_config.tool_only_in_editor
239239
}
240240

241+
/// Converts a default parameter value to a runtime-immutable `Variant`.
242+
///
243+
/// This function is used internally by the `#[opt(default)]` attribute to:
244+
/// 1. Convert the value using `AsArg` trait for argument conversions (e.g. `"str"` for `AsArg<GString>`).
245+
/// 2. Apply immutability transformation.
246+
/// 3. Convert to `Variant` for Godot's storage.
247+
pub fn opt_default_value<T>(value: impl crate::meta::AsArg<T>) -> crate::builtin::Variant
248+
where
249+
T: crate::meta::GodotImmutable + crate::meta::ToGodot + Clone,
250+
{
251+
// We currently need cow_into_owned() to create an owned value for the immutability transform. This may be revisited once `#[opt]`
252+
// supports more types (e.g. `Gd<RefCounted>`, where `cow_into_owned()` would increment ref-counts).
253+
254+
let value = crate::meta::AsArg::<T>::into_arg(value);
255+
let value = value.cow_into_owned();
256+
let value = <T as crate::meta::GodotImmutable>::into_runtime_immutable(value);
257+
crate::builtin::Variant::from(value)
258+
}
259+
241260
// ----------------------------------------------------------------------------------------------------------------------------------------------
242261
// Panic *hook* management
243262

godot-macros/src/class/data_models/func.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -217,9 +217,7 @@ fn make_default_argument_vec(
217217
.zip(optional_param_types)
218218
.map(|(value, param_type)| {
219219
quote! {
220-
::godot::builtin::Variant::from(
221-
::godot::meta::AsArg::<#param_type>::into_arg(#value)
222-
)
220+
::godot::private::opt_default_value::<#param_type>(#value)
223221
}
224222
});
225223

itest/rust/src/register_tests/func_test.rs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,15 @@ impl FuncObj {
6666
varray![required, string, integer]
6767
}
6868

69+
#[func]
70+
fn method_with_immutable_array_default(
71+
&self,
72+
#[opt(default = &array![1, 2, 3])] arr: Array<i64>,
73+
) -> Array<i64> {
74+
arr
75+
}
76+
77+
/* For now, Gd<T> types cannot be used as default parameters due to immutability requirement.
6978
#[func]
7079
fn static_with_defaults(
7180
#[opt(default = &RefCounted::new_gd())] mut required: Gd<RefCounted>,
@@ -79,6 +88,7 @@ impl FuncObj {
7988
required.set_meta("nullable_id", &id.to_variant());
8089
required
8190
}
91+
*/
8292
}
8393

8494
impl FuncObj {
@@ -345,6 +355,7 @@ fn func_default_parameters() {
345355
let c = obj.call("method_with_defaults", vslice![2, "Another string", 456]);
346356
assert_eq!(c.to::<VariantArray>(), varray![2, "Another string", 456]);
347357

358+
/* For now, Gd<T> defaults are disabled due to immutability.
348359
// Test that object is passed through, and that Option<Gd> with default Gd::null_arg() works.
349360
let first = RefCounted::new_gd();
350361
let d = obj
@@ -360,8 +371,10 @@ fn func_default_parameters() {
360371
.to::<Gd<RefCounted>>();
361372
assert_eq!(e.instance_id(), first.instance_id());
362373
assert_eq!(e.get_meta("nullable_id"), second.instance_id().to_variant());
374+
*/
363375
}
364376

377+
/* For now, Gd<T> defaults are disabled due to immutability.
365378
#[itest]
366379
fn func_defaults_re_evaluate_expr() {
367380
// ClassDb::class_call_static() added in Godot 4.4, but non-static dispatch works even before.
@@ -386,8 +399,23 @@ fn func_defaults_re_evaluate_expr() {
386399
"#[opt = EXPR] should create evaluate EXPR on each call"
387400
);
388401
}
402+
*/
389403

390-
// No test for Gd::from_object(), as that simply moves the existing object without running user code.
404+
#[itest]
405+
fn func_immutable_defaults() {
406+
let mut obj = FuncObj::new_gd();
407+
408+
// Test Array<T> default parameter.
409+
let arr = obj
410+
.call("method_with_immutable_array_default", &[])
411+
.to::<Array<i64>>();
412+
assert_eq!(arr, array![1, 2, 3]);
413+
414+
assert!(
415+
arr.is_read_only(),
416+
"GodotImmutable trait did its job to make array read-only"
417+
);
418+
}
391419

392420
#[itest]
393421
fn cfg_doesnt_interfere_with_valid_method_impls() {
@@ -431,6 +459,8 @@ fn cfg_removes_or_keeps_signals() {
431459
assert!(!class_has_signal::<GdSelfObj>("cfg_removes_signal"));
432460
}
433461

462+
// No test for Gd::from_object(), as that simply moves the existing object without running user code.
463+
434464
// ----------------------------------------------------------------------------------------------------------------------------------------------
435465
// Helpers
436466

0 commit comments

Comments
 (0)