From b277a1e67be33dea05ca1ef51178b7086336fad0 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Thu, 19 Jun 2025 23:26:05 +0200 Subject: [PATCH] Make the ICU SONAME configurable (#495) This PR was tested on Ubuntu with: ``` EDIT_CFG_ICUUC_SONAME=libicuuc.so.74 EDIT_CFG_ICUI18N_SONAME=libicui18n.so.74 EDIT_CFG_ICU_RENAMING_VERSION=74 cargo build --config .cargo/release.toml --release ``` Search & Replace now works flawlessly. I hope that package maintainers will be able to make use of this when ingesting future versions of Edit. Closes #172 --- README.md | 50 ++++++++++++++++++++---- build.rs | 89 ++++++++++++++++++++++++++++++++++++++++++- src/arena/release.rs | 6 ++- src/arena/scratch.rs | 5 +++ src/icu.rs | 91 +++++++++++++++++++++++++------------------- src/sys/unix.rs | 76 ++++++++++++++++++++++++++---------- src/sys/windows.rs | 81 ++++++++++++++++++++++++++++++++++----- 7 files changed, 317 insertions(+), 81 deletions(-) diff --git a/README.md b/README.md index 1cbf903..021daa5 100644 --- a/README.md +++ b/README.md @@ -19,14 +19,6 @@ You can install the latest version with WinGet: winget install Microsoft.Edit ``` -### Notes to Package Maintainers - -The canonical executable name is "edit" and the alternative name is "msedit". - -We're aware of the potential conflict of "edit" with existing commands and as such recommend naming packages and executables "msedit". -Names such as "ms-edit" should be avoided. -Assigning an "edit" alias is recommended if possible. - ## Build Instructions * [Install Rust](https://www.rust-lang.org/tools/install) @@ -34,3 +26,45 @@ Assigning an "edit" alias is recommended if possible. * Alternatively, set the environment variable `RUSTC_BOOTSTRAP=1` * Clone the repository * For a release build, run: `cargo build --config .cargo/release.toml --release` + +## Notes to Package Maintainers + +### Package Naming + +The canonical executable name is "edit" and the alternative name is "msedit". +We're aware of the potential conflict of "edit" with existing commands and recommend alternatively naming packages and executables "msedit". +Names such as "ms-edit" should be avoided. +Assigning an "edit" alias is recommended, if possible. + +### ICU library name (SONAME) + +This project _optionally_ depends on the ICU library for its Search and Replace functionality. +By default, the project will look for a SONAME without version suffix: +* Windows: `icuuc.dll` +* macOS: `libicuuc.dylib` +* UNIX, and other OS: `libicuuc.so` + +If your installation uses a different SONAME, please set the following environment variable at build time: +* `EDIT_CFG_ICUUC_SONAME`: + For instance, `libicuuc.so.76`. +* `EDIT_CFG_ICUI18N_SONAME`: + For instance, `libicui18n.so.76`. + +Additionally, this project assumes that the ICU exports are exported without `_` prefix and without version suffix, such as `u_errorName`. +If your installation uses versioned exports, please set: +* `EDIT_CFG_ICU_CPP_EXPORTS`: + If set to `true`, it'll look for C++ symbols such as `_u_errorName`. + Enabled by default on macOS. +* `EDIT_CFG_ICU_RENAMING_VERSION`: + If set to a version number, such as `76`, it'll look for symbols such as `u_errorName_76`. + +Finally, you can set the following environment variables: +* `EDIT_CFG_ICU_RENAMING_AUTO_DETECT`: + If set to `true`, the executable will try to detect the `EDIT_CFG_ICU_RENAMING_VERSION` value at runtime. + The way it does this is not officially supported by ICU and as such is not recommended to be relied upon. + Enabled by default on UNIX (excluding macOS) if no other options are set. + +To test your settings, run `cargo test` again but with the `--ignored` flag. For instance: +```sh +cargo test -- --ignored +``` diff --git a/build.rs b/build.rs index 7a56606..c017bf7 100644 --- a/build.rs +++ b/build.rs @@ -1,9 +1,86 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +use core::panic; +use std::env::VarError; + +#[derive(PartialEq, Eq)] +enum TargetOs { + Windows, + MacOS, + Unix, +} + fn main() { + let target_os = match env_opt("CARGO_CFG_TARGET_OS").as_str() { + "windows" => TargetOs::Windows, + "macos" | "ios" => TargetOs::MacOS, + _ => TargetOs::Unix, + }; + let icuuc_soname = env_opt("EDIT_CFG_ICUUC_SONAME"); + let icui18n_soname = env_opt("EDIT_CFG_ICUI18N_SONAME"); + let cpp_exports = env_opt("EDIT_CFG_ICU_CPP_EXPORTS"); + let renaming_version = env_opt("EDIT_CFG_ICU_RENAMING_VERSION"); + let renaming_auto_detect = env_opt("EDIT_CFG_ICU_RENAMING_AUTO_DETECT"); + + // If none of the `EDIT_CFG_ICU*` environment variables are set, + // we default to enabling `EDIT_CFG_ICU_RENAMING_AUTO_DETECT` on UNIX. + // This slightly improves portability at least in the cases where the SONAMEs match our defaults. + let renaming_auto_detect = if !renaming_auto_detect.is_empty() { + renaming_auto_detect.parse::().unwrap() + } else { + target_os == TargetOs::Unix + && icuuc_soname.is_empty() + && icui18n_soname.is_empty() + && cpp_exports.is_empty() + && renaming_version.is_empty() + }; + if renaming_auto_detect && !renaming_version.is_empty() { + // It makes no sense to specify an explicit version and also ask for auto-detection. + panic!( + "Either `EDIT_CFG_ICU_RENAMING_AUTO_DETECT` or `EDIT_CFG_ICU_RENAMING_VERSION` must be set, but not both" + ); + } + + let icuuc_soname = if !icuuc_soname.is_empty() { + &icuuc_soname + } else { + match target_os { + TargetOs::Windows => "icuuc.dll", + TargetOs::MacOS => "libicucore.dylib", + TargetOs::Unix => "libicuuc.so", + } + }; + let icui18n_soname = if !icui18n_soname.is_empty() { + &icui18n_soname + } else { + match target_os { + TargetOs::Windows => "icuin.dll", + TargetOs::MacOS => "libicucore.dylib", + TargetOs::Unix => "libicui18n.so", + } + }; + let icu_export_prefix = + if !cpp_exports.is_empty() && cpp_exports.parse::().unwrap() { "_" } else { "" }; + let icu_export_suffix = + if !renaming_version.is_empty() { format!("_{renaming_version}") } else { String::new() }; + + println!("cargo::rerun-if-env-changed=EDIT_CFG_ICUUC_SONAME"); + println!("cargo::rustc-env=EDIT_CFG_ICUUC_SONAME={icuuc_soname}"); + println!("cargo::rerun-if-env-changed=EDIT_CFG_ICUI18N_SONAME"); + println!("cargo::rustc-env=EDIT_CFG_ICUI18N_SONAME={icui18n_soname}"); + println!("cargo::rerun-if-env-changed=EDIT_CFG_ICU_EXPORT_PREFIX"); + println!("cargo::rustc-env=EDIT_CFG_ICU_EXPORT_PREFIX={icu_export_prefix}"); + println!("cargo::rerun-if-env-changed=EDIT_CFG_ICU_EXPORT_SUFFIX"); + println!("cargo::rustc-env=EDIT_CFG_ICU_EXPORT_SUFFIX={icu_export_suffix}"); + println!("cargo::rerun-if-env-changed=EDIT_CFG_ICU_RENAMING_AUTO_DETECT"); + println!("cargo::rustc-check-cfg=cfg(edit_icu_renaming_auto_detect)"); + if renaming_auto_detect { + println!("cargo::rustc-cfg=edit_icu_renaming_auto_detect"); + } + #[cfg(windows)] - if std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default() == "windows" { + if target_os == TargetOs::Windows { winresource::WindowsResource::new() .set_manifest_file("src/bin/edit/edit.exe.manifest") .set("FileDescription", "Microsoft Edit") @@ -13,3 +90,13 @@ fn main() { .unwrap(); } } + +fn env_opt(name: &str) -> String { + match std::env::var(name) { + Ok(value) => value, + Err(VarError::NotPresent) => String::new(), + Err(VarError::NotUnicode(_)) => { + panic!("Environment variable `{name}` is not valid Unicode") + } + } +} diff --git a/src/arena/release.rs b/src/arena/release.rs index 6e2d32f..7e4ebcf 100644 --- a/src/arena/release.rs +++ b/src/arena/release.rs @@ -79,6 +79,10 @@ impl Arena { }) } + pub fn is_empty(&self) -> bool { + self.base == NonNull::dangling() + } + pub fn offset(&self) -> usize { self.offset.get() } @@ -171,7 +175,7 @@ impl Arena { impl Drop for Arena { fn drop(&mut self) { - if self.base != NonNull::dangling() { + if !self.is_empty() { unsafe { sys::virtual_release(self.base, self.capacity) }; } } diff --git a/src/arena/scratch.rs b/src/arena/scratch.rs index 4805bfb..bb91ead 100644 --- a/src/arena/scratch.rs +++ b/src/arena/scratch.rs @@ -46,6 +46,11 @@ pub fn init(capacity: usize) -> apperr::Result<()> { /// If your function takes an [`Arena`] argument, you **MUST** pass it to `scratch_arena` as `Some(&arena)`. pub fn scratch_arena(conflict: Option<&Arena>) -> ScratchArena<'static> { unsafe { + #[cfg(test)] + if S_SCRATCH[0].is_empty() { + init(128 * 1024 * 1024).unwrap(); + } + #[cfg(debug_assertions)] let conflict = conflict.map(|a| a.delegate_target_unchecked()); diff --git a/src/icu.rs b/src/icu.rs index b0261ab..9687794 100644 --- a/src/icu.rs +++ b/src/icu.rs @@ -4,7 +4,7 @@ //! Bindings to the ICU library. use std::cmp::Ordering; -use std::ffi::CStr; +use std::ffi::{CStr, c_char}; use std::mem; use std::mem::MaybeUninit; use std::ops::Range; @@ -922,33 +922,40 @@ struct LibraryFunctions { uregex_end64: icu_ffi::uregex_end64, } +macro_rules! proc_name { + ($s:literal) => { + concat!(env!("EDIT_CFG_ICU_EXPORT_PREFIX"), $s, env!("EDIT_CFG_ICU_EXPORT_SUFFIX"), "\0") + .as_ptr() as *const c_char + }; +} + // Found in libicuuc.so on UNIX, icuuc.dll/icu.dll on Windows. -const LIBICUUC_PROC_NAMES: [&CStr; 10] = [ - c"u_errorName", - c"ucasemap_open", - c"ucasemap_utf8FoldCase", - c"ucnv_getAvailableName", - c"ucnv_getStandardName", - c"ucnv_open", - c"ucnv_close", - c"ucnv_convertEx", - c"utext_setup", - c"utext_close", +const LIBICUUC_PROC_NAMES: [*const c_char; 10] = [ + proc_name!("u_errorName"), + proc_name!("ucasemap_open"), + proc_name!("ucasemap_utf8FoldCase"), + proc_name!("ucnv_getAvailableName"), + proc_name!("ucnv_getStandardName"), + proc_name!("ucnv_open"), + proc_name!("ucnv_close"), + proc_name!("ucnv_convertEx"), + proc_name!("utext_setup"), + proc_name!("utext_close"), ]; // Found in libicui18n.so on UNIX, icuin.dll/icu.dll on Windows. -const LIBICUI18N_PROC_NAMES: [&CStr; 11] = [ - c"ucol_open", - c"ucol_strcollUTF8", - c"uregex_open", - c"uregex_close", - c"uregex_setTimeLimit", - c"uregex_setUText", - c"uregex_reset64", - c"uregex_findNext", - c"uregex_groupCount", - c"uregex_start64", - c"uregex_end64", +const LIBICUI18N_PROC_NAMES: [*const c_char; 11] = [ + proc_name!("ucol_open"), + proc_name!("ucol_strcollUTF8"), + proc_name!("uregex_open"), + proc_name!("uregex_close"), + proc_name!("uregex_setTimeLimit"), + proc_name!("uregex_setUText"), + proc_name!("uregex_reset64"), + proc_name!("uregex_findNext"), + proc_name!("uregex_groupCount"), + proc_name!("uregex_start64"), + proc_name!("uregex_end64"), ]; enum LibraryFunctionsState { @@ -971,10 +978,7 @@ fn init_if_needed() -> apperr::Result<&'static LibraryFunctions> { unsafe { LIBRARY_FUNCTIONS = LibraryFunctionsState::Failed; - let Ok(libicuuc) = sys::load_libicuuc() else { - return; - }; - let Ok(libicui18n) = sys::load_libicui18n() else { + let Ok(icu) = sys::load_icu() else { return; }; @@ -998,25 +1002,26 @@ fn init_if_needed() -> apperr::Result<&'static LibraryFunctions> { let mut funcs = MaybeUninit::::uninit(); let mut ptr = funcs.as_mut_ptr() as *mut TransparentFunction; - #[cfg(unix)] + #[cfg(edit_icu_renaming_auto_detect)] let scratch_outer = scratch_arena(None); - #[cfg(unix)] - let suffix = sys::icu_proc_suffix(&scratch_outer, libicuuc); + #[cfg(edit_icu_renaming_auto_detect)] + let suffix = sys::icu_detect_renaming_suffix(&scratch_outer, icu.libicuuc); - for (handle, names) in - [(libicuuc, &LIBICUUC_PROC_NAMES[..]), (libicui18n, &LIBICUI18N_PROC_NAMES[..])] - { - for name in names { - #[cfg(unix)] + for (handle, names) in [ + (icu.libicuuc, &LIBICUUC_PROC_NAMES[..]), + (icu.libicui18n, &LIBICUI18N_PROC_NAMES[..]), + ] { + for &name in names { + #[cfg(edit_icu_renaming_auto_detect)] let scratch = scratch_arena(Some(&scratch_outer)); - #[cfg(unix)] - let name = &sys::add_icu_proc_suffix(&scratch, name, &suffix); + #[cfg(edit_icu_renaming_auto_detect)] + let name = sys::icu_add_renaming_suffix(&scratch, name, &suffix); let Ok(func) = sys::get_proc_address(handle, name) else { debug_assert!( false, - "Failed to load ICU function: {}", - name.to_string_lossy() + "Failed to load ICU function: {:?}", + CStr::from_ptr(name) ); return; }; @@ -1314,6 +1319,12 @@ mod icu_ffi { mod tests { use super::*; + #[ignore] + #[test] + fn init() { + assert!(init_if_needed().is_ok()); + } + #[test] fn test_compare_strings_ascii() { // Empty strings diff --git a/src/sys/unix.rs b/src/sys/unix.rs index 4389f30..ce7eb20 100644 --- a/src/sys/unix.rs +++ b/src/sys/unix.rs @@ -6,8 +6,8 @@ //! Read the `windows` module for reference. //! TODO: This reminds me that the sys API should probably be a trait. -use std::ffi::{CStr, c_int, c_void}; -use std::fs::{self, File}; +use std::ffi::{CStr, c_char, c_int, c_void}; +use std::fs::File; use std::mem::{self, ManuallyDrop, MaybeUninit}; use std::os::fd::{AsRawFd as _, FromRawFd as _}; use std::path::Path; @@ -433,9 +433,9 @@ pub unsafe fn virtual_commit(base: NonNull, size: usize) -> apperr::Result<( } } -unsafe fn load_library(name: &CStr) -> apperr::Result> { +unsafe fn load_library(name: *const c_char) -> apperr::Result> { unsafe { - NonNull::new(libc::dlopen(name.as_ptr(), libc::RTLD_LAZY)) + NonNull::new(libc::dlopen(name, libc::RTLD_LAZY)) .ok_or_else(|| errno_to_apperr(libc::ENOENT)) } } @@ -448,9 +448,12 @@ unsafe fn load_library(name: &CStr) -> apperr::Result> { /// of the function you're loading. No type checks whatsoever are performed. // // It'd be nice to constrain T to std::marker::FnPtr, but that's unstable. -pub unsafe fn get_proc_address(handle: NonNull, name: &CStr) -> apperr::Result { +pub unsafe fn get_proc_address( + handle: NonNull, + name: *const c_char, +) -> apperr::Result { unsafe { - let sym = libc::dlsym(handle.as_ptr(), name.as_ptr()); + let sym = libc::dlsym(handle.as_ptr(), name); if sym.is_null() { Err(errno_to_apperr(libc::ENOENT)) } else { @@ -459,20 +462,46 @@ pub unsafe fn get_proc_address(handle: NonNull, name: &CStr) -> apper } } -pub fn load_libicuuc() -> apperr::Result> { - unsafe { load_library(c"libicuuc.so") } +pub struct LibIcu { + pub libicuuc: NonNull, + pub libicui18n: NonNull, } -pub fn load_libicui18n() -> apperr::Result> { - unsafe { load_library(c"libicui18n.so") } -} +pub fn load_icu() -> apperr::Result { + const fn const_str_eq(a: &str, b: &str) -> bool { + let a = a.as_bytes(); + let b = b.as_bytes(); + let mut i = 0; + loop { + if i >= a.len() || i >= b.len() { + return a.len() == b.len(); + } + if a[i] != b[i] { + return false; + } + i += 1; + } + } + + const LIBICUUC: &str = concat!(env!("EDIT_CFG_ICUUC_SONAME"), "\0"); + const LIBICUI18N: &str = concat!(env!("EDIT_CFG_ICUI18N_SONAME"), "\0"); + + if const { const_str_eq(LIBICUUC, LIBICUI18N) } { + let icu = unsafe { load_library(LIBICUUC.as_ptr() as *const _)? }; + Ok(LibIcu { libicuuc: icu, libicui18n: icu }) + } else { + let libicuuc = unsafe { load_library(LIBICUUC.as_ptr() as *const _)? }; + let libicui18n = unsafe { load_library(LIBICUI18N.as_ptr() as *const _)? }; + Ok(LibIcu { libicuuc, libicui18n }) + } +} /// ICU, by default, adds the major version as a suffix to each exported symbol. /// They also recommend to disable this for system-level installations (`runConfigureICU Linux --disable-renaming`), /// but I found that many (most?) Linux distributions don't do this for some reason. /// This function returns the suffix, if any. -#[allow(clippy::not_unsafe_ptr_arg_deref)] -pub fn icu_proc_suffix(arena: &Arena, handle: NonNull) -> ArenaString<'_> { +#[cfg(edit_icu_renaming_auto_detect)] +pub fn icu_detect_renaming_suffix(arena: &Arena, handle: NonNull) -> ArenaString<'_> { unsafe { type T = *const c_void; @@ -480,7 +509,7 @@ pub fn icu_proc_suffix(arena: &Arena, handle: NonNull) -> ArenaString<'_ // Check if the ICU library is using unversioned symbols. // Return an empty suffix in that case. - if get_proc_address::(handle, c"u_errorName").is_ok() { + if get_proc_address::(handle, c"u_errorName".as_ptr()).is_ok() { return res; } @@ -488,7 +517,7 @@ pub fn icu_proc_suffix(arena: &Arena, handle: NonNull) -> ArenaString<'_ // this symbol seems to be always present. This allows us to call `dladdr`. // It's the `UCaseMap::~UCaseMap()` destructor which for some reason isn't // in a namespace. Thank you ICU maintainers for this oversight. - let proc = match get_proc_address::(handle, c"_ZN8UCaseMapD1Ev") { + let proc = match get_proc_address::(handle, c"_ZN8UCaseMapD1Ev".as_ptr()) { Ok(proc) => proc, Err(_) => return res, }; @@ -506,7 +535,7 @@ pub fn icu_proc_suffix(arena: &Arena, handle: NonNull) -> ArenaString<'_ Err(_) => return res, }; - let path = match fs::read_link(path) { + let path = match std::fs::read_link(path) { Ok(path) => path, Err(_) => path.into(), }; @@ -528,7 +557,13 @@ pub fn icu_proc_suffix(arena: &Arena, handle: NonNull) -> ArenaString<'_ } } -pub fn add_icu_proc_suffix<'a, 'b, 'r>(arena: &'a Arena, name: &'b CStr, suffix: &str) -> &'r CStr +#[cfg(edit_icu_renaming_auto_detect)] +#[allow(clippy::not_unsafe_ptr_arg_deref)] +pub fn icu_add_renaming_suffix<'a, 'b, 'r>( + arena: &'a Arena, + name: *const c_char, + suffix: &str, +) -> *const c_char where 'a: 'r, 'b: 'r, @@ -538,16 +573,15 @@ where } else { // SAFETY: In this particular case we know that the string // is valid UTF-8, because it comes from icu.rs. + let name = unsafe { CStr::from_ptr(name) }; let name = unsafe { name.to_str().unwrap_unchecked() }; - let mut res = ArenaString::new_in(arena); + let mut res = ManuallyDrop::new(ArenaString::new_in(arena)); res.reserve(name.len() + suffix.len() + 1); res.push_str(name); res.push_str(suffix); res.push('\0'); - - let bytes: &'a [u8] = unsafe { mem::transmute(res.as_bytes()) }; - unsafe { CStr::from_bytes_with_nul_unchecked(bytes) } + res.as_ptr() as *const c_char } } diff --git a/src/sys/windows.rs b/src/sys/windows.rs index 71d9b51..be524ea 100644 --- a/src/sys/windows.rs +++ b/src/sys/windows.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use std::ffi::{CStr, OsString, c_void}; +use std::ffi::{OsString, c_char, c_void}; use std::fmt::Write as _; use std::fs::{self, File}; use std::mem::MaybeUninit; @@ -20,6 +20,35 @@ use crate::apperr; use crate::arena::{Arena, ArenaString, scratch_arena}; use crate::helpers::*; +macro_rules! w_env { + ($s:literal) => {{ + const INPUT: &[u8] = env!($s).as_bytes(); + const OUTPUT_LEN: usize = windows_sys::core::utf16_len(INPUT) + 1; + const OUTPUT: &[u16; OUTPUT_LEN] = { + let mut buffer = [0; OUTPUT_LEN]; + let mut input_pos = 0; + let mut output_pos = 0; + while let Some((mut code_point, new_pos)) = + windows_sys::core::decode_utf8_char(INPUT, input_pos) + { + input_pos = new_pos; + if code_point <= 0xffff { + buffer[output_pos] = code_point as u16; + output_pos += 1; + } else { + code_point -= 0x10000; + buffer[output_pos] = 0xd800 + (code_point >> 10) as u16; + output_pos += 1; + buffer[output_pos] = 0xdc00 + (code_point & 0x3ff) as u16; + output_pos += 1; + } + } + &{ buffer } + }; + OUTPUT.as_ptr() + }}; +} + type ReadConsoleInputExW = unsafe extern "system" fn( h_console_input: Foundation::HANDLE, lp_buffer: *mut Console::INPUT_RECORD, @@ -109,7 +138,10 @@ pub fn init() -> apperr::Result { } unsafe fn load_read_func(module: *const u16) -> apperr::Result { - unsafe { get_module(module).and_then(|m| get_proc_address(m, c"ReadConsoleInputExW")) } + unsafe { + get_module(module) + .and_then(|m| get_proc_address(m, c"ReadConsoleInputExW".as_ptr())) + } } // `kernel32.dll` doesn't exist on OneCore variants of Windows. @@ -564,21 +596,50 @@ unsafe fn load_library(name: *const u16) -> apperr::Result> { /// of the function you're loading. No type checks whatsoever are performed. // // It'd be nice to constrain T to std::marker::FnPtr, but that's unstable. -pub unsafe fn get_proc_address(handle: NonNull, name: &CStr) -> apperr::Result { +pub unsafe fn get_proc_address( + handle: NonNull, + name: *const c_char, +) -> apperr::Result { unsafe { - let ptr = LibraryLoader::GetProcAddress(handle.as_ptr(), name.as_ptr() as *const u8); + let ptr = LibraryLoader::GetProcAddress(handle.as_ptr(), name as *const u8); if let Some(ptr) = ptr { Ok(mem::transmute_copy(&ptr)) } else { Err(get_last_error()) } } } -/// Loads the "common" portion of ICU4C. -pub fn load_libicuuc() -> apperr::Result> { - unsafe { load_library(w!("icuuc.dll")) } +pub struct LibIcu { + pub libicuuc: NonNull, + pub libicui18n: NonNull, } -/// Loads the internationalization portion of ICU4C. -pub fn load_libicui18n() -> apperr::Result> { - unsafe { load_library(w!("icuin.dll")) } +pub fn load_icu() -> apperr::Result { + const fn const_ptr_u16_eq(a: *const u16, b: *const u16) -> bool { + unsafe { + let mut a = a; + let mut b = b; + loop { + if *a != *b { + return false; + } + if *a == 0 { + return true; + } + a = a.add(1); + b = b.add(1); + } + } + } + + const LIBICUUC: *const u16 = w_env!("EDIT_CFG_ICUUC_SONAME"); + const LIBICUI18N: *const u16 = w_env!("EDIT_CFG_ICUI18N_SONAME"); + + if const { const_ptr_u16_eq(LIBICUUC, LIBICUI18N) } { + let icu = unsafe { load_library(LIBICUUC)? }; + Ok(LibIcu { libicuuc: icu, libicui18n: icu }) + } else { + let libicuuc = unsafe { load_library(LIBICUUC)? }; + let libicui18n = unsafe { load_library(LIBICUI18N)? }; + Ok(LibIcu { libicuuc, libicui18n }) + } } /// Returns a list of preferred languages for the current user.