From f2ebc7c9959e58529418391c12e37cfbb79f0fe3 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Tue, 25 Nov 2025 23:32:22 +0100 Subject: [PATCH 1/2] Add `StringName::chars()` Safe because StringName -> GString conversion preserves internal buffer (ref-counting), and getting the char pointer through GString will return underlying StringName storage. --- godot-core/src/builtin/string/gstring.rs | 31 +++++++++++++------ godot-core/src/builtin/string/string_name.rs | 23 ++++++++++++++ .../builtin_tests/string/string_name_test.rs | 30 ++++++++++++++++++ 3 files changed, 75 insertions(+), 9 deletions(-) diff --git a/godot-core/src/builtin/string/gstring.rs b/godot-core/src/builtin/string/gstring.rs index b001d4ce2..1585b7e6c 100644 --- a/godot-core/src/builtin/string/gstring.rs +++ b/godot-core/src/builtin/string/gstring.rs @@ -172,18 +172,31 @@ impl GString { pub fn chars(&self) -> &[char] { // SAFETY: Since 4.1, Godot ensures valid UTF-32, making interpreting as char slice safe. // See https://github.com/godotengine/godot/pull/74760. - unsafe { - let s = self.string_sys(); - let len = interface_fn!(string_to_utf32_chars)(s, std::ptr::null_mut(), 0); - let ptr = interface_fn!(string_operator_index_const)(s, 0); + let (ptr, len) = self.raw_slice(); - // Even when len == 0, from_raw_parts requires ptr != null. - if ptr.is_null() { - return &[]; - } + // Even when len == 0, from_raw_parts requires ptr != null. + if ptr.is_null() { + return &[]; + } + + unsafe { std::slice::from_raw_parts(ptr, len) } + } - std::slice::from_raw_parts(ptr as *const char, len as usize) + /// Returns the raw pointer and length of the internal UTF-32 character array. + /// + /// This is used by `StringName::chars()` in Godot 4.5+ where the buffer is shared via reference counting. + /// Since Godot 4.1, the buffer contains valid UTF-32. + pub(crate) fn raw_slice(&self) -> (*const char, usize) { + let s = self.string_sys(); + + let len: sys::GDExtensionInt; + let ptr: *const sys::char32_t; + unsafe { + len = interface_fn!(string_to_utf32_chars)(s, std::ptr::null_mut(), 0); + ptr = interface_fn!(string_operator_index_const)(s, 0); } + + (ptr.cast(), len as usize) } ffi_methods! { diff --git a/godot-core/src/builtin/string/string_name.rs b/godot-core/src/builtin/string/string_name.rs index 418c29a69..107b49e29 100644 --- a/godot-core/src/builtin/string/string_name.rs +++ b/godot-core/src/builtin/string/string_name.rs @@ -179,6 +179,29 @@ impl StringName { TransientStringNameOrd(self) } + /// Gets the UTF-32 character slice from a `StringName`. + /// + /// # Compatibility + /// This method is only available for Godot 4.5 and later, where `StringName` to `GString` conversions preserve the + /// underlying buffer pointer via reference counting. + #[cfg(since_api = "4.5")] + pub fn chars(&self) -> &[char] { + let gstring = GString::from(self); + let (ptr, len) = gstring.raw_slice(); + + // Even when len == 0, from_raw_parts requires ptr != null. + if ptr.is_null() { + return &[]; + } + + // SAFETY: In Godot 4.5+, StringName always uses String (GString) as backing storage internally, see + // https://github.com/godotengine/godot/pull/104985. + // The conversion preserves the original buffer pointer via reference counting. As long as the GString is not modified, + // the buffer remains valid and is kept alive by the StringName's reference count, even after the temporary GString drops. + // The returned slice's lifetime is tied to &self, which is correct since self keeps the buffer alive. + unsafe { std::slice::from_raw_parts(ptr, len) } + } + ffi_methods! { type sys::GDExtensionStringNamePtr = *mut Opaque; diff --git a/itest/rust/src/builtin_tests/string/string_name_test.rs b/itest/rust/src/builtin_tests/string/string_name_test.rs index 8678ea627..5e55f78d5 100644 --- a/itest/rust/src/builtin_tests/string/string_name_test.rs +++ b/itest/rust/src/builtin_tests/string/string_name_test.rs @@ -177,6 +177,36 @@ fn string_name_with_null() { } } +#[cfg(since_api = "4.5")] +#[itest] +fn string_name_chars() { + // Empty string edge case (regression test similar to GString) + let name = StringName::default(); + let empty_char_slice: &[char] = &[]; + assert_eq!(name.chars(), empty_char_slice); + + // Unicode characters including emoji + let name = StringName::from("â🍎AπŸ’‘"); + assert_eq!( + name.chars(), + &[ + char::from_u32(0x00F6).unwrap(), // ΓΆ + char::from_u32(0x1F34E).unwrap(), // 🍎 + char::from(65), // A + char::from_u32(0x1F4A1).unwrap(), // πŸ’‘ + ] + ); + + // Verify it matches GString::chars() + let gstring = GString::from(&name); + assert_eq!(name.chars(), gstring.chars()); + + // Verify multiple calls work correctly + let chars1 = name.chars(); + let chars2 = name.chars(); + assert_eq!(chars1, chars2); +} + // Byte and C-string conversions. crate::generate_string_bytes_and_cstr_tests!( builtin: StringName, From 0a0f564e865eb3c22a729503872cda26636d8623 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Tue, 25 Nov 2025 23:42:06 +0100 Subject: [PATCH 2/2] Minor code reuse in itest --- .../rust/src/builtin_tests/string/gstring_test.rs | 15 ++++----------- .../src/builtin_tests/string/string_name_test.rs | 14 ++++---------- .../builtin_tests/string/string_test_macros.rs | 11 +++++++++++ 3 files changed, 19 insertions(+), 21 deletions(-) diff --git a/itest/rust/src/builtin_tests/string/gstring_test.rs b/itest/rust/src/builtin_tests/string/gstring_test.rs index 2b7744fca..84610a8c5 100644 --- a/itest/rust/src/builtin_tests/string/gstring_test.rs +++ b/itest/rust/src/builtin_tests/string/gstring_test.rs @@ -9,6 +9,7 @@ use std::collections::HashSet; use godot::builtin::{Encoding, GString, PackedStringArray}; +use super::string_test_macros::{APPLE_CHARS, APPLE_STR}; use crate::framework::{expect_panic_or_nothing, itest}; // TODO use tests from godot-rust/gdnative @@ -68,27 +69,19 @@ fn string_chars() { assert_eq!(string.chars(), empty_char_slice); assert_eq!(string, GString::from(empty_char_slice)); - let string = String::from("â🍎AπŸ’‘"); + let string = String::from(APPLE_STR); let string_chars: Vec = string.chars().collect(); let gstring = GString::from(&string); assert_eq!(gstring.chars(), string_chars.as_slice()); - assert_eq!( - gstring.chars(), - &[ - char::from_u32(0x00F6).unwrap(), - char::from_u32(0x1F34E).unwrap(), - char::from(65), - char::from_u32(0x1F4A1).unwrap(), - ] - ); + assert_eq!(gstring.chars(), APPLE_CHARS); assert_eq!(gstring, GString::from(string_chars.as_slice())); } #[itest] fn string_unicode_at() { - let s = GString::from("â🍎AπŸ’‘"); + let s = GString::from(APPLE_STR); assert_eq!(s.unicode_at(0), 'ΓΆ'); assert_eq!(s.unicode_at(1), '🍎'); assert_eq!(s.unicode_at(2), 'A'); diff --git a/itest/rust/src/builtin_tests/string/string_name_test.rs b/itest/rust/src/builtin_tests/string/string_name_test.rs index 5e55f78d5..30b88d398 100644 --- a/itest/rust/src/builtin_tests/string/string_name_test.rs +++ b/itest/rust/src/builtin_tests/string/string_name_test.rs @@ -9,6 +9,8 @@ use std::collections::HashSet; use godot::builtin::{static_sname, Encoding, GString, NodePath, StringName}; +#[cfg(since_api = "4.5")] +use super::string_test_macros::{APPLE_CHARS, APPLE_STR}; use crate::framework::{assert_eq_self, itest}; #[itest] @@ -186,16 +188,8 @@ fn string_name_chars() { assert_eq!(name.chars(), empty_char_slice); // Unicode characters including emoji - let name = StringName::from("â🍎AπŸ’‘"); - assert_eq!( - name.chars(), - &[ - char::from_u32(0x00F6).unwrap(), // ΓΆ - char::from_u32(0x1F34E).unwrap(), // 🍎 - char::from(65), // A - char::from_u32(0x1F4A1).unwrap(), // πŸ’‘ - ] - ); + let name = StringName::from(APPLE_STR); + assert_eq!(name.chars(), APPLE_CHARS); // Verify it matches GString::chars() let gstring = GString::from(&name); diff --git a/itest/rust/src/builtin_tests/string/string_test_macros.rs b/itest/rust/src/builtin_tests/string/string_test_macros.rs index 38ba5412f..13f48ea7e 100644 --- a/itest/rust/src/builtin_tests/string/string_test_macros.rs +++ b/itest/rust/src/builtin_tests/string/string_test_macros.rs @@ -7,6 +7,17 @@ //! Byte and C-string conversions. +/// Test string containing Unicode and emoji characters. +pub(super) const APPLE_STR: &str = "â🍎AπŸ’‘"; + +/// Expected UTF-32 character array for `APPLE_STR`. +pub(super) const APPLE_CHARS: &[char] = &[ + '\u{00F6}', // ΓΆ + '\u{1F34E}', // 🍎 + 'A', + '\u{1F4A1}', // πŸ’‘ +]; + #[macro_export] macro_rules! generate_string_bytes_and_cstr_tests { (