Skip to content

Commit 0932417

Browse files
authored
Merge pull request #427 from godot-rust/feature/direct-stringname-ctor
Add `StringName::from_latin1_with_nul()`, direct construction in `String::from(&str)`
2 parents a7402e8 + 6ad9754 commit 0932417

File tree

7 files changed

+121
-20
lines changed

7 files changed

+121
-20
lines changed

godot-codegen/src/util.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,13 +181,20 @@ pub(crate) fn make_utility_function_ptr_name(function: &UtilityFunction) -> Iden
181181
safe_ident(&function.name)
182182
}
183183

184-
// TODO should eventually be removed, as all StringNames are cached
184+
#[cfg(since_api = "4.2")]
185185
pub fn make_string_name(identifier: &str) -> TokenStream {
186+
let lit = proc_macro2::Literal::byte_string(format!("{identifier}\0").as_bytes());
186187
quote! {
187-
StringName::from(#identifier)
188+
StringName::from_latin1_with_nul(#lit)
188189
}
189190
}
190191

192+
#[cfg(before_api = "4.2")]
193+
pub fn make_string_name(identifier: &str) -> TokenStream {
194+
quote! {
195+
StringName::from(#identifier)
196+
}
197+
}
191198
pub fn make_sname_ptr(identifier: &str) -> TokenStream {
192199
quote! {
193200
string_names.fetch(#identifier)

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

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,6 @@
88

99
macro_rules! impl_rust_string_conv {
1010
($Ty:ty) => {
11-
impl<S> From<S> for $Ty
12-
where
13-
S: AsRef<str>,
14-
{
15-
fn from(string: S) -> Self {
16-
let intermediate = GodotString::from(string.as_ref());
17-
Self::from(&intermediate)
18-
}
19-
}
20-
2111
impl From<&$Ty> for String {
2212
fn from(string: &$Ty) -> Self {
2313
let intermediate = GodotString::from(string);

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,16 @@ impl fmt::Debug for NodePath {
9898

9999
impl_rust_string_conv!(NodePath);
100100

101+
impl<S> From<S> for NodePath
102+
where
103+
S: AsRef<str>,
104+
{
105+
fn from(string: S) -> Self {
106+
let intermediate = GodotString::from(string.as_ref());
107+
Self::from(&intermediate)
108+
}
109+
}
110+
101111
impl From<&GodotString> for NodePath {
102112
fn from(string: &GodotString) -> Self {
103113
unsafe {

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

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,10 @@
77
use godot_ffi as sys;
88
use sys::{ffi_methods, GodotFfi};
99

10-
use std::fmt;
11-
1210
use crate::builtin::inner;
11+
use crate::builtin::{GodotString, NodePath};
1312

14-
use super::{GodotString, NodePath};
13+
use std::fmt;
1514

1615
/// A string optimized for unique names.
1716
///
@@ -27,6 +26,43 @@ impl StringName {
2726
Self { opaque }
2827
}
2928

29+
/// Creates a `StringName` from a static, nul-terminated ASCII/Latin-1 `b"string"` literal.
30+
///
31+
/// Avoids unnecessary copies and allocations and directly uses the backing buffer. Useful for literals.
32+
///
33+
/// # Example
34+
/// ```no_run
35+
/// use godot::builtin::StringName;
36+
///
37+
/// let sname = StringName::from_latin1_with_nul(b"± Latin-1 string\0");
38+
/// ```
39+
///
40+
/// # Panics
41+
/// When the string is not nul-terminated or contains interior nul bytes.
42+
///
43+
/// Note that every byte is valid in Latin-1, so there is no encoding validation being performed.
44+
#[cfg(since_api = "4.2")]
45+
pub fn from_latin1_with_nul(latin1_c_str: &'static [u8]) -> Self {
46+
let c_str = std::ffi::CStr::from_bytes_with_nul(latin1_c_str)
47+
.unwrap_or_else(|_| panic!("invalid or not nul-terminated CStr: '{latin1_c_str:?}'"));
48+
49+
let result = unsafe {
50+
Self::from_string_sys_init(|ptr| {
51+
sys::interface_fn!(string_name_new_with_latin1_chars)(
52+
ptr,
53+
c_str.as_ptr(),
54+
true as sys::GDExtensionBool, // p_is_static
55+
)
56+
})
57+
};
58+
59+
// StringName expects that the destructor is not invoked on static instances (or only at global exit; see SNAME(..) macro in Godot).
60+
// According to testing with godot4 --verbose, there is no mention of "Orphan StringName" at shutdown when incrementing the ref-count,
61+
// so this should not leak memory.
62+
result.inc_ref();
63+
result
64+
}
65+
3066
/// Returns the number of characters in the string.
3167
///
3268
/// _Godot equivalent: `length`_
@@ -63,6 +99,11 @@ impl StringName {
6399
pub fn as_inner(&self) -> inner::InnerStringName {
64100
inner::InnerStringName::from_outer(self)
65101
}
102+
103+
/// Increment ref-count. This may leak memory if used wrongly.
104+
fn inc_ref(&self) {
105+
std::mem::forget(self.clone());
106+
}
66107
}
67108

68109
// SAFETY:
@@ -84,7 +125,7 @@ unsafe impl GodotFfi for StringName {
84125

85126
unsafe fn from_arg_ptr(ptr: sys::GDExtensionTypePtr, _call_type: sys::PtrcallType) -> Self {
86127
let string_name = Self::from_sys(ptr);
87-
std::mem::forget(string_name.clone());
128+
string_name.inc_ref();
88129
string_name
89130
}
90131

@@ -134,6 +175,33 @@ unsafe impl Send for StringName {}
134175

135176
impl_rust_string_conv!(StringName);
136177

178+
impl<S> From<S> for StringName
179+
where
180+
S: AsRef<str>,
181+
{
182+
#[cfg(before_api = "4.2")]
183+
fn from(string: S) -> Self {
184+
let intermediate = GodotString::from(string.as_ref());
185+
Self::from(&intermediate)
186+
}
187+
188+
#[cfg(since_api = "4.2")]
189+
fn from(string: S) -> Self {
190+
let utf8 = string.as_ref().as_bytes();
191+
192+
// SAFETY: Rust guarantees validity and range of string.
193+
unsafe {
194+
Self::from_string_sys_init(|ptr| {
195+
sys::interface_fn!(string_name_new_with_utf8_chars_and_len)(
196+
ptr,
197+
utf8.as_ptr() as *const std::ffi::c_char,
198+
utf8.len() as i64,
199+
);
200+
})
201+
}
202+
}
203+
}
204+
137205
impl From<&GodotString> for StringName {
138206
fn from(string: &GodotString) -> Self {
139207
unsafe {

godot-core/src/log.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ macro_rules! inner_godot_msg {
1212
//($($args:tt),* $(,)?) => {
1313
unsafe {
1414
let msg = format!("{}\0", format_args!($fmt $(, $args)*));
15-
assert!(msg.is_ascii(), "godot_error: message must be ASCII");
15+
// assert!(msg.is_ascii(), "godot_error: message must be ASCII");
1616

1717
// Check whether engine is loaded, otherwise fall back to stderr.
1818
if $crate::sys::is_initialized() {

godot-ffi/src/toolbox.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,10 +123,9 @@ pub fn c_str(s: &[u8]) -> *const std::ffi::c_char {
123123
s.as_ptr() as *const std::ffi::c_char
124124
}
125125

126+
/// Returns a C `const char*` for a null-terminated string slice. UTF-8 encoded.
126127
#[inline]
127128
pub fn c_str_from_str(s: &str) -> *const std::ffi::c_char {
128-
debug_assert!(s.is_ascii());
129-
130129
c_str(s.as_bytes())
131130
}
132131

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

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ fn string_name_default() {
1919

2020
#[itest]
2121
fn string_name_conversion() {
22+
// Note: StringName::from(&str) uses direct FFI constructor from Godot 4.2 onwards.
23+
2224
let string = GodotString::from("some string");
2325
let name = StringName::from(&string);
2426
let back = GodotString::from(&name);
@@ -57,7 +59,7 @@ fn string_name_equality() {
5759

5860
// TODO: add back in when ordering StringNames is fixed
5961
#[itest(skip)]
60-
fn string_ordering() {
62+
fn string_name_ordering() {
6163
let _low = StringName::from("Alpha");
6264
let _high = StringName::from("Beta");
6365
/*
@@ -109,3 +111,28 @@ fn string_name_is_empty() {
109111
let empty = StringName::default();
110112
assert!(empty.is_empty());
111113
}
114+
115+
#[itest]
116+
#[cfg(since_api = "4.2")]
117+
fn string_name_from_latin1_with_nul() {
118+
let cases: [(&[u8], &str); 3] = [
119+
(b"pure ASCII\t[~]\0", "pure ASCII\t[~]\0"),
120+
(b"\xB1\0", "±"),
121+
(b"Latin-1 \xA3 \xB1 text \xBE\0", "Latin-1 £ ± text ¾"),
122+
];
123+
124+
for (bytes, string) in cases.into_iter() {
125+
let a = StringName::from_latin1_with_nul(bytes);
126+
let b = StringName::from(string);
127+
128+
println!();
129+
println!(
130+
"Arrays: a={:?}, b={:?}",
131+
a.to_string().as_bytes(),
132+
b.to_string().as_bytes()
133+
);
134+
println!("Hashes: a={:?}, b={:?}", a.hash(), b.hash());
135+
println!("Lengths: a={}, b={}", a.len(), b.len());
136+
assert_eq!(a, b);
137+
}
138+
}

0 commit comments

Comments
 (0)