Skip to content

Commit 7102813

Browse files
committed
Generate accessor methods for indexed properties
Getters and setters are now generated for indexed properties that are naturally accessible in GDScript (i.e. do not have '/' in the name), like `SpatialMaterial::albedo_texture`. Doc generation for the underlying accessors has also been improved. Close #689
1 parent 58db05f commit 7102813

File tree

4 files changed

+195
-10
lines changed

4 files changed

+195
-10
lines changed

bindings-generator/src/class_docs.rs

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,15 +44,17 @@ impl GodotXmlDocs {
4444
.find(|node| node.tag_name().name() == "class")
4545
{
4646
if let Some(class_name) = class.attribute("name") {
47-
let methods_node = class
48-
.descendants()
49-
.find(|node| node.tag_name().name() == "methods");
50-
self.parse_methods(class_name, methods_node);
51-
47+
// Parse members first, so more general docs for indexed accessors can be used
48+
// if they exist.
5249
let members_node = class
5350
.descendants()
5451
.find(|node| node.tag_name().name() == "members");
5552
self.parse_members(class_name, members_node);
53+
54+
let methods_node = class
55+
.descendants()
56+
.find(|node| node.tag_name().name() == "methods");
57+
self.parse_methods(class_name, methods_node);
5658
}
5759
}
5860
}
@@ -62,6 +64,26 @@ impl GodotXmlDocs {
6264
for node in members.descendants() {
6365
if node.tag_name().name() == "member" {
6466
if let Some(desc) = node.text() {
67+
if let Some(property_name) = node.attribute("name") {
68+
if !property_name.contains('/') {
69+
if node.has_attribute("setter") {
70+
self.add_fn(
71+
class,
72+
&format!("set_{}", property_name),
73+
desc,
74+
&[],
75+
);
76+
}
77+
if node.has_attribute("getter") {
78+
self.add_fn(
79+
class,
80+
&format!("get_{}", property_name),
81+
desc,
82+
&[],
83+
);
84+
}
85+
}
86+
}
6587
if let Some(func) = node.attribute("setter") {
6688
self.add_fn(class, func, desc, &[]);
6789
}

bindings-generator/src/methods.rs

Lines changed: 133 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ use proc_macro2::TokenStream;
66
use quote::{format_ident, quote};
77

88
use std::collections::HashMap;
9-
use std::collections::HashSet;
109

1110
/// Types of icalls.
11+
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
1212
pub(crate) enum IcallType {
1313
Ptr,
1414
Varargs,
@@ -265,6 +265,14 @@ pub(crate) fn generate_methods(
265265
icalls: &mut HashMap<String, MethodSig>,
266266
docs: Option<&GodotXmlDocs>,
267267
) -> TokenStream {
268+
/// Memorized information about generated methods. Used to generate indexed property accessors.
269+
struct Generated {
270+
icall: proc_macro2::Ident,
271+
icall_ty: IcallType,
272+
maybe_unsafe: TokenStream,
273+
maybe_unsafe_reason: &'static str,
274+
}
275+
268276
// Brings values of some types to a type with less information.
269277
fn arg_erase(ty: &Ty, name: &proc_macro2::Ident) -> TokenStream {
270278
match ty {
@@ -280,11 +288,14 @@ pub(crate) fn generate_methods(
280288

281289
Ty::Object(_) => quote! { #name.as_arg_ptr() },
282290

291+
// Allow lossy casting of numeric types into similar primitives, see also to_return_post
292+
Ty::I64 | Ty::F64 | Ty::Bool => quote! { #name as _ },
293+
283294
_ => quote! { #name },
284295
}
285296
}
286297

287-
let mut method_set = HashSet::new();
298+
let mut generated = HashMap::new();
288299
let mut result = TokenStream::new();
289300

290301
for method in &class.methods {
@@ -301,11 +312,9 @@ pub(crate) fn generate_methods(
301312
let mut rust_ret_type = ret_type.to_rust();
302313

303314
// Ensure that methods are not injected several times.
304-
let method_name_string = method_name.to_string();
305-
if method_set.contains(&method_name_string) {
315+
if generated.contains_key(method_name) {
306316
continue;
307317
}
308-
method_set.insert(method_name_string);
309318

310319
let mut params_decl = TokenStream::new();
311320
let mut params_use = TokenStream::new();
@@ -387,7 +396,126 @@ pub(crate) fn generate_methods(
387396
}
388397
}
389398
};
399+
390400
result.extend(output);
401+
402+
generated.insert(
403+
method_name.to_string(),
404+
Generated {
405+
icall,
406+
icall_ty,
407+
maybe_unsafe,
408+
maybe_unsafe_reason,
409+
},
410+
);
411+
}
412+
413+
for property in &class.properties {
414+
if property.index < 0 || property.name.contains('/') {
415+
continue;
416+
}
417+
418+
let property_index = property.index;
419+
let ty = Ty::from_src(&property.type_);
420+
421+
if let Some(Generated {
422+
icall,
423+
icall_ty,
424+
maybe_unsafe,
425+
maybe_unsafe_reason,
426+
}) = generated.get(&property.getter)
427+
{
428+
let rusty_name = rust_safe_name(&property.name);
429+
let rust_ret_type = ty.to_rust();
430+
431+
let method_bind_fetch = {
432+
let method_table = format_ident!("{}MethodTable", class.name);
433+
let rust_method_name = format_ident!("{}", property.getter);
434+
435+
quote! {
436+
let method_bind: *mut sys::godot_method_bind = #method_table::get(get_api()).#rust_method_name;
437+
}
438+
};
439+
440+
let doc_comment = docs
441+
.and_then(|docs| {
442+
docs.get_class_method_desc(
443+
class.name.as_str(),
444+
&format!("get_{}", property.name),
445+
)
446+
})
447+
.unwrap_or("");
448+
449+
let recover = ret_recover(&ty, *icall_ty);
450+
451+
let output = quote! {
452+
#[doc = #doc_comment]
453+
#[doc = #maybe_unsafe_reason]
454+
#[inline]
455+
pub #maybe_unsafe fn #rusty_name(&self) -> #rust_ret_type {
456+
unsafe {
457+
#method_bind_fetch
458+
459+
let ret = crate::icalls::#icall(method_bind, self.this.sys().as_ptr(), #property_index);
460+
461+
#recover
462+
}
463+
}
464+
};
465+
466+
result.extend(output);
467+
}
468+
469+
if let Some(Generated {
470+
icall,
471+
icall_ty,
472+
maybe_unsafe,
473+
maybe_unsafe_reason,
474+
}) = generated.get(&property.setter)
475+
{
476+
let rusty_name = rust_safe_name(&format!("set_{}", property.name));
477+
478+
let rust_arg_ty = ty.to_rust_arg();
479+
let arg_ident = format_ident!("value");
480+
let arg_erased = arg_erase(&ty, &arg_ident);
481+
482+
let method_bind_fetch = {
483+
let method_table = format_ident!("{}MethodTable", class.name);
484+
let rust_method_name = format_ident!("{}", property.setter);
485+
486+
quote! {
487+
let method_bind: *mut sys::godot_method_bind = #method_table::get(get_api()).#rust_method_name;
488+
}
489+
};
490+
491+
let doc_comment = docs
492+
.and_then(|docs| {
493+
docs.get_class_method_desc(
494+
class.name.as_str(),
495+
&format!("set_{}", property.name),
496+
)
497+
})
498+
.unwrap_or("");
499+
500+
let recover = ret_recover(&Ty::Void, *icall_ty);
501+
502+
let output = quote! {
503+
#[doc = #doc_comment]
504+
#[doc = #maybe_unsafe_reason]
505+
#[inline]
506+
pub #maybe_unsafe fn #rusty_name(&self, #arg_ident: #rust_arg_ty) {
507+
unsafe {
508+
#method_bind_fetch
509+
510+
let ret = crate::icalls::#icall(method_bind, self.this.sys().as_ptr(), #property_index, #arg_erased);
511+
512+
#recover
513+
}
514+
}
515+
};
516+
517+
result.extend(output);
518+
}
391519
}
392520

393521
result

test/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ mod test_async;
99
mod test_constructor;
1010
mod test_derive;
1111
mod test_free_ub;
12+
mod test_indexed_props;
1213
mod test_map_owned;
1314
mod test_register;
1415
mod test_return_leak;
@@ -76,6 +77,7 @@ pub extern "C" fn run_tests(
7677
status &= test_constructor::run_tests();
7778
status &= test_derive::run_tests();
7879
status &= test_free_ub::run_tests();
80+
status &= test_indexed_props::run_tests();
7981
status &= test_map_owned::run_tests();
8082
status &= test_register::run_tests();
8183
status &= test_return_leak::run_tests();
@@ -238,6 +240,7 @@ fn init(handle: InitHandle) {
238240
test_constructor::register(handle);
239241
test_derive::register(handle);
240242
test_free_ub::register(handle);
243+
test_indexed_props::register(handle);
241244
test_map_owned::register(handle);
242245
test_register::register(handle);
243246
test_return_leak::register(handle);

test/src/test_indexed_props.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
use gdnative::core_types::Margin;
2+
use gdnative::prelude::*;
3+
4+
pub(crate) fn run_tests() -> bool {
5+
let mut status = true;
6+
7+
status &= test_indexed_props();
8+
9+
status
10+
}
11+
12+
pub(crate) fn register(_handle: InitHandle) {}
13+
14+
crate::godot_itest! { test_indexed_props {
15+
let control = Control::new();
16+
17+
assert_eq!(0, control.margin_top());
18+
assert_eq!(0, control.margin_left());
19+
20+
control.set_margin_top(42);
21+
22+
assert_eq!(42, control.margin_top());
23+
assert_eq!(42, control.margin(Margin::Top.into()) as i64);
24+
assert_eq!(0, control.margin_left());
25+
assert_eq!(0, control.margin(Margin::Left.into()) as i64);
26+
27+
control.set_margin(Margin::Left.into(), 24.0);
28+
29+
assert_eq!(24, control.margin_left());
30+
31+
control.free();
32+
}}

0 commit comments

Comments
 (0)