Skip to content

Commit f2ebc7c

Browse files
committed
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.
1 parent 7b8f4e4 commit f2ebc7c

File tree

3 files changed

+75
-9
lines changed

3 files changed

+75
-9
lines changed

godot-core/src/builtin/string/gstring.rs

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -172,18 +172,31 @@ impl GString {
172172
pub fn chars(&self) -> &[char] {
173173
// SAFETY: Since 4.1, Godot ensures valid UTF-32, making interpreting as char slice safe.
174174
// See https://github.com/godotengine/godot/pull/74760.
175-
unsafe {
176-
let s = self.string_sys();
177-
let len = interface_fn!(string_to_utf32_chars)(s, std::ptr::null_mut(), 0);
178-
let ptr = interface_fn!(string_operator_index_const)(s, 0);
175+
let (ptr, len) = self.raw_slice();
179176

180-
// Even when len == 0, from_raw_parts requires ptr != null.
181-
if ptr.is_null() {
182-
return &[];
183-
}
177+
// Even when len == 0, from_raw_parts requires ptr != null.
178+
if ptr.is_null() {
179+
return &[];
180+
}
181+
182+
unsafe { std::slice::from_raw_parts(ptr, len) }
183+
}
184184

185-
std::slice::from_raw_parts(ptr as *const char, len as usize)
185+
/// Returns the raw pointer and length of the internal UTF-32 character array.
186+
///
187+
/// This is used by `StringName::chars()` in Godot 4.5+ where the buffer is shared via reference counting.
188+
/// Since Godot 4.1, the buffer contains valid UTF-32.
189+
pub(crate) fn raw_slice(&self) -> (*const char, usize) {
190+
let s = self.string_sys();
191+
192+
let len: sys::GDExtensionInt;
193+
let ptr: *const sys::char32_t;
194+
unsafe {
195+
len = interface_fn!(string_to_utf32_chars)(s, std::ptr::null_mut(), 0);
196+
ptr = interface_fn!(string_operator_index_const)(s, 0);
186197
}
198+
199+
(ptr.cast(), len as usize)
187200
}
188201

189202
ffi_methods! {

godot-core/src/builtin/string/string_name.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,29 @@ impl StringName {
179179
TransientStringNameOrd(self)
180180
}
181181

182+
/// Gets the UTF-32 character slice from a `StringName`.
183+
///
184+
/// # Compatibility
185+
/// This method is only available for Godot 4.5 and later, where `StringName` to `GString` conversions preserve the
186+
/// underlying buffer pointer via reference counting.
187+
#[cfg(since_api = "4.5")]
188+
pub fn chars(&self) -> &[char] {
189+
let gstring = GString::from(self);
190+
let (ptr, len) = gstring.raw_slice();
191+
192+
// Even when len == 0, from_raw_parts requires ptr != null.
193+
if ptr.is_null() {
194+
return &[];
195+
}
196+
197+
// SAFETY: In Godot 4.5+, StringName always uses String (GString) as backing storage internally, see
198+
// https://github.com/godotengine/godot/pull/104985.
199+
// The conversion preserves the original buffer pointer via reference counting. As long as the GString is not modified,
200+
// the buffer remains valid and is kept alive by the StringName's reference count, even after the temporary GString drops.
201+
// The returned slice's lifetime is tied to &self, which is correct since self keeps the buffer alive.
202+
unsafe { std::slice::from_raw_parts(ptr, len) }
203+
}
204+
182205
ffi_methods! {
183206
type sys::GDExtensionStringNamePtr = *mut Opaque;
184207

itest/rust/src/builtin_tests/string/string_name_test.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,36 @@ fn string_name_with_null() {
177177
}
178178
}
179179

180+
#[cfg(since_api = "4.5")]
181+
#[itest]
182+
fn string_name_chars() {
183+
// Empty string edge case (regression test similar to GString)
184+
let name = StringName::default();
185+
let empty_char_slice: &[char] = &[];
186+
assert_eq!(name.chars(), empty_char_slice);
187+
188+
// Unicode characters including emoji
189+
let name = StringName::from("ö🍎A💡");
190+
assert_eq!(
191+
name.chars(),
192+
&[
193+
char::from_u32(0x00F6).unwrap(), // ö
194+
char::from_u32(0x1F34E).unwrap(), // 🍎
195+
char::from(65), // A
196+
char::from_u32(0x1F4A1).unwrap(), // 💡
197+
]
198+
);
199+
200+
// Verify it matches GString::chars()
201+
let gstring = GString::from(&name);
202+
assert_eq!(name.chars(), gstring.chars());
203+
204+
// Verify multiple calls work correctly
205+
let chars1 = name.chars();
206+
let chars2 = name.chars();
207+
assert_eq!(chars1, chars2);
208+
}
209+
180210
// Byte and C-string conversions.
181211
crate::generate_string_bytes_and_cstr_tests!(
182212
builtin: StringName,

0 commit comments

Comments
 (0)