Skip to content

Commit

Permalink
Merge #205
Browse files Browse the repository at this point in the history
205: Implement #[init(default = ...)] annotation r=Bromeon a=ttencate

Fixes #199

Co-authored-by: Thomas ten Cate <[email protected]>
  • Loading branch information
bors[bot] and ttencate authored Mar 27, 2023
2 parents c1c2d60 + 2a201f5 commit c6ebd3d
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 33 deletions.
80 changes: 48 additions & 32 deletions godot-macros/src/derive_godot_class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,10 @@ fn parse_struct_attributes(class: &Struct) -> ParseResult<ClassAttributes> {

/// Returns field names and 1 base field, if available
fn parse_fields(class: &Struct) -> ParseResult<Fields> {
let mut all_field_names = vec![];
let mut exported_fields = vec![];
let mut all_fields = vec![];
let mut base_field = Option::<Field>::None;

let fields: Vec<(NamedField, Punct)> = match &class.fields {
let named_fields: Vec<(NamedField, Punct)> = match &class.fields {
StructFields::Unit => {
vec![]
}
Expand All @@ -105,42 +104,50 @@ fn parse_fields(class: &Struct) -> ParseResult<Fields> {
};

// Attributes on struct fields
for (field, _punct) in fields {
for (named_field, _punct) in named_fields {
let mut is_base = false;
let mut field = Field::new(&named_field);

// #[base]
if let Some(parser) = KvParser::parse(&field.attributes, "base")? {
if let Some(prev_base) = base_field {
if let Some(parser) = KvParser::parse(&named_field.attributes, "base")? {
if let Some(prev_base) = base_field.as_ref() {
bail(
format!(
"#[base] allowed for at most 1 field, already applied to '{}'",
"#[base] allowed for at most 1 field, already applied to `{}`",
prev_base.name
),
parser.span(),
)?;
}
is_base = true;
base_field = Some(Field::new(&field));
parser.finish()?;
}

// #[init]
if let Some(mut parser) = KvParser::parse(&named_field.attributes, "init")? {
let default = parser.handle_expr("default")?;
field.default = default;
parser.finish()?;
}

// #[export]
if let Some(mut parser) = KvParser::parse(&field.attributes, "export")? {
let exported_field = ExportedField::new_from_kv(Field::new(&field), &mut parser)?;
exported_fields.push(exported_field);
if let Some(mut parser) = KvParser::parse(&named_field.attributes, "export")? {
let export = FieldExport::new_from_kv(&mut parser)?;
field.export = Some(export);
parser.finish()?;
}

// Exported or Rust-only fields
if !is_base {
all_field_names.push(field.name.clone())
if is_base {
base_field = Some(field);
} else {
all_fields.push(field);
}
}

Ok(Fields {
all_field_names,
all_fields,
base_field,
exported_fields,
})
}

Expand All @@ -153,27 +160,31 @@ struct ClassAttributes {
}

struct Fields {
all_field_names: Vec<Ident>,
/// All fields except `base_field`.
all_fields: Vec<Field>,
/// The field annotated with `#[base]`.
base_field: Option<Field>,
exported_fields: Vec<ExportedField>,
}

struct Field {
name: Ident,
ty: TyExpr,
default: Option<TokenStream>,
export: Option<FieldExport>,
}

impl Field {
fn new(field: &NamedField) -> Self {
Self {
name: field.name.clone(),
ty: field.ty.clone(),
default: None,
export: None,
}
}
}

struct ExportedField {
field: Field,
struct FieldExport {
getter: GetterSetter,
setter: GetterSetter,
hint: Option<ExportHint>,
Expand Down Expand Up @@ -219,8 +230,8 @@ impl ExportHint {
}
}

impl ExportedField {
pub fn new_from_kv(field: Field, parser: &mut KvParser) -> ParseResult<ExportedField> {
impl FieldExport {
pub fn new_from_kv(parser: &mut KvParser) -> ParseResult<FieldExport> {
let mut getter = GetterSetter::parse(parser, "get")?;
let mut setter = GetterSetter::parse(parser, "set")?;
if getter == GetterSetter::Omitted && setter == GetterSetter::Omitted {
Expand All @@ -238,8 +249,7 @@ impl ExportedField {
})
.transpose()?;

Ok(ExportedField {
field,
Ok(FieldExport {
getter,
setter,
hint,
Expand All @@ -254,8 +264,13 @@ fn make_godot_init_impl(class_name: &Ident, fields: Fields) -> TokenStream {
TokenStream::new()
};

let rest_init = fields.all_field_names.into_iter().map(|field| {
quote! { #field: std::default::Default::default(), }
let rest_init = fields.all_fields.into_iter().map(|field| {
let field_name = field.name;
let value_expr = match field.default {
None => quote!(::std::default::Default::default()),
Some(default) => default,
};
quote! { #field_name: #value_expr, }
});

quote! {
Expand Down Expand Up @@ -295,20 +310,21 @@ fn make_deref_impl(class_name: &Ident, fields: &Fields) -> TokenStream {

fn make_exports_impl(class_name: &Ident, fields: &Fields) -> TokenStream {
let mut getter_setter_impls = Vec::new();
let mut export_tokens = Vec::with_capacity(fields.exported_fields.len());
let mut export_tokens = Vec::new();

for exported_field in &fields.exported_fields {
let field_name = exported_field.field.name.to_string();
for field in &fields.all_fields {
let Some(export) = &field.export else { continue; };
let field_name = field.name.to_string();
let field_ident = ident(&field_name);
let field_type = exported_field.field.ty.clone();
let field_type = field.ty.clone();

let ExportHint {
hint_type,
description,
} = exported_field.hint.clone().unwrap_or_else(ExportHint::none);
} = export.hint.clone().unwrap_or_else(ExportHint::none);

let getter_name;
match &exported_field.getter {
match &export.getter {
GetterSetter::Omitted => {
getter_name = "".to_owned();
}
Expand All @@ -334,7 +350,7 @@ fn make_exports_impl(class_name: &Ident, fields: &Fields) -> TokenStream {
}

let setter_name;
match &exported_field.setter {
match &export.setter {
GetterSetter::Omitted => {
setter_name = "".to_owned();
}
Expand Down
34 changes: 33 additions & 1 deletion godot-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,38 @@ mod util;
/// }
/// ```
///
/// The generated `init` function will initialize each struct field (except the field annotated
/// with `#[base]`, if any) using `Default::default()`. To assign some other value, annotate the
/// field with `#[init(default = ...)]`:
///
/// ```
/// # use godot_macros::GodotClass;
/// #[derive(GodotClass)]
/// #[class(init)]
/// struct MyStruct {
/// #[init(default = 42)]
/// my_field: i64
/// }
/// ```
///
/// The given value can be any Rust expression that can be evaluated in the scope where you write
/// the attribute. However, due to limitations in the parser, some complex expressions must be
/// surrounded by parentheses. This is the case if the expression includes a `,` that is _not_
/// inside any pair of `(...)`, `[...]` or `{...}` (even if it is, for example, inside `<...>` or
/// `|...|`). A contrived example:
///
/// ```
/// # use godot_macros::GodotClass;
/// # use std::collections::HashMap;
/// # #[derive(GodotClass)]
/// # #[class(init)]
/// # struct MyStruct {
/// #[init(default = (HashMap::<i64, i64>::new()))]
/// // ^ parentheses needed due to this comma
/// # my_field: HashMap<i64, i64>,
/// # }
/// ```
///
/// # Inheritance
///
/// Unlike C++, Rust doesn't really have inheritance, but the GDExtension API lets us "inherit"
Expand Down Expand Up @@ -223,7 +255,7 @@ mod util;
///
/// The `#[signal]` attribute is accepted, but not yet implemented. See [issue
/// #8](https://github.com/godot-rust/gdext/issues/8).
#[proc_macro_derive(GodotClass, attributes(class, export, base, signal))]
#[proc_macro_derive(GodotClass, attributes(class, base, export, init, signal))]
pub fn derive_native_class(input: TokenStream) -> TokenStream {
translate(input, derive_godot_class::transform)
}
Expand Down
7 changes: 7 additions & 0 deletions itest/godot/ManualFfiTests.gd
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ func test_missing_init():

print("[GD] WithoutInit is: ", instance)

func test_init_defaults():
var obj = WithInitDefaults.new()

assert_eq(obj.default_int, 0)
assert_eq(obj.literal_int, 42)
assert_eq(obj.expr_int, -42)

func test_to_string():
var ffi = VirtualMethodTest.new()

Expand Down
26 changes: 26 additions & 0 deletions itest/rust/src/init_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* 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::prelude::*;

#[derive(GodotClass)]
#[class(init)]
struct WithInitDefaults {
#[export(get)]
default_int: i64,

#[export(get)]
#[init(default = 42)]
literal_int: i64,

#[export(get)]
#[init(default = -42)]
expr_int: i64,
}

// TODO Remove once https://github.com/godot-rust/gdext/issues/187 is fixed
#[godot_api]
impl WithInitDefaults {}
1 change: 1 addition & 0 deletions itest/rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ mod dictionary_test;
mod enum_test;
mod export_test;
mod gdscript_ffi_test;
mod init_test;
mod node_test;
mod object_test;
mod packed_array_test;
Expand Down

0 comments on commit c6ebd3d

Please sign in to comment.