From be929b9cfaafa5314756b9e81a47a8e211715504 Mon Sep 17 00:00:00 2001 From: divybot Date: Mon, 1 Jun 2026 14:15:53 +0000 Subject: [PATCH 1/5] feat(icu): support runtime ICU data files Add owned aligned ICU common data storage and a file-loading helper so embedders can keep V8 i18n enabled while selecting a valid ICU .dat bundle at startup. Document the external-data GN configuration and reduced Chromium ICU bundle sizes observed during the investigation. Co-Authored-By: Divy Srivastava --- README.md | 35 ++++++++++++++ src/icu.rs | 115 +++++++++++++++++++++++++++++++++++++++++++++- tests/test_api.rs | 20 ++++++++ 3 files changed, 168 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bd17bc29a4..b3c800e8a8 100644 --- a/README.md +++ b/README.md @@ -172,6 +172,41 @@ 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 and, by default, +links Chromium's full ICU common data into the static library: + +```gn +v8_enable_i18n_support=true +icu_use_data_file=false +``` + +Embedders that need a smaller standalone binary can instead build with external +ICU data and provide a valid ICU `.dat` package at runtime: + +```bash +GN_ARGS='icu_use_data_file=true v8_depend_on_icu_data_file=false' \ + V8_FROM_SOURCE=1 cargo build +``` + +At startup, before creating isolates, load the selected data file and keep the +returned guard alive while ICU can use it: + +```rust +let icu_data = + v8::icu::set_common_data_77_from_file("icudtl.dat")?; +``` + +The data file must be valid for the ICU major version linked into rusty_v8. +Chromium's vendored ICU 77 data bundles in this checkout are approximately: +`third_party/icu/common/icudtl.dat` 11 MiB, +`third_party/icu/cast/icudtl.dat` 5.1 MiB, +`third_party/icu/flutter_desktop/icudtl.dat` 1.7 MiB, and +`third_party/icu/flutter/icudtl.dat` 844 KiB. Reduced bundles are expected to +provide partial locale behavior similar to Node's `small-icu` mode rather than +removing `Intl`. + ## FAQ **Building V8 takes over 30 minutes, this is too slow for me to use this crate. diff --git a/src/icu.rs b/src/icu.rs index 7737f1aaca..21526cd1c9 100644 --- a/src/icu.rs +++ b/src/icu.rs @@ -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; @@ -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) -> io::Result { + 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 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); @@ -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, +) -> Result { + 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]; diff --git a/tests/test_api.rs b/tests/test_api.rs index bb3ae8d3ea..824f4f7474 100644 --- a/tests/test_api.rs +++ b/tests/test_api.rs @@ -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(); From 06fc35ebba69ada41337809f58f8b47a2bdc1d3f Mon Sep 17 00:00:00 2001 From: divybot Date: Mon, 1 Jun 2026 18:09:37 +0000 Subject: [PATCH 2/5] ci: extend aarch64 simdutf timeout The release aarch64-unknown-linux-gnu simdutf CI job was cancelled after hitting the existing 180 minute job timeout during the Test step. Give only that slow source-build matrix entry a 240 minute timeout while preserving the existing 180 minute limit for the rest of the matrix. Co-Authored-By: Divy Srivastava --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a42c2987ac..83f35d35d7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 From 36d69f58e1ea7929d8d652e77bccd70a97b8aed1 Mon Sep 17 00:00:00 2001 From: Divy Srivastava Date: Mon, 8 Jun 2026 16:32:16 +0530 Subject: [PATCH 3/5] feat(icu): default to external ICU data Set icu_use_data_file=true by default so the static library no longer embeds the ~11 MiB ICU common data. Embedders supply a valid ICU .dat package at runtime via set_common_data_77_from_file. Build with icu_use_data_file=false to opt back into linked-in ICU data. --- .gn | 5 ++++- README.md | 28 +++++++++++++++------------- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/.gn b/.gn index 5f9df22a68..3384ab6f8c 100644 --- a/.gn +++ b/.gn @@ -78,9 +78,12 @@ default_args = { use_relative_vtables_abi = 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). v8_depend_on_icu_data_file = false icu_copy_icudata_to_root_build_dir = false - icu_use_data_file = false + icu_use_data_file = true # TODO: third_party/compiler-rt missing? use_llvm_libatomic = false diff --git a/README.md b/README.md index b3c800e8a8..e30e157989 100644 --- a/README.md +++ b/README.md @@ -174,30 +174,32 @@ Env vars used in when building from source: `SCCACHE`, `CCACHE`, `GN`, `NINJA`, ### ICU data -rusty_v8 builds V8 with internationalization support enabled and, by default, -links Chromium's full ICU common data into the static library: +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=false +icu_use_data_file=true +v8_depend_on_icu_data_file=false ``` -Embedders that need a smaller standalone binary can instead build with external -ICU data and provide a valid ICU `.dat` package at runtime: - -```bash -GN_ARGS='icu_use_data_file=true v8_depend_on_icu_data_file=false' \ - V8_FROM_SOURCE=1 cargo build -``` - -At startup, before creating isolates, load the selected data file and keep the -returned guard alive while ICU can use it: +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's vendored ICU 77 data bundles in this checkout are approximately: `third_party/icu/common/icudtl.dat` 11 MiB, From f3b8dc7b006f5066f059e86fed06140aaaa91dc1 Mon Sep 17 00:00:00 2001 From: Divy Srivastava Date: Mon, 8 Jun 2026 16:42:50 +0530 Subject: [PATCH 4/5] fix(icu): feed mksnapshot ICU data for external-data build mksnapshot must initialize ICU to generate the V8 snapshot. With icu_use_data_file=true but the icudata copy disabled, it failed with 'Failed to initialize ICU'. Enable icu_copy_icudata_to_root_build_dir and v8_depend_on_icu_data_file so mksnapshot gets a build-time copy of icudtl.dat. These only affect the build graph, not the published lib. --- .gn | 9 +++++++-- README.md | 1 - 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.gn b/.gn index 3384ab6f8c..6374522dd4 100644 --- a/.gn +++ b/.gn @@ -81,9 +81,14 @@ default_args = { # 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). - v8_depend_on_icu_data_file = false - icu_copy_icudata_to_root_build_dir = false + # + # `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 diff --git a/README.md b/README.md index e30e157989..c7a3b1e543 100644 --- a/README.md +++ b/README.md @@ -181,7 +181,6 @@ data into the static library, keeping the binary small: ```gn v8_enable_i18n_support=true icu_use_data_file=true -v8_depend_on_icu_data_file=false ``` At startup, before creating isolates, load a valid ICU `.dat` package and keep From 8ac49f5fc0aaa801854a7800deb9322cd3d57282 Mon Sep 17 00:00:00 2001 From: Divy Srivastava Date: Mon, 8 Jun 2026 18:24:30 +0530 Subject: [PATCH 5/5] docs(icu): list vendored ICU data bundle sizes and locale coverage --- README.md | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index c7a3b1e543..af83c0fc10 100644 --- a/README.md +++ b/README.md @@ -200,13 +200,19 @@ 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's vendored ICU 77 data bundles in this checkout are approximately: -`third_party/icu/common/icudtl.dat` 11 MiB, -`third_party/icu/cast/icudtl.dat` 5.1 MiB, -`third_party/icu/flutter_desktop/icudtl.dat` 1.7 MiB, and -`third_party/icu/flutter/icudtl.dat` 844 KiB. Reduced bundles are expected to -provide partial locale behavior similar to Node's `small-icu` mode rather than -removing `Intl`. +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