Skip to content
Open
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
build:
name: ${{ matrix.config.variant }} ${{ matrix.config.target }} ${{ matrix.config.v8_enable_pointer_compression && 'ptrcomp' || '' }} ${{ matrix.config.simdutf && 'simdutf' || '' }}
runs-on: ${{ matrix.config.os }}
timeout-minutes: 180
timeout-minutes: ${{ matrix.config.target == 'aarch64-unknown-linux-gnu' && matrix.config.variant == 'release' && matrix.config.simdutf && 240 || 180 }}
strategy:
# Always run main branch builds to completion. This allows the cache to
# stay mostly up-to-date in situations where a single job fails due to
Expand Down
14 changes: 11 additions & 3 deletions .gn
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,17 @@ default_args = {

use_relative_vtables_abi = false

v8_depend_on_icu_data_file = false
icu_copy_icudata_to_root_build_dir = false
icu_use_data_file = false
# Build with external ICU data instead of linking the ~11 MiB ICU common
# data into the static library. Embedders provide a valid ICU `.dat` package
# at runtime via `v8::icu::set_common_data_77_from_file` (see README).
#
# `icu_use_data_file` keeps the data out of the static library; the other two
# flags only feed the build-time `mksnapshot` step a copy of `icudtl.dat`
# (mksnapshot must initialize ICU to generate the snapshot). They do not
# relink ICU data into the published library.
icu_use_data_file = true
icu_copy_icudata_to_root_build_dir = true
v8_depend_on_icu_data_file = true

# TODO: third_party/compiler-rt missing?
use_llvm_libatomic = false
Expand Down
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,48 @@ this shared-library-compatible TLS mode.
Env vars used in when building from source: `SCCACHE`, `CCACHE`, `GN`, `NINJA`,
`CLANG_BASE_PATH`, `GN_ARGS`

### ICU data

rusty_v8 builds V8 with internationalization support enabled. By default it
builds with external ICU data rather than linking Chromium's ~11 MiB ICU common
data into the static library, keeping the binary small:

```gn
v8_enable_i18n_support=true
icu_use_data_file=true
```

At startup, before creating isolates, load a valid ICU `.dat` package and keep
the returned guard alive for as long as ICU can use it:

```rust
let icu_data =
v8::icu::set_common_data_77_from_file("icudtl.dat")?;
```

Embedders that prefer the full ICU common data linked into the static library
(so no runtime data file is needed) can opt out of external data when building
from source:

```bash
GN_ARGS='icu_use_data_file=false' V8_FROM_SOURCE=1 cargo build
```

The data file must be valid for the ICU major version linked into rusty_v8.
Chromium vendors several ICU 77 data bundles in this checkout, trading size for
locale coverage:

| Bundle (`third_party/icu/.../icudtl.dat`) | Size | Locale coverage |
| --- | --- | --- |
| `common` | ~10.3 MiB | full (~1400 locales) |
| `cast` | ~5.0 MiB | ~250 locales |
| `flutter_desktop` | ~1.7 MiB | reduced |
| `flutter` | ~0.82 MiB | English + root |

Smaller bundles still expose the full `Intl` API; non-English locales fall back
toward root behavior, similar to Node's `small-icu` mode. The bundle to ship is
an embedder decision — pick one that covers the locales your application needs.

## FAQ

**Building V8 takes over 30 minutes, this is too slow for me to use this crate.
Expand Down
115 changes: 113 additions & 2 deletions src/icu.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
use crate::support::char;

use std::ffi::CString;
use std::fmt;
use std::fs;
use std::io;
use std::path::Path;
use std::slice;

unsafe extern "C" {
fn icu_get_default_locale(output: *mut char, output_len: usize) -> usize;
Expand Down Expand Up @@ -40,9 +45,93 @@ unsafe extern "C" {
///
/// This function has no effect on application (non ICU) data. See udata_setAppData() for similar
/// functionality for application data.
// TODO(ry) Map error code to something useful.
#[repr(align(16))]
#[allow(dead_code)]
struct Align16([u8; 16]);

/// Owned ICU common data with an address that is aligned for
/// [`set_common_data_77`].
///
/// Keep this value alive for as long as ICU may use the data.
pub struct CommonData {
storage: Box<[Align16]>,
len: usize,
}

impl CommonData {
/// Copies ICU common data into owned 16-byte-aligned storage.
pub fn from_bytes(data: &[u8]) -> Self {
let chunk_count = data.len().div_ceil(16);
let mut storage = Vec::with_capacity(chunk_count);
storage.resize_with(chunk_count, || Align16([0; 16]));
let mut common_data = Self {
storage: storage.into_boxed_slice(),
len: data.len(),
};
common_data.as_bytes_mut()[..data.len()].copy_from_slice(data);
common_data
}

/// Loads ICU common data from a file into owned 16-byte-aligned storage.
pub fn from_file(path: impl AsRef<Path>) -> io::Result<Self> {
fs::read(path).map(|data| Self::from_bytes(&data))
}

/// Returns the aligned ICU common data bytes.
pub fn as_bytes(&self) -> &[u8] {
unsafe {
slice::from_raw_parts(self.storage.as_ptr() as *const u8, self.len)
}
}

fn as_bytes_mut(&mut self) -> &mut [u8] {
unsafe {
slice::from_raw_parts_mut(
self.storage.as_mut_ptr() as *mut u8,
self.storage.len() * 16,
)
}
}
}

/// Error returned by [`set_common_data_77_from_file`].
#[derive(Debug)]
pub enum SetCommonDataError {
/// The ICU common data file could not be read.
Io(io::Error),
/// ICU rejected the common data package.
Icu(i32),
}

impl fmt::Display for SetCommonDataError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Io(err) => write!(f, "failed to read ICU common data: {err}"),
Self::Icu(error_code) => {
write!(f, "ICU rejected common data with error code {error_code}")
}
}
}
}

