Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 22 additions & 9 deletions godot-core/src/builtin/string/gstring.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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! {
Expand Down
23 changes: 23 additions & 0 deletions godot-core/src/builtin/string/string_name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
15 changes: 4 additions & 11 deletions itest/rust/src/builtin_tests/string/gstring_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<char> = 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');
Expand Down
24 changes: 24 additions & 0 deletions itest/rust/src/builtin_tests/string/string_name_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -177,6 +179,28 @@ 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(APPLE_STR);
assert_eq!(name.chars(), APPLE_CHARS);

// 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,
Expand Down
11 changes: 11 additions & 0 deletions itest/rust/src/builtin_tests/string/string_test_macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
(
Expand Down
Loading