Skip to content

Add groups export. #1214

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

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
32 changes: 31 additions & 1 deletion godot-core/src/registry/godot_register_wrappers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

//! Internal registration machinery used by proc-macro APIs.

use crate::builtin::StringName;
use crate::builtin::{GString, StringName};
use crate::global::PropertyUsageFlags;
use crate::meta::{ClassName, GodotConvert, GodotType, PropertyHintInfo, PropertyInfo};
use crate::obj::GodotClass;
Expand Down Expand Up @@ -79,3 +79,33 @@ fn register_var_or_export_inner(
);
}
}

pub fn register_group<C: GodotClass>(group_name: &str, prefix: &str) {
let group_name = GString::from(group_name);
let prefix = GString::from(prefix);
let class_name = C::class_name();

unsafe {
sys::interface_fn!(classdb_register_extension_class_property_group)(
sys::get_library(),
class_name.string_sys(),
group_name.string_sys(),
prefix.string_sys(),
);
}
}

pub fn register_subgroup<C: GodotClass>(subgroup_name: &str, prefix: &str) {
let subgroup_name = GString::from(subgroup_name);
let prefix = GString::from(prefix);
let class_name = C::class_name();

unsafe {
sys::interface_fn!(classdb_register_extension_class_property_subgroup)(
sys::get_library(),
class_name.string_sys(),
subgroup_name.string_sys(),
prefix.string_sys(),
);
}
}
19 changes: 5 additions & 14 deletions godot-macros/src/class/data_models/field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

use crate::class::data_models::group_export::FieldGroup;
use crate::class::{FieldExport, FieldVar};
use crate::util::{error, KvParser};
use proc_macro2::{Ident, Span, TokenStream};
Expand All @@ -16,6 +17,8 @@ pub struct Field {
pub default_val: Option<FieldDefault>,
pub var: Option<FieldVar>,
pub export: Option<FieldExport>,
pub group: Option<FieldGroup>,
pub subgroup: Option<FieldGroup>,
pub is_onready: bool,
pub is_oneditor: bool,
#[cfg(feature = "register-docs")]
Expand All @@ -31,6 +34,8 @@ impl Field {
default_val: None,
var: None,
export: None,
group: None,
subgroup: None,
is_onready: false,
is_oneditor: false,
#[cfg(feature = "register-docs")]
Expand Down Expand Up @@ -110,20 +115,6 @@ pub enum FieldCond {
IsOnEditor,
}

pub struct Fields {
/// All fields except `base_field`.
pub all_fields: Vec<Field>,

/// The field with type `Base<T>`, if available.
pub base_field: Option<Field>,

/// Deprecation warnings.
pub deprecations: Vec<TokenStream>,

/// Errors during macro evaluation that shouldn't abort the execution of the macro.
pub errors: Vec<venial::Error>,
}

#[derive(Clone)]
pub struct FieldDefault {
pub default_val: TokenStream,
Expand Down
48 changes: 48 additions & 0 deletions godot-macros/src/class/data_models/fields.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* 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/.
*/

use crate::class::Field;
use crate::util::bail;
use crate::ParseResult;
use proc_macro2::{Punct, TokenStream};
use std::fmt::Display;

pub struct Fields {
/// All fields except `base_field`.
pub all_fields: Vec<Field>,

/// The field with type `Base<T>`, if available.
pub base_field: Option<Field>,

/// Deprecation warnings.
pub deprecations: Vec<TokenStream>,

/// Errors during macro evaluation that shouldn't abort the execution of the macro.
pub errors: Vec<venial::Error>,
}

/// Fetches data for all named fields for a struct.
///
/// Errors if `class` is a tuple struct.
pub fn named_fields(
class: &venial::Struct,
derive_macro_name: impl Display,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Making this function generic means it has to be monomorphized for every slightly different instantiation. Especially for internal APIs, I'd say compile times + simplicity are more important than minor convenience -> just use &str here.

(There are probably other places across the code where this happens...)

) -> ParseResult<Vec<(venial::NamedField, Punct)>> {
// This is separate from parse_fields to improve compile errors. The errors from here demand larger and more non-local changes from the API
// user than those from parse_struct_attributes, so this must be run first.
match &class.fields {
// TODO disallow unit structs in the future
// It often happens that over time, a registered class starts to require a base field.
// Extending a {} struct requires breaking less code, so we should encourage it from the start.
venial::Fields::Unit => Ok(vec![]),
venial::Fields::Tuple(_) => bail!(
&class.fields,
"{derive_macro_name} is not supported for tuple structs",
)?,
venial::Fields::Named(fields) => Ok(fields.fields.inner.clone()),
}
}
42 changes: 42 additions & 0 deletions godot-macros/src/class/data_models/group_export.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* 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/.
*/

// Note: group membership for properties in Godot is based on the order of their registration.
// All the properties belong to group or subgroup registered beforehand, identically as in gdscript.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// All the properties belong to group or subgroup registered beforehand, identically as in gdscript.
// All the properties belong to group or subgroup registered beforehand, identically as in GDScript.

// Initial implementation providing clap-like API with an explicit sorting
// & groups/subgroups declared for each field (`#[export(group = ..., subgroup = ...)]`
// can be found at: https://github.com/godot-rust/gdext/pull/1214.

use crate::util::KvParser;
use crate::ParseResult;
use proc_macro2::Literal;

/// Specifies group or subgroup which starts with a given field.
/// Group membership for properties in Godot is based on the order of their registration –
/// i.e. given field belongs to group declared beforehand (for example with some previous field).
pub struct FieldGroup {
pub(crate) name: Literal,
pub(crate) prefix: Literal,
}

impl FieldGroup {
pub(crate) fn new_from_kv(parser: &mut KvParser) -> ParseResult<Self> {
// Groups without name and prefix are totally valid in godot and used
// to "break" out of groups and subgroups. See:
Comment on lines +28 to +29
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Groups without name and prefix are totally valid in godot and used
// to "break" out of groups and subgroups. See:
// Groups without name and prefix are totally valid in Godot and used to "break out" of groups and subgroups. See:

// https://docs.godotengine.org/en/4.4/classes/[email protected]#class-gdscript-annotation-export-group
let (name, prefix) = (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no point in using a tuple here, just keep things simple and declare two variables 🙂

parser
.handle_literal("name", "String")?
.unwrap_or(Literal::string("")),
Comment on lines +32 to +34
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

name is optional? Why do we support this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

empty group/prefix is used to break out of groups in gdscript (works the same via GDExtension) https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/gdscript_exports.html#grouping-exports. At some point we need to provide name anyway, so there is no point to use Option instead

Copy link
Member

@Bromeon Bromeon Jul 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but "we need to support """ doesn't mean the name should be optional 🙂

According to GDScript docs, the name key is required, too.

"Break out" is also not happening that often, so people can still specify "" explicitly. And with the export_fwd proposal, group or subgroup would anyway be required.

parser
.handle_literal("prefix", "String")?
.unwrap_or(Literal::string("")),
);

Ok(Self { name, prefix })
}
}
54 changes: 46 additions & 8 deletions godot-macros/src/class/data_models/property.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@

//! Parses the `#[var]` and `#[export]` attributes on fields.

use crate::class::{Field, FieldVar, Fields, GetSet, GetterSetterImpl, UsageFlags};
use crate::util::{format_funcs_collection_constant, format_funcs_collection_struct};
use crate::class::data_models::fields::Fields;
use crate::class::data_models::group_export::FieldGroup;
use crate::class::{Field, FieldVar, GetSet, GetterSetterImpl, UsageFlags};
use crate::util::{format_funcs_collection_constant, format_funcs_collection_struct, ident};
use proc_macro2::{Ident, TokenStream};
use quote::quote;

Expand Down Expand Up @@ -48,6 +50,8 @@ pub fn make_property_impl(class_name: &Ident, fields: &Fields) -> TokenStream {
ty: field_type,
var,
export,
group,
subgroup,
..
} = field;

Expand All @@ -59,18 +63,17 @@ pub fn make_property_impl(class_name: &Ident, fields: &Fields) -> TokenStream {
} else {
UsageFlags::InferredExport
};
Some(FieldVar {
FieldVar {
usage_flags,
..Default::default()
})
}
}

(_, var) => var.clone(),
(_, Some(var)) => var.clone(),
_ => continue,
};

let Some(var) = var else {
continue;
};
maybe_register_groups(group, subgroup, &mut export_tokens, class_name);

let field_name = field_ident.to_string();

Expand Down Expand Up @@ -220,3 +223,38 @@ fn make_getter_setter(

quote! { #funcs_collection::#constant }
}

fn maybe_register_groups(
group: &Option<FieldGroup>,
subgroup: &Option<FieldGroup>,
export_tokens: &mut Vec<TokenStream>,
class_name: &Ident,
) {
Comment on lines +227 to +232
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should have docs, the "maybe" isn't obvious. What does it depend on?

Also, "register_groups" seems inaccurate -- this doesn't register anything, it just collects to tokens...

export_tokens.push(make_group_registration(
group,
ident("register_group"),
class_name,
));
export_tokens.push(make_group_registration(
subgroup,
ident("register_subgroup"),
class_name,
));
}

fn make_group_registration(
group: &Option<FieldGroup>,
register_fn: Ident,
class_name: &Ident,
) -> TokenStream {
let Some(FieldGroup { name, prefix }) = group else {
return TokenStream::new();
};

quote! {
::godot::register::private::#register_fn::<#class_name>(
#name,
#prefix
);
}
Comment on lines +254 to +259
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Formatting

}
44 changes: 21 additions & 23 deletions godot-macros/src/class/derive_godot_class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

use crate::class::data_models::fields::{named_fields, Fields};
use crate::class::data_models::group_export::FieldGroup;
use crate::class::{
make_property_impl, make_virtual_callback, BeforeKind, Field, FieldCond, FieldDefault,
FieldExport, FieldVar, Fields, SignatureInfo,
FieldExport, FieldVar, SignatureInfo,
};
use crate::util::{
bail, error, format_funcs_collection_struct, ident, path_ends_with_complex,
Expand All @@ -33,9 +35,10 @@ pub fn derive_godot_class(item: venial::Item) -> ParseResult<TokenStream> {
}

let mut modifiers = Vec::new();
let named_fields = named_fields(class)?;
let named_fields = named_fields(class, "#[derive(GodotClass)]")?;
let mut struct_cfg = parse_struct_attributes(class)?;
let mut fields = parse_fields(named_fields, struct_cfg.init_strategy)?;

if struct_cfg.is_editor_plugin() {
modifiers.push(quote! { with_editor_plugin })
}
Expand Down Expand Up @@ -559,25 +562,6 @@ fn parse_struct_attributes(class: &venial::Struct) -> ParseResult<ClassAttribute
})
}

/// Fetches data for all named fields for a struct.
///
/// Errors if `class` is a tuple struct.
fn named_fields(class: &venial::Struct) -> ParseResult<Vec<(venial::NamedField, Punct)>> {
// This is separate from parse_fields to improve compile errors. The errors from here demand larger and more non-local changes from the API
// user than those from parse_struct_attributes, so this must be run first.
match &class.fields {
// TODO disallow unit structs in the future
// It often happens that over time, a registered class starts to require a base field.
// Extending a {} struct requires breaking less code, so we should encourage it from the start.
venial::Fields::Unit => Ok(vec![]),
venial::Fields::Tuple(_) => bail!(
&class.fields,
"#[derive(GodotClass)] is not supported for tuple structs",
)?,
venial::Fields::Named(fields) => Ok(fields.fields.inner.clone()),
}
}

/// Returns field names and 1 base field, if available.
fn parse_fields(
named_fields: Vec<(venial::NamedField, Punct)>,
Expand Down Expand Up @@ -627,10 +611,10 @@ fn parse_fields(
}

// Deprecated #[init(default = expr)]
if let Some(default) = parser.handle_expr("default")? {
if let Some((key, default)) = parser.handle_expr_with_key("default")? {
if field.default_val.is_some() {
return bail!(
parser.span(),
key,
"Cannot use both `val` and `default` keys in #[init]; prefer using `val`"
);
Comment on lines -630 to 619
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is unrelated to the PR, right? Is it a span improvement?

Note that #[init(default)] will be removed in v0.4; others like #[init(val)] would benefit more from this. But if you plan bigger changes, maybe a separate PR is worth it 🤔

}
Expand Down Expand Up @@ -683,6 +667,20 @@ fn parse_fields(
parser.finish()?;
}

// #[export_group(name = ..., prefix = ...)]
if let Some(mut parser) = KvParser::parse(&named_field.attributes, "export_group")? {
let group = FieldGroup::new_from_kv(&mut parser)?;
field.group = Some(group);
parser.finish()?;
}

// #[export_subgroup(name = ..., prefix = ...)]
if let Some(mut parser) = KvParser::parse(&named_field.attributes, "export_subgroup")? {
let subgroup = FieldGroup::new_from_kv(&mut parser)?;
field.subgroup = Some(subgroup);
parser.finish()?;
}

// #[var]
if let Some(mut parser) = KvParser::parse(&named_field.attributes, "var")? {
let var = FieldVar::new_from_kv(&mut parser)?;
Expand Down
3 changes: 3 additions & 0 deletions godot-macros/src/class/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@
mod derive_godot_class;
mod godot_api;
mod godot_dyn;

mod data_models {
pub mod constant;
pub mod field;
pub mod field_export;
pub mod field_var;
pub mod fields;
pub mod func;
pub mod group_export;
pub mod inherent_impl;
pub mod interface_trait_impl;
pub mod property;
Expand Down
Loading
Loading