impl std::error::Error for SetCommonDataError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Io(err) => Some(err),
Self::Icu(_) => None,
}
}
}

impl From<io::Error> for SetCommonDataError {
fn from(err: io::Error) -> Self {
Self::Io(err)
}
}

// TODO(ry) Map ICU error code to something useful.
#[inline(always)]
pub fn set_common_data_77(data: &'static [u8]) -> Result<(), i32> {
fn set_common_data_77_impl(data: &[u8]) -> Result<(), i32> {
let mut error_code = 0i32;
unsafe {
udata_setCommonData_77(data.as_ptr(), &mut error_code);
Expand All @@ -54,6 +143,28 @@ pub fn set_common_data_77(data: &'static [u8]) -> Result<(), i32> {
}
}

/// Set ICU common data from static aligned bytes.
///
/// For runtime-selected data, use [`set_common_data_77_from_file`] so the data
/// storage stays alive and aligned.
#[inline(always)]
pub fn set_common_data_77(data: &'static [u8]) -> Result<(), i32> {
set_common_data_77_impl(data)
}

/// Loads and sets ICU common data from a runtime-selected `.dat` file.
///
/// Keep the returned [`CommonData`] value alive for as long as ICU may use the
/// data. The data file must match the ICU major version used to build
/// rusty_v8.
pub fn set_common_data_77_from_file(
path: impl AsRef<Path>,
) -> Result<CommonData, SetCommonDataError> {
let data = CommonData::from_file(path)?;
set_common_data_77_impl(data.as_bytes()).map_err(SetCommonDataError::Icu)?;
Ok(data)
}

/// Returns BCP47 language tag.
pub fn get_language_tag() -> String {
let mut output = [0u8; 1024];
Expand Down
20 changes: 20 additions & 0 deletions tests/test_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9812,6 +9812,26 @@ fn icu_set_common_data_fail() {
);
}

#[test]
fn icu_common_data_from_bytes_is_aligned() {
let data = v8::icu::CommonData::from_bytes(&[1, 2, 3]);
assert_eq!(data.as_bytes(), &[1, 2, 3]);
assert_eq!(data.as_bytes().as_ptr() as usize % 16, 0);
}

#[test]
fn icu_set_common_data_from_file_fail() {
let path = std::env::temp_dir().join(format!(
"rusty_v8_invalid_icu_{}_{}.dat",
std::process::id(),
line!(),
));
std::fs::write(&path, [1, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0]).unwrap();
let result = v8::icu::set_common_data_77_from_file(&path);
std::fs::remove_file(path).unwrap();
assert!(matches!(result, Err(v8::icu::SetCommonDataError::Icu(_))));
}

#[test]
fn icu_format() {
let _setup_guard = setup::parallel_test();
Expand Down
Loading