Skip to content

Commit 6588315

Browse files
authored
Merge pull request #1320 from godot-rust/feature/export-limits
Numeric `#[export]` limits and type checks
2 parents 1fff463 + 25872f4 commit 6588315

File tree

11 files changed

+157
-76
lines changed

11 files changed

+157
-76
lines changed

godot-core/src/builtin/string/node_path.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
use std::fmt;
99

1010
use godot_ffi as sys;
11-
use godot_ffi::{ffi_methods, ExtVariantType, GdextBuild, GodotFfi};
11+
use godot_ffi::{ffi_methods, ExtVariantType, GodotFfi};
1212

1313
use super::{GString, StringName};
1414
use crate::builtin::inner;
@@ -160,7 +160,7 @@ impl NodePath {
160160
pub fn subpath(&self, range: impl SignedRange) -> NodePath {
161161
let (from, exclusive_end) = range.signed();
162162
// Polyfill for bug https://github.com/godotengine/godot/pull/100954, fixed in 4.4.
163-
let begin = if GdextBuild::since_api("4.4") {
163+
let begin = if sys::GdextBuild::since_api("4.4") {
164164
from
165165
} else {
166166
let name_count = self.get_name_count() as i64;

godot-core/src/meta/property_info.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use crate::meta::{element_godot_type_name, ArrayElement, ClassId, GodotType, Pac
1313
use crate::obj::{bounds, Bounds, EngineBitfield, EngineEnum, GodotClass};
1414
use crate::registry::class::get_dyn_property_hint_string;
1515
use crate::registry::property::{Export, Var};
16-
use crate::{classes, sys};
16+
use crate::{classes, godot_str, sys};
1717

1818
/// Describes a property in Godot.
1919
///
@@ -298,6 +298,13 @@ impl PropertyHintInfo {
298298
}
299299
}
300300

301+
pub fn type_name_range<T: GodotType + std::fmt::Display>(lower: T, upper: T) -> Self {
302+
Self {
303+
hint: PropertyHint::RANGE,
304+
hint_string: godot_str!("{lower},{upper}"), // also valid for <4.3.
305+
}
306+
}
307+
301308
/// Use for `#[var]` properties -- [`PROPERTY_HINT_ARRAY_TYPE`](PropertyHint::ARRAY_TYPE) with the type name as hint string.
302309
pub fn var_array_element<T: ArrayElement>() -> Self {
303310
Self {

godot-core/src/registry/property/mod.rs

Lines changed: 47 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ use crate::global::PropertyHint;
1717
use crate::meta::{ClassId, FromGodot, GodotConvert, GodotType, PropertyHintInfo, ToGodot};
1818
use crate::obj::{EngineEnum, GodotClass};
1919

20+
// ----------------------------------------------------------------------------------------------------------------------------------------------
21+
// Child modules
22+
2023
mod phantom_var;
2124

2225
pub use phantom_var::PhantomVar;
@@ -229,18 +232,22 @@ where
229232
/// Each function is named the same as the equivalent Godot annotation.
230233
/// For instance, `@export_range` in Godot is `fn export_range` here.
231234
pub mod export_info_functions {
235+
use std::fmt;
236+
use std::fmt::Write;
237+
232238
use godot_ffi::VariantType;
233239

234240
use crate::builtin::GString;
235241
use crate::global::PropertyHint;
236242
use crate::meta::{GodotType, PropertyHintInfo, PropertyInfo};
237243
use crate::obj::EngineEnum;
238244
use crate::registry::property::Export;
245+
use crate::{godot_str, sys};
239246

240247
/// Turn a list of variables into a comma separated string containing only the identifiers corresponding
241248
/// to a true boolean variable.
242249
macro_rules! comma_separate_boolean_idents {
243-
($( $ident:ident),* $(,)?) => {
250+
($( $ident:ident ),* $(,)?) => {
244251
{
245252
let mut strings = Vec::new();
246253

@@ -261,24 +268,26 @@ pub mod export_info_functions {
261268
/// You'll never call this function itself, but will instead use the macro `#[export(range=(...))]`, as below. The syntax is
262269
/// very similar to Godot's [`@export_range`](https://docs.godotengine.org/en/stable/classes/class_%40gdscript.html#class-gdscript-annotation-export-range).
263270
/// `min`, `max`, and `step` are `f32` positional arguments, with `step` being optional and defaulting to `1.0`. The rest of
264-
/// the arguments can be written in any order. The symbols of type `bool` just need to have those symbols written, and those of type `Option<T>` will be written as `{KEY}={VALUE}`, e.g. `suffix="px"`.
271+
/// the arguments can be written in any order. The symbols of type `bool` just need to have those symbols written, and those of type
272+
/// `Option<T>` will be written as `{KEY}={VALUE}`, e.g. `suffix="px"`.
265273
///
266274
/// ```
267275
/// # use godot::prelude::*;
268276
/// #[derive(GodotClass)]
269277
/// #[class(init, base=Node)]
270278
/// struct MyClassWithRangedValues {
271-
/// #[export(range=(0.0, 400.0, 1.0, or_greater, suffix="px"))]
279+
/// #[export(range=(0, 400, 1, or_greater, suffix="px"))]
272280
/// icon_width: i32,
281+
///
273282
/// #[export(range=(-180.0, 180.0, degrees))]
274283
/// angle: f32,
275284
/// }
276285
/// ```
277286
#[allow(clippy::too_many_arguments)]
278-
pub fn export_range(
279-
min: f64,
280-
max: f64,
281-
step: Option<f64>,
287+
pub fn export_range<T: Export + fmt::Display>(
288+
min: T,
289+
max: T,
290+
step: Option<T>,
282291
or_greater: bool,
283292
or_less: bool,
284293
exp: bool,
@@ -307,10 +316,10 @@ pub mod export_info_functions {
307316

308317
let mut hint_string = hint_beginning;
309318
if !rest.is_empty() {
310-
hint_string.push_str(&format!(",{rest}"));
319+
write!(hint_string, ",{rest}").unwrap();
311320
}
312321
if let Some(suffix) = suffix {
313-
hint_string.push_str(&format!(",suffix:{suffix}"));
322+
write!(hint_string, ",suffix:{suffix}").unwrap();
314323
}
315324

316325
PropertyHintInfo {
@@ -319,6 +328,7 @@ pub mod export_info_functions {
319328
}
320329
}
321330

331+
// See also godot-macros > c_style_enum.rs; some code duplication.
322332
#[doc(hidden)]
323333
pub struct ExportValueWithKey<T> {
324334
variant: String,
@@ -339,23 +349,19 @@ pub mod export_info_functions {
339349
where
340350
for<'a> &'a V: Into<Self>,
341351
{
342-
let values = values
343-
.iter()
344-
.map(|v| v.into().as_hint_string())
345-
.collect::<Vec<_>>();
346-
347-
values.join(",")
352+
sys::join_with(values.iter(), ",", |v| (*v).into().as_hint_string())
348353
}
349354
}
350355

356+
/// Allows syntax `("name", None)` and `("name", Some(10))`.
351357
impl<T, S> From<&(S, Option<T>)> for ExportValueWithKey<T>
352358
where
353359
T: Clone,
354360
S: AsRef<str>,
355361
{
356362
fn from((variant, key): &(S, Option<T>)) -> Self {
357363
Self {
358-
variant: variant.as_ref().into(),
364+
variant: variant.as_ref().to_string(),
359365
key: key.clone(),
360366
}
361367
}
@@ -493,14 +499,14 @@ pub mod export_info_functions {
493499

494500
PropertyHintInfo {
495501
hint: PropertyHint::TYPE_STRING,
496-
hint_string: GString::from(&format!("{hint_string}:{filter}")),
502+
hint_string: godot_str!("{hint_string}:{filter}"),
497503
}
498504
}
499505

500-
pub fn export_placeholder<S: AsRef<str>>(placeholder: S) -> PropertyHintInfo {
506+
pub fn export_placeholder(placeholder: &str) -> PropertyHintInfo {
501507
PropertyHintInfo {
502508
hint: PropertyHint::PLACEHOLDER_TEXT,
503-
hint_string: GString::from(placeholder.as_ref()),
509+
hint_string: GString::from(placeholder),
504510
}
505511
}
506512

@@ -544,6 +550,12 @@ mod export_impls {
544550
impl_property_by_godot_convert!(@property $Ty);
545551
};
546552

553+
($Ty:ty, limit_range) => {
554+
impl_property_by_godot_convert!(@property $Ty);
555+
impl_property_by_godot_convert!(@export_range $Ty);
556+
impl_property_by_godot_convert!(@builtin $Ty);
557+
};
558+
547559
($Ty:ty) => {
548560
impl_property_by_godot_convert!(@property $Ty);
549561
impl_property_by_godot_convert!(@export $Ty);
@@ -570,6 +582,14 @@ mod export_impls {
570582
}
571583
};
572584

585+
(@export_range $Ty:ty) => {
586+
impl Export for $Ty {
587+
fn export_hint() -> PropertyHintInfo {
588+
PropertyHintInfo::type_name_range(<$Ty>::MIN, <$Ty>::MAX)
589+
}
590+
}
591+
};
592+
573593
(@builtin $Ty:ty) => {
574594
impl BuiltinExport for $Ty {}
575595
}
@@ -618,16 +638,14 @@ mod export_impls {
618638
// then the property will just round the value or become inf.
619639
impl_property_by_godot_convert!(f32);
620640

621-
// Godot uses i64 internally for integers, and if Godot tries to pass an invalid integer into a property
622-
// accepting one of the below values then rust will panic. In the editor this will appear as the property
623-
// failing to be set to a value and an error printed in the console. During runtime this will crash the
624-
// program and print the panic from rust stating that the property cannot store the value.
625-
impl_property_by_godot_convert!(i32);
626-
impl_property_by_godot_convert!(i16);
627-
impl_property_by_godot_convert!(i8);
628-
impl_property_by_godot_convert!(u32);
629-
impl_property_by_godot_convert!(u16);
630-
impl_property_by_godot_convert!(u8);
641+
// Godot uses i64 internally for integers. We use the @export_range hints to limit the integers to a sub-range of i64, so the editor UI
642+
// can only set the respective values. If values are assigned in other ways (e.g. GDScript), a panic may occur, causing a Godot error.
643+
impl_property_by_godot_convert!(i32, limit_range);
644+
impl_property_by_godot_convert!(i16, limit_range);
645+
impl_property_by_godot_convert!(i8, limit_range);
646+
impl_property_by_godot_convert!(u32, limit_range);
647+
impl_property_by_godot_convert!(u16, limit_range);
648+
impl_property_by_godot_convert!(u8, limit_range);
631649

632650
// Callables and Signals are useless when exported to the editor, so we only need to make them available as
633651
// properties.

godot-ffi/src/toolbox.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,10 +177,11 @@ where
177177
// String formatting by itself is an infallible operation.
178178
// Read more at: https://doc.rust-lang.org/stable/std/fmt/index.html#formatting-traits
179179
write!(&mut result, "{first}", first = format_elem(&first))
180-
.expect("Formatter should not fail!");
180+
.expect("formatter should not fail");
181+
181182
for item in iter {
182183
write!(&mut result, "{sep}{item}", item = format_elem(&item))
183-
.expect("Formatter should not fail!");
184+
.expect("formatter should not fail");
184185
}
185186
}
186187
result

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

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,18 @@ use quote::quote;
1212

1313
use crate::util::{ident, KvParser, ListParser};
1414
use crate::ParseResult;
15-
1615
pub struct FieldExport {
1716
pub export_type: ExportType,
1817
pub span: Span,
1918
}
2019

2120
impl FieldExport {
22-
pub(crate) fn new_from_kv(parser: &mut KvParser) -> ParseResult<Self> {
21+
pub(crate) fn new_from_kv(
22+
parser: &mut KvParser,
23+
field_ty: venial::TypeExpr,
24+
) -> ParseResult<Self> {
2325
let span = parser.span();
24-
let export_type = ExportType::new_from_kv(parser)?;
26+
let export_type = ExportType::new_from_kv(parser, field_ty)?;
2527
Ok(Self { export_type, span })
2628
}
2729

@@ -65,6 +67,7 @@ pub enum ExportType {
6567
/// ### Property hints
6668
/// - `RANGE`
6769
Range {
70+
field_ty: venial::TypeExpr,
6871
min: TokenStream,
6972
max: TokenStream,
7073
step: TokenStream,
@@ -168,13 +171,15 @@ impl ExportType {
168171
/// - `@export_{flags/enum}("elem1", "elem2:key2", ...)`
169172
/// becomes
170173
/// `#[export(flags/enum = (elem1, elem2 = key2, ...))]`
171-
pub(crate) fn new_from_kv(parser: &mut KvParser) -> ParseResult<Self> {
174+
pub(crate) fn new_from_kv(
175+
parser: &mut KvParser,
176+
field_ty: venial::TypeExpr,
177+
) -> ParseResult<Self> {
172178
if parser.handle_alone("storage")? {
173179
return Self::new_storage();
174180
}
175-
176181
if let Some(list_parser) = parser.handle_list("range")? {
177-
return Self::new_range_list(list_parser);
182+
return Self::new_range_list(list_parser, field_ty);
178183
}
179184

180185
if let Some(list_parser) = parser.handle_list("enum")? {
@@ -300,7 +305,7 @@ impl ExportType {
300305
Ok(Self::Storage)
301306
}
302307

303-
fn new_range_list(mut parser: ListParser) -> ParseResult<Self> {
308+
fn new_range_list(mut parser: ListParser, field_ty: venial::TypeExpr) -> ParseResult<Self> {
304309
const FLAG_OPTIONS: [&str; 7] = [
305310
"or_greater",
306311
"or_less",
@@ -314,6 +319,7 @@ impl ExportType {
314319

315320
let min = parser.next_expr()?;
316321
let max = parser.next_expr()?;
322+
317323
// If there is a next element, and it is a literal, we take its tokens directly.
318324
let step = if parser.peek().is_some_and(|kv| kv.as_literal().is_ok()) {
319325
let value = parser
@@ -330,6 +336,7 @@ impl ExportType {
330336
loop {
331337
let key_maybe_value =
332338
parser.next_allowed_key_optional_value(&FLAG_OPTIONS, &KV_OPTIONS)?;
339+
333340
match key_maybe_value {
334341
Some((option, None)) => {
335342
flags.insert(option.to_string());
@@ -344,6 +351,7 @@ impl ExportType {
344351
parser.finish()?;
345352

346353
Ok(Self::Range {
354+
field_ty,
347355
min,
348356
max,
349357
step,
@@ -411,12 +419,19 @@ impl ExportType {
411419
}
412420

413421
macro_rules! quote_export_func {
414-
($function_name:ident($($tt:tt)*)) => {
422+
($function_name:ident ($($tt:tt)*) ) => {
415423
Some(quote! {
416424
::godot::register::property::export_info_functions::$function_name($($tt)*)
417425
})
418426
};
419427

428+
// Use [ ] for generic args due to parsing ambiguity with ::< > turbofish.
429+
($function_name:ident [ $($generic_args:tt)* ] ($($tt:tt)*) ) => {
430+
Some(quote! {
431+
::godot::register::property::export_info_functions::$function_name::< $($generic_args)* >($($tt)*)
432+
})
433+
};
434+
420435
// Passes in a previously declared local `type FieldType = ...` as first generic argument.
421436
// Doesn't work if function takes other generic arguments -- in that case it could be converted to a Type<...> parameter.
422437
($function_name:ident < T > ($($tt:tt)*)) => {
@@ -434,6 +449,7 @@ impl ExportType {
434449
Self::Storage => quote_export_func! { export_storage() },
435450

436451
Self::Range {
452+
field_ty,
437453
min,
438454
max,
439455
step,
@@ -451,9 +467,11 @@ impl ExportType {
451467
} else {
452468
quote! { None }
453469
};
470+
454471
let export_func = quote_export_func! {
455-
export_range(#min, #max, #step, #or_greater, #or_less, #exp, #radians_as_degrees || #radians, #degrees, #hide_slider, #suffix)
472+
export_range [ #field_ty ] (#min, #max, #step, #or_greater, #or_less, #exp, #radians_as_degrees || #radians, #degrees, #hide_slider, #suffix)
456473
}?;
474+
457475
let deprecation_warning = if *radians {
458476
// For some reason, rustfmt formatting like this. Probably a bug.
459477
// See https://github.com/godot-rust/gdext/pull/783#discussion_r1669105958 and
@@ -465,6 +483,7 @@ impl ExportType {
465483
} else {
466484
quote! { #export_func }
467485
};
486+
468487
Some(quote! {
469488
#deprecation_warning
470489
})

godot-macros/src/class/derive_godot_class.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -680,7 +680,7 @@ fn parse_fields(
680680

681681
// #[export]
682682
if let Some(mut parser) = KvParser::parse(&named_field.attributes, "export")? {
683-
let export = FieldExport::new_from_kv(&mut parser)?;
683+
let export = FieldExport::new_from_kv(&mut parser, named_field.ty.clone())?;
684684
field.export = Some(export);
685685
parser.finish()?;
686686
}

godot-macros/src/derive/data_models/c_style_enum.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,8 @@ impl CStyleEnum {
122122

123123
/// Return a hint string for use with `PropertyHint::ENUM` where the variants are just kept as strings.
124124
pub fn to_string_hint(&self) -> TokenStream {
125+
// See also export_info_functions::slice_as_hint_string(), some code duplication.
126+
125127
let hint_string = self
126128
.enumerator_names
127129
.iter()

0 commit comments

Comments
 (0)