Skip to main content

deno_core/
fast_string.rs

1// Copyright 2018-2026 the Deno authors. MIT license.
2
3use std::borrow::Borrow;
4use std::ffi::OsStr;
5use std::fmt::Debug;
6use std::fmt::Display;
7use std::fmt::Formatter;
8use std::hash::Hash;
9use std::ops::Deref;
10use std::sync::Arc;
11
12use serde::Deserializer;
13use serde::Serializer;
14use url::Url;
15use v8::NewStringType;
16
17use crate::ToV8;
18
19static EMPTY_STRING: v8::OneByteConst =
20  v8::String::create_external_onebyte_const("".as_bytes());
21
22/// A static string that is compile-time checked to be ASCII and is stored in the
23/// most efficient possible way to create V8 strings.
24#[derive(Clone, Copy)]
25#[repr(transparent)]
26pub struct FastStaticString {
27  s: &'static v8::OneByteConst,
28}
29
30impl FastStaticString {
31  pub const fn new(s: &'static v8::OneByteConst) -> Self {
32    FastStaticString { s }
33  }
34
35  pub fn as_str(&self) -> &'static str {
36    self.s.as_ref()
37  }
38
39  pub fn as_bytes(&self) -> &'static [u8] {
40    self.s.as_ref()
41  }
42
43  #[doc(hidden)]
44  pub const fn create_external_onebyte_const(
45    s: &'static [u8],
46  ) -> v8::OneByteConst {
47    v8::String::create_external_onebyte_const(s)
48  }
49
50  pub fn v8_string<'s, 'i>(
51    &self,
52    scope: &mut v8::PinScope<'s, 'i>,
53  ) -> Result<v8::Local<'s, v8::String>, FastStringV8AllocationError> {
54    FastString::from(*self).v8_string(scope)
55  }
56
57  pub const fn into_v8_const_ptr(&self) -> *const v8::OneByteConst {
58    self.s as _
59  }
60}
61
62impl From<&'static v8::OneByteConst> for FastStaticString {
63  fn from(s: &'static v8::OneByteConst) -> Self {
64    Self::new(s)
65  }
66}
67
68impl From<FastStaticString> for *const v8::OneByteConst {
69  fn from(val: FastStaticString) -> Self {
70    val.into_v8_const_ptr()
71  }
72}
73
74impl Hash for FastStaticString {
75  fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
76    self.as_str().hash(state)
77  }
78}
79
80impl AsRef<str> for FastStaticString {
81  fn as_ref(&self) -> &str {
82    self.as_str()
83  }
84}
85
86impl Deref for FastStaticString {
87  type Target = str;
88  fn deref(&self) -> &Self::Target {
89    self.as_str()
90  }
91}
92
93impl Borrow<str> for FastStaticString {
94  fn borrow(&self) -> &str {
95    self.as_str()
96  }
97}
98
99impl Debug for FastStaticString {
100  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101    Debug::fmt(self.as_str(), f)
102  }
103}
104
105impl Default for FastStaticString {
106  fn default() -> Self {
107    FastStaticString { s: &EMPTY_STRING }
108  }
109}
110
111impl PartialEq for FastStaticString {
112  fn eq(&self, other: &Self) -> bool {
113    self.as_bytes() == other.as_bytes()
114  }
115}
116
117impl Eq for FastStaticString {}
118
119impl Display for FastStaticString {
120  fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
121    f.write_str(self.as_str())
122  }
123}
124
125#[derive(Debug, deno_error::JsError)]
126#[class(type)]
127pub struct FastStringV8AllocationError;
128
129impl std::error::Error for FastStringV8AllocationError {}
130impl std::fmt::Display for FastStringV8AllocationError {
131  fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
132    write!(
133      f,
134      "failed to allocate string; buffer exceeds maximum length"
135    )
136  }
137}
138
139/// Module names and code can be sourced from strings or bytes that are either owned or borrowed. This enumeration allows us
140/// to perform a minimal amount of cloning and format-shifting of the underlying data.
141///
142/// Note that any [`FastString`] created using [`ascii_str!`] must contain only ASCII characters. Other [`FastString`] types
143/// may be UTF-8, though this will incur a small performance penalty. It is recommended that large, static strings always
144/// use [`ascii_str!`].
145///
146/// Examples of ways to construct a [`FastString`]:
147///
148/// ```rust
149/// # use deno_core::{ascii_str, FastString};
150///
151/// let code: FastString = ascii_str!("a string").into();
152/// let code: FastString = format!("a string").into();
153/// ```
154pub struct FastString {
155  inner: FastStringInner,
156}
157
158enum FastStringInner {
159  /// Created from static data.
160  Static(&'static str),
161
162  /// Created from static ascii, known to contain only ASCII chars.
163  StaticAscii(&'static str),
164
165  /// Created from static data, known to contain only ASCII chars.
166  StaticConst(FastStaticString),
167
168  /// An owned chunk of data. Note that we use `Box` rather than `Vec` to avoid the
169  /// storage overhead.
170  Owned(Box<str>),
171
172  // Scripts loaded from the `deno_graph` infrastructure.
173  Arc(Arc<str>),
174}
175
176impl FastString {
177  /// Create a [`FastString`] from a static string. The string may contain
178  /// non-ASCII characters, and if so, will take the slower path when used
179  /// in v8.
180  pub const fn from_static(s: &'static str) -> Self {
181    if s.is_ascii() {
182      Self {
183        inner: FastStringInner::StaticAscii(s),
184      }
185    } else {
186      Self {
187        inner: FastStringInner::Static(s),
188      }
189    }
190  }
191
192  /// Create a [`FastString`] from a static string that is known to contain
193  /// only ASCII characters.
194  ///
195  /// Note: This function is deliberately not `const fn`. Use `from_static`
196  /// in const contexts.
197  ///
198  /// # Safety
199  ///
200  /// It is unsafe to specify a non-ASCII string here because this will be
201  /// referenced in an external one byte static string in v8, which requires
202  /// the data be Latin-1 or ASCII.
203  ///
204  /// This should only be used in scenarios where you know a string is ASCII
205  /// and you want to avoid the performance overhead of checking if a string
206  /// is ASCII that `from_static` does.
207  pub unsafe fn from_ascii_static_unchecked(s: &'static str) -> Self {
208    debug_assert!(
209      s.is_ascii(),
210      "use `from_non_ascii_static_unsafe` for non-ASCII strings",
211    );
212    Self {
213      inner: FastStringInner::StaticAscii(s),
214    }
215  }
216
217  /// Create a [`FastString`] from a static string that may contain non-ASCII
218  /// characters.
219  ///
220  /// This should only be used in scenarios where you know a string is not ASCII
221  /// and you want to avoid the performance overhead of checking if the string
222  /// is ASCII that `from_static` does.
223  ///
224  /// Note: This function is deliberately not `const fn`. Use `from_static`
225  /// in const contexts. This function is not unsafe because using this with
226  /// an ascii string will just not be as optimal for performance.
227  pub fn from_non_ascii_static(s: &'static str) -> Self {
228    Self {
229      inner: FastStringInner::Static(s),
230    }
231  }
232
233  /// Returns a static string from this `FastString`, if available.
234  pub fn as_static_str(&self) -> Option<&'static str> {
235    match self.inner {
236      FastStringInner::Static(s) => Some(s),
237      FastStringInner::StaticAscii(s) => Some(s),
238      FastStringInner::StaticConst(s) => Some(s.as_str()),
239      _ => None,
240    }
241  }
242
243  /// Creates a cheap copy of this [`FastString`], potentially transmuting it
244  /// to a faster form. Note that this is not a clone operation as it consumes
245  /// the old [`FastString`].
246  pub fn into_cheap_copy(self) -> (Self, Self) {
247    match self.inner {
248      FastStringInner::Owned(s) => {
249        let s: Arc<str> = s.into();
250        (
251          Self {
252            inner: FastStringInner::Arc(s.clone()),
253          },
254          Self {
255            inner: FastStringInner::Arc(s),
256          },
257        )
258      }
259      _ => (self.try_clone().unwrap(), self),
260    }
261  }
262
263  /// If this [`FastString`] is cheaply cloneable, returns a clone.
264  pub fn try_clone(&self) -> Option<Self> {
265    match &self.inner {
266      FastStringInner::Static(s) => Some(Self {
267        inner: FastStringInner::Static(s),
268      }),
269      FastStringInner::StaticAscii(s) => Some(Self {
270        inner: FastStringInner::StaticAscii(s),
271      }),
272      FastStringInner::StaticConst(s) => Some(Self {
273        inner: FastStringInner::StaticConst(*s),
274      }),
275      FastStringInner::Arc(s) => Some(Self {
276        inner: FastStringInner::Arc(s.clone()),
277      }),
278      FastStringInner::Owned(_s) => None,
279    }
280  }
281
282  #[inline(always)]
283  pub fn as_bytes(&self) -> &[u8] {
284    self.as_str().as_bytes()
285  }
286
287  #[inline(always)]
288  pub fn as_str(&self) -> &str {
289    match &self.inner {
290      // TODO(mmastrac): When we get a const deref, as_str can be const
291      FastStringInner::Arc(s) => s,
292      FastStringInner::Owned(s) => s,
293      FastStringInner::Static(s) => s,
294      FastStringInner::StaticAscii(s) => s,
295      FastStringInner::StaticConst(s) => s.as_str(),
296    }
297  }
298
299  /// Create a v8 string from this [`FastString`]. If the string is static and contains only ASCII characters,
300  /// an external one-byte static is created.
301  pub fn v8_string<'a, 'i>(
302    &self,
303    scope: &mut v8::PinScope<'a, 'i>,
304  ) -> Result<v8::Local<'a, v8::String>, FastStringV8AllocationError> {
305    match self.inner {
306      FastStringInner::StaticAscii(s) => {
307        v8::String::new_external_onebyte_static(scope, s.as_bytes())
308          .ok_or(FastStringV8AllocationError)
309      }
310      FastStringInner::StaticConst(s) => {
311        v8::String::new_from_onebyte_const(scope, s.s)
312          .ok_or(FastStringV8AllocationError)
313      }
314      _ => {
315        v8::String::new_from_utf8(scope, self.as_bytes(), NewStringType::Normal)
316          .ok_or(FastStringV8AllocationError)
317      }
318    }
319  }
320
321  /// Truncates a [`FastString`] value, possibly re-allocating or memcpy'ing. May be slow.
322  pub fn truncate(&mut self, index: usize) {
323    match &mut self.inner {
324      FastStringInner::Static(b) => {
325        self.inner = FastStringInner::Static(&b[..index])
326      }
327      FastStringInner::StaticAscii(b) => {
328        self.inner = FastStringInner::StaticAscii(&b[..index])
329      }
330      FastStringInner::StaticConst(b) => {
331        self.inner = FastStringInner::StaticAscii(&b.as_str()[..index])
332      }
333      // TODO(mmastrac): this could be more efficient
334      FastStringInner::Owned(b) => {
335        self.inner = FastStringInner::Owned(b[..index].to_owned().into())
336      }
337      // We can't do much if we have an Arc<str>, so we'll just take ownership of the truncated version
338      FastStringInner::Arc(s) => {
339        self.inner = FastStringInner::Arc(s[..index].to_owned().into())
340      }
341    }
342  }
343}
344
345impl Hash for FastString {
346  fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
347    self.as_str().hash(state)
348  }
349}
350
351impl AsRef<str> for FastString {
352  fn as_ref(&self) -> &str {
353    self.as_str()
354  }
355}
356
357impl AsRef<[u8]> for FastString {
358  fn as_ref(&self) -> &[u8] {
359    self.as_str().as_ref()
360  }
361}
362
363impl AsRef<OsStr> for FastString {
364  fn as_ref(&self) -> &OsStr {
365    self.as_str().as_ref()
366  }
367}
368
369impl Deref for FastString {
370  type Target = str;
371  fn deref(&self) -> &Self::Target {
372    self.as_str()
373  }
374}
375
376impl Borrow<str> for FastString {
377  fn borrow(&self) -> &str {
378    self.as_str()
379  }
380}
381
382impl Debug for FastString {
383  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
384    Debug::fmt(self.as_str(), f)
385  }
386}
387
388impl Default for FastString {
389  fn default() -> Self {
390    Self {
391      inner: FastStringInner::StaticConst(FastStaticString::default()),
392    }
393  }
394}
395
396impl PartialEq for FastString {
397  fn eq(&self, other: &Self) -> bool {
398    self.as_bytes() == other.as_bytes()
399  }
400}
401
402impl Eq for FastString {}
403
404impl Display for FastString {
405  fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
406    f.write_str(self.as_str())
407  }
408}
409
410/// [`FastString`] can be made cheaply from [`Url`] as we know it's owned and don't need to do an
411/// ASCII check.
412impl From<FastStaticString> for FastString {
413  fn from(value: FastStaticString) -> Self {
414    Self {
415      inner: FastStringInner::StaticConst(value),
416    }
417  }
418}
419
420/// [`FastString`] can be made cheaply from [`Url`] as we know it's owned and don't need to do an
421/// ASCII check.
422impl From<Url> for FastString {
423  fn from(value: Url) -> Self {
424    let s: String = value.into();
425    s.into()
426  }
427}
428
429/// [`FastString`] can be made cheaply from [`String`] as we know it's owned and don't need to do an
430/// ASCII check.
431impl From<String> for FastString {
432  fn from(value: String) -> Self {
433    Self {
434      inner: FastStringInner::Owned(value.into_boxed_str()),
435    }
436  }
437}
438
439/// [`FastString`] can be made cheaply from [`Arc<str>`] as we know it's shared and don't need to do an
440/// ASCII check.
441impl From<Arc<str>> for FastString {
442  fn from(value: Arc<str>) -> Self {
443    Self {
444      inner: FastStringInner::Arc(value),
445    }
446  }
447}
448
449impl From<FastString> for Arc<str> {
450  fn from(value: FastString) -> Self {
451    use FastStringInner::*;
452    match value.inner {
453      Static(text) | StaticAscii(text) => text.into(),
454      StaticConst(text) => text.as_ref().into(),
455      Owned(text) => text.into(),
456      Arc(text) => text,
457    }
458  }
459}
460
461impl<'s> ToV8<'s> for FastString {
462  type Error = FastStringV8AllocationError;
463
464  #[inline]
465  fn to_v8<'i>(
466    self,
467    scope: &mut v8::PinScope<'s, 'i>,
468  ) -> Result<v8::Local<'s, v8::Value>, Self::Error> {
469    Ok(self.v8_string(scope)?.into())
470  }
471}
472
473impl serde::Serialize for FastString {
474  fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
475  where
476    S: Serializer,
477  {
478    serializer.serialize_str(self.as_str())
479  }
480}
481
482type DeserializeProxy<'de> = &'de str;
483
484impl<'de> serde::Deserialize<'de> for FastString {
485  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
486  where
487    D: Deserializer<'de>,
488  {
489    DeserializeProxy::<'de>::deserialize(deserializer)
490      .map(|v| v.to_owned().into())
491  }
492}
493
494/// Include a fast string in the binary. This string is asserted at compile-time to be 7-bit ASCII for optimal
495/// v8 performance.
496///
497/// This macro creates a [`FastStaticString`] that may be converted to a [`FastString`] via [`Into::into`].
498#[macro_export]
499macro_rules! ascii_str_include {
500  ($file:expr_2021) => {{
501    const STR: $crate::v8::OneByteConst =
502      $crate::FastStaticString::create_external_onebyte_const(
503        ::std::include_str!($file).as_bytes(),
504      );
505    let s: &'static $crate::v8::OneByteConst = &STR;
506    $crate::FastStaticString::new(s)
507  }};
508}
509
510/// Include a fast string in the binary from a string literal. This string is asserted at compile-time to be
511/// 7-bit ASCII for optimal v8 performance.
512///
513/// This macro creates a [`FastStaticString`] that may be converted to a [`FastString`] via [`Into::into`].
514#[macro_export]
515macro_rules! ascii_str {
516  ($str:expr_2021) => {{
517    const C: $crate::v8::OneByteConst =
518      $crate::FastStaticString::create_external_onebyte_const($str.as_bytes());
519    $crate::FastStaticString::new(&C)
520  }};
521}
522
523/// Used to generate the fast, const versions of op names. Internal only.
524#[macro_export]
525#[doc(hidden)]
526macro_rules! __op_name_fast {
527  ($op:ident) => {{
528    const LITERAL: &'static [u8] = stringify!($op).as_bytes();
529    const STR: $crate::v8::OneByteConst =
530      $crate::FastStaticString::create_external_onebyte_const(LITERAL);
531    let s: &'static $crate::v8::OneByteConst = &STR;
532    (stringify!($op), $crate::FastStaticString::new(s))
533  }};
534}
535
536#[cfg(test)]
537mod tests {
538  use super::*;
539
540  #[test]
541  fn string_eq() {
542    let s: FastString = ascii_str!("Testing").into();
543    assert_eq!("Testing", s.as_str());
544    let s2 = FastString::from_static("Testing");
545    assert_eq!(s, s2);
546    let (s1, s2) = s.into_cheap_copy();
547    assert_eq!("Testing", s1.as_str());
548    assert_eq!("Testing", s2.as_str());
549
550    let s = FastString::from("Testing".to_owned());
551    assert_eq!("Testing", s.as_str());
552    let (s1, s2) = s.into_cheap_copy();
553    assert_eq!("Testing", s1.as_str());
554    assert_eq!("Testing", s2.as_str());
555  }
556
557  #[test]
558  fn truncate() {
559    let mut s = "123456".to_owned();
560    s.truncate(3);
561
562    let mut code: FastString = ascii_str!("123456").into();
563    code.truncate(3);
564    assert_eq!(s, code.as_str());
565
566    let mut code: FastString = "123456".to_owned().into();
567    code.truncate(3);
568    assert_eq!(s, code.as_str());
569
570    let arc_str: Arc<str> = "123456".into();
571    let mut code: FastString = arc_str.into();
572    code.truncate(3);
573    assert_eq!(s, code.as_str());
574  }
575
576  #[test]
577  fn test_large_include() {
578    // This test would require an excessively large file in the repo, so we just run this manually
579    // ascii_str_include!("runtime/tests/large_string.txt");
580    // ascii_str_include!(concat!("runtime", "/tests/", "large_string.txt"));
581  }
582
583  /// Ensure that all of our macros compile properly in a static context.
584  #[test]
585  fn test_const() {
586    const _: (&str, FastStaticString) = __op_name_fast!(op_name);
587    const _: FastStaticString = ascii_str!("hmm");
588    const _: FastStaticString = ascii_str!(concat!("hmm", "hmmmmm"));
589    const _: FastStaticString = ascii_str_include!("Cargo.toml");
590    const _: FastStaticString = ascii_str_include!(concat!("./", "Cargo.toml"));
591  }
592}