Description
I haven't been using this library for long, but it seems common in Rust code to connect symbols from GodotClass to signals of other script classes. For this, I end up creating StringName FFI instances to specify the identifiers for these symbols. To make this process more convenient, I wrote a macro.
#[macro_export]
macro_rules! string_name {
($base:ty, $ident:ident) => {{
// Just ensure ident exist
let str = $crate::string_name!(#[str], $ident);
let _ = <$base>::$ident; // Generates compile-time error
$crate::string_name!(#[set], str)
}};
($name:literal) => {{
$crate::string_name!(#[set], concat!($name, "\0").as_bytes())
}};
(#[str], $id:ident) => {
concat!(stringify!($ident), "\0").as_bytes()
};
(#[set], $str:expr) => {
// XXX: Optimization
// - Is it cheap enough to create everytime?
// - Otherwise, is it safe to store in OnceLock?
godot::prelude::StringName::from_latin1_with_nul($str)
};
}
In particular, I often call registered methods declared within the #[godot_api] block of Rust's GodotClass. The following usage simplifies this process and helps prevent human errors such as typos.
// BEFORE
timer.connect(
"timeout".into(),
Callable::from_object_method(
&self.to_gd(),
"__internal_try_start_frame_inner".into(),
),
);
// THEN
timer.connect(
string_name!("timeout"), // Godot side signal name; no way to find compile-time symbol!
Callable::from_object_method(
&self.to_gd(),
string_name!(Self, __internal_try_start_frame_inner),
// ^^^^ `$base:ty` takes this
// + IDE support, compile time error, ...
),
);
I think it would be beneficial for this type of API to be added to the library, especially if the #[godot_api] generator could also create type tags for methods connected to scripts. This would prevent accidentally creating symbol names for non-script methods with the string_name macro.
Here's a simple example of a compile-time verification macro that is partially supported by proc-macro.
#[godot_api]
impl MyClass {
#[func]
fn script_method() {}
}
impl MyClass {
fn non_script_method() {}
}
// -- generated --
#[doc(hidden)]
struct __ScriptSymbols_MyClass {
script_method: &'static str,
}
impl MyClass {
#[doc(hidden)]
const __SYMBOLS: __ScriptSymbols_MyClass {
script_method: "script_method", // Later, it'll be able to handle aliases
};
}
// -- usage
// let name: StringName = symbol_name!(MyClass, non_script_method);
// ^^^^^^^^^^^^^^^^^
// Compile Error: `non_script_method` not found in `__ScriptSymbols_MyClass`
let name: StringName = symbol_name!(MyClass, script_method);
// -- expands to
let name: StringName = {
// Check if symbol exist
let __sym = <$type> :: __SYMBOLS . $ident;
// Build identifier name.
StringName::from_latin1_with_nul(__sym.as_bytes())
};
I believe this approach would allow us to handle Rust bindings more safely than simply referencing symbols through strings.