napi/js_values/string/
utf16.rs

1use std::convert::TryFrom;
2#[cfg(feature = "napi10")]
3use std::ffi::c_void;
4use std::ops::Deref;
5
6use crate::{bindgen_runtime::ToNapiValue, sys, Error, JsString, Result, Status};
7
8#[cfg(feature = "napi10")]
9use crate::Env;
10
11pub struct JsStringUtf16<'env> {
12  pub(crate) inner: JsString<'env>,
13  pub(crate) buf: &'env [u16],
14  pub(crate) _inner_buf: Vec<u16>,
15}
16
17impl<'env> JsStringUtf16<'env> {
18  #[cfg(feature = "napi10")]
19  /// Try to create a new JavaScript utf16 string from a Rust `Vec<u16>` without copying the data
20  /// ## Behavior
21  ///
22  /// The `copied` parameter in the underlying `node_api_create_external_string_utf16` call
23  /// indicates whether the string data was copied into V8's heap rather than being used
24  /// as an external reference.
25  ///
26  /// ### When `copied` is `true`:
27  /// - String data is copied to V8's heap
28  /// - Finalizer is called immediately if provided
29  /// - Original buffer can be freed after the call
30  /// - Performance benefit of external strings is not achieved
31  ///
32  /// ### When `copied` is `false`:
33  /// - V8 creates an external string that references the original buffer without copying
34  /// - Original buffer must remain valid for the lifetime of the JS string
35  /// - Finalizer called when string is garbage collected
36  /// - Memory usage and copying overhead is reduced
37  ///
38  /// ## Common scenarios where `copied` is `true`:
39  /// - String is too short (typically < 10-15 characters)
40  /// - V8 heap is under memory pressure
41  /// - V8 is running with pointer compression or sandbox features
42  /// - Invalid UTF-16 encoding that requires sanitization
43  /// - Platform doesn't support external strings
44  /// - Memory alignment issues with the provided buffer
45  ///
46  /// The `copied` parameter serves as feedback to understand whether the external string
47  /// optimization was successful or if V8 fell back to traditional string creation.
48  pub fn from_data(env: &'env Env, data: Vec<u16>) -> Result<JsStringUtf16<'env>> {
49    use std::mem;
50    use std::ptr;
51
52    use crate::{check_status, Value, ValueType};
53
54    if data.is_empty() {
55      return Err(Error::new(
56        Status::InvalidArg,
57        "Cannot create external string from empty data".to_owned(),
58      ));
59    }
60
61    let mut raw_value = ptr::null_mut();
62    let mut copied = false;
63    let data_ptr = data.as_ptr();
64    let len = data.len();
65    let cap = data.capacity();
66    let finalize_hint = Box::into_raw(Box::new((len, cap)));
67
68    check_status!(
69      unsafe {
70        sys::node_api_create_external_string_utf16(
71          env.0,
72          data_ptr,
73          len as isize,
74          Some(drop_utf16_string),
75          finalize_hint.cast(),
76          &mut raw_value,
77          &mut copied,
78        )
79      },
80      "Failed to create external string utf16"
81    )?;
82
83    let inner_buf = if copied {
84      // If the data was copied, the finalizer won't be called
85      // We need to clean up the finalize_hint and let the Vec be dropped
86      unsafe {
87        let _ = Box::from_raw(finalize_hint);
88      };
89      data
90    } else {
91      // Only forget the data if it wasn't copied
92      // The finalizer will handle cleanup
93      mem::forget(data);
94      vec![]
95    };
96
97    Ok(Self {
98      inner: JsString(
99        Value {
100          env: env.0,
101          value: raw_value,
102          value_type: ValueType::String,
103        },
104        std::marker::PhantomData,
105      ),
106      buf: unsafe { std::slice::from_raw_parts(data_ptr.cast(), len) },
107      _inner_buf: inner_buf,
108    })
109  }
110
111  #[cfg(feature = "napi10")]
112  /// Creates an external UTF-16 string from raw data with a custom finalize callback.
113  ///
114  /// ## Safety
115  ///
116  /// The caller must ensure that:
117  /// - The data pointer is valid for the lifetime of the string
118  /// - The finalize callback properly cleans up the data
119  ///
120  /// ## Behavior
121  ///
122  /// The `copied` parameter in the underlying `node_api_create_external_string_utf16` call
123  /// indicates whether the string data was copied into V8's heap rather than being used
124  /// as an external reference.
125  ///
126  /// ### When `copied` is `true`:
127  /// - String data is copied to V8's heap
128  /// - Finalizer is called immediately if provided
129  /// - Original buffer can be freed after the call
130  /// - Performance benefit of external strings is not achieved
131  ///
132  /// ### When `copied` is `false`:
133  /// - V8 creates an external string that references the original buffer without copying
134  /// - Original buffer must remain valid for the lifetime of the JS string
135  /// - Finalizer called when string is garbage collected
136  /// - Memory usage and copying overhead is reduced
137  ///
138  /// ## Common scenarios where `copied` is `true`:
139  /// - String is too short (typically < 10-15 characters)
140  /// - V8 heap is under memory pressure
141  /// - V8 is running with pointer compression or sandbox features
142  /// - Invalid UTF-16 encoding that requires sanitization
143  /// - Platform doesn't support external strings
144  /// - Memory alignment issues with the provided buffer
145  ///
146  /// The `copied` parameter serves as feedback to understand whether the external string
147  /// optimization was successful or if V8 fell back to traditional string creation.
148  pub unsafe fn from_external<T: 'env, F: FnOnce(Env, T) + 'env>(
149    env: &'env Env,
150    data: *const u16,
151    len: usize,
152    finalize_hint: T,
153    finalize_callback: F,
154  ) -> Result<JsStringUtf16<'env>> {
155    use std::ptr;
156
157    use crate::{check_status, Value, ValueType};
158
159    if data.is_null() {
160      return Err(Error::new(
161        Status::InvalidArg,
162        "Data pointer should not be null".to_owned(),
163      ));
164    }
165
166    let hint_ptr = Box::into_raw(Box::new((finalize_hint, finalize_callback)));
167    let mut raw_value = ptr::null_mut();
168    let mut copied = false;
169
170    check_status!(
171      unsafe {
172        sys::node_api_create_external_string_utf16(
173          env.0,
174          data,
175          len as isize,
176          Some(finalize_with_custom_callback::<T, F>),
177          hint_ptr.cast(),
178          &mut raw_value,
179          &mut copied,
180        )
181      },
182      "Failed to create external string utf16"
183    )?;
184
185    if copied {
186      unsafe {
187        let (hint, finalize) = *Box::from_raw(hint_ptr);
188        finalize(*env, hint);
189      }
190    }
191
192    Ok(Self {
193      inner: JsString(
194        Value {
195          env: env.0,
196          value: raw_value,
197          value_type: ValueType::String,
198        },
199        std::marker::PhantomData,
200      ),
201      buf: unsafe { std::slice::from_raw_parts(data, len) },
202      _inner_buf: vec![],
203    })
204  }
205
206  #[cfg(feature = "napi10")]
207  pub fn from_static(env: &'env Env, data: &'static [u16]) -> Result<JsStringUtf16<'env>> {
208    use std::ptr;
209
210    use crate::{check_status, Value, ValueType};
211
212    if data.is_empty() {
213      return Err(Error::new(
214        Status::InvalidArg,
215        "Data should not be empty".to_owned(),
216      ));
217    }
218
219    let mut raw_value = ptr::null_mut();
220    let mut copied = false;
221
222    check_status!(
223      unsafe {
224        sys::node_api_create_external_string_utf16(
225          env.0,
226          data.as_ptr(),
227          data.len() as isize,
228          None,
229          ptr::null_mut(),
230          &mut raw_value,
231          &mut copied,
232        )
233      },
234      "Failed to create external string utf16"
235    )?;
236
237    Ok(Self {
238      inner: JsString(
239        Value {
240          env: env.0,
241          value: raw_value,
242          value_type: ValueType::String,
243        },
244        std::marker::PhantomData,
245      ),
246      buf: data,
247      _inner_buf: vec![],
248    })
249  }
250
251  pub fn as_str(&self) -> Result<String> {
252    if let Some((_, prefix)) = self.as_slice().split_last() {
253      String::from_utf16(prefix).map_err(|e| Error::new(Status::InvalidArg, format!("{e}")))
254    } else {
255      Ok(String::new())
256    }
257  }
258
259  pub fn as_slice(&self) -> &[u16] {
260    self.buf
261  }
262
263  pub fn len(&self) -> usize {
264    self.buf.len()
265  }
266
267  pub fn is_empty(&self) -> bool {
268    self.buf.is_empty()
269  }
270
271  pub fn into_value(self) -> JsString<'env> {
272    self.inner
273  }
274}
275
276impl TryFrom<JsStringUtf16<'_>> for String {
277  type Error = Error;
278
279  fn try_from(value: JsStringUtf16) -> Result<String> {
280    value.as_str()
281  }
282}
283
284impl Deref for JsStringUtf16<'_> {
285  type Target = [u16];
286
287  fn deref(&self) -> &[u16] {
288    self.buf
289  }
290}
291
292impl AsRef<[u16]> for JsStringUtf16<'_> {
293  fn as_ref(&self) -> &[u16] {
294    self.buf
295  }
296}
297
298impl From<JsStringUtf16<'_>> for Vec<u16> {
299  fn from(value: JsStringUtf16) -> Self {
300    value.as_slice().to_vec()
301  }
302}
303
304impl ToNapiValue for JsStringUtf16<'_> {
305  unsafe fn to_napi_value(_: sys::napi_env, val: JsStringUtf16) -> Result<sys::napi_value> {
306    Ok(val.inner.0.value)
307  }
308}
309
310#[cfg(feature = "napi10")]
311extern "C" fn drop_utf16_string(
312  _: sys::node_api_basic_env,
313  finalize_data: *mut c_void,
314  finalize_hint: *mut c_void,
315) {
316  let (size, capacity): (usize, usize) = unsafe { *Box::from_raw(finalize_hint.cast()) };
317  if size == 0 || finalize_data.is_null() {
318    return;
319  }
320  let data: Vec<u16> = unsafe { Vec::from_raw_parts(finalize_data.cast(), size, capacity) };
321  drop(data);
322}
323
324#[cfg(feature = "napi10")]
325extern "C" fn finalize_with_custom_callback<T, F: FnOnce(Env, T)>(
326  env: sys::node_api_basic_env,
327  _finalize_data: *mut c_void,
328  finalize_hint: *mut c_void,
329) {
330  let (hint, callback) = unsafe { *Box::from_raw(finalize_hint as *mut (T, F)) };
331  callback(Env(env.cast()), hint);
332}