Skip to content

Commit 3416265

Browse files
authored
Merge pull request #1419 from godot-rust/feature/stringname-chars
Add `StringName::chars()`
2 parents d54797c + 0a0f564 commit 3416265

File tree

5 files changed

+84
-20
lines changed

5 files changed

+84
-20
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/gstring_test.rs

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use std::collections::HashSet;
99

1010
use godot::builtin::{Encoding, GString, PackedStringArray};
1111

12+
use super::string_test_macros::{APPLE_CHARS, APPLE_STR};
1213
use crate::framework::{expect_panic_or_nothing, itest};
1314

1415
// TODO use tests from godot-rust/gdnative
@@ -68,27 +69,19 @@ fn string_chars() {
6869
assert_eq!(string.chars(), empty_char_slice);
6970
assert_eq!(string, GString::from(empty_char_slice));
7071

71-
let string = String::from("ö🍎A💡");
72+
let string = String::from(APPLE_STR);
7273
let string_chars: Vec<char> = string.chars().collect();
7374
let gstring = GString::from(&string);
7475

7576
assert_eq!(gstring.chars(), string_chars.as_slice());
76-
assert_eq!(
77-
gstring.chars(),
78-
&[
79-
char::from_u32(0x00F6).unwrap(),
80-
char::from_u32(0x1F34E).unwrap(),
81-
char::from(65),
82-
char::from_u32(0x1F4A1).unwrap(),
83-
]
84-
);
77+
assert_eq!(gstring.chars(), APPLE_CHARS);
8578

8679
assert_eq!(gstring, GString::from(string_chars.as_slice()));
8780
}
8881

8982
#[itest]
9083
fn string_unicode_at() {
91-
let s = GString::from("ö🍎A💡");
84+
let s = GString::from(APPLE_STR);
9285
assert_eq!(s.unicode_at(0), 'ö');
9386
assert_eq!(s.unicode_at(1), '🍎');
9487
assert_eq!(s.unicode_at(2), 'A');

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ use std::collections::HashSet;
99

1010
use godot::builtin::{static_sname, Encoding, GString, NodePath, StringName};
1111

12+
#[cfg(since_api = "4.5")]
13+
use super::string_test_macros::{APPLE_CHARS, APPLE_STR};
1214
use crate::framework::{assert_eq_self, itest};
1315

1416
#[itest]
@@ -177,6 +179,28 @@ fn string_name_with_null() {
177179
}
178180
}
179181

182+
#[cfg(since_api = "4.5")]
183+
#[itest]
184+
fn string_name_chars() {
185+
// Empty string edge case (regression test similar to GString)
186+
let name = StringName::default();
187+
let empty_char_slice: &[char] = &[];
188+
assert_eq!(name.chars(), empty_char_slice);
189+
190+
// Unicode characters including emoji
191+
let name = StringName::from(APPLE_STR);
192+
assert_eq!(name.chars(), APPLE_CHARS);
193+
194+
// Verify it matches GString::chars()
195+
let gstring = GString::from(&name);
196+
assert_eq!(name.chars(), gstring.chars());
197+
198+
// Verify multiple calls work correctly
199+
let chars1 = name.chars();
200+
let chars2 = name.chars();
201+
assert_eq!(chars1, chars2);
202+
}
203+
180204
// Byte and C-string conversions.
181205
crate::generate_string_bytes_and_cstr_tests!(
182206
builtin: StringName,

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,17 @@
77

88
//! Byte and C-string conversions.
99
10+
/// Test string containing Unicode and emoji characters.
11+
pub(super) const APPLE_STR: &str = "ö🍎A💡";
12+
13+
/// Expected UTF-32 character array for `APPLE_STR`.
14+
pub(super) const APPLE_CHARS: &[char] = &[
15+
'\u{00F6}', // ö
16+
'\u{1F34E}', // 🍎
17+
'A',
18+
'\u{1F4A1}', // 💡
19+
];
20+
1021
#[macro_export]
1122
macro_rules! generate_string_bytes_and_cstr_tests {
1223
(

0 commit comments

Comments
 (0)