Skip to content

Refer to #[func] names in type-safe ways (not just strings) #620

Closed
@kang-sw

Description

@kang-sw

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    c: coreCore componentsfeatureAdds functionality to the library

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions