Skip to main content

miniextendr_api/
encoding.rs

1//! Encoding / locale probing utilities.
2//!
3//! This module exists mainly for debugging + experiments around R's string
4//! encodings. R's runtime has both:
5//! - per-CHARSXP encoding tags (UTF-8 / Latin-1 / bytes / native)
6//! - global/locale-level settings (native encoding, UTF-8 locale flags)
7//!
8//! # Availability
9//!
10//! The global signals are **non-API** (from `Defn.h`) and require the `nonapi`
11//! feature. Only the locale flags R's shared library exports are read
12//! (`utf8locale`, `mbcslocale`, `known_to_be_latin1`); the hidden ones
13//! (`known_to_be_utf8`, `latin1locale`, `R_nativeEncoding`) must not even be
14//! *referenced* — an eager data relocation against a hidden symbol aborts
15//! `dyn.load` of the whole package (see `sys::nonapi_encoding`).
16//!
17//! `miniextendr_encoding_init()` is not called by `package_init`; it exists
18//! for debugging and for standalone Rust applications embedding R.
19
20use std::sync::OnceLock;
21
22/// Cached snapshot of R's encoding / locale state at init time.
23#[derive(Debug, Clone)]
24pub struct REncodingInfo {
25    #[cfg(feature = "nonapi")]
26    /// Whether R thinks the current locale is UTF-8 (non-API `utf8locale`).
27    pub utf8_locale: Option<bool>,
28    #[cfg(feature = "nonapi")]
29    /// Whether the current locale is multi-byte (non-API `mbcslocale`).
30    pub mbcs_locale: Option<bool>,
31    #[cfg(feature = "nonapi")]
32    /// Whether R treats unknown-encoding strings as Latin-1 (non-API
33    /// `known_to_be_latin1`).
34    pub known_to_be_latin1: Option<bool>,
35}
36
37static ENCODING_INFO: OnceLock<REncodingInfo> = OnceLock::new();
38
39/// Return the cached encoding info (if `miniextendr_encoding_init()` has run).
40#[inline]
41pub fn encoding_info() -> Option<&'static REncodingInfo> {
42    ENCODING_INFO.get()
43}
44
45/// Assert that R's locale is UTF-8.
46///
47/// Called once from `R_init_*` (package init). Errors if the R session
48/// does not use UTF-8, since `charsxp_to_str` assumes all CHARSXP bytes
49/// are valid UTF-8.
50///
51/// Uses `l10n_info()[["UTF-8"]]` which is public R API.
52///
53/// ABI: must be `extern "C-unwind"`, not `extern "C"` — the failure branch
54/// raises via `Rf_error`, and under webR (Emscripten builds R with
55/// `-sSUPPORT_LONGJMP=wasm`) that longjmp is a real wasm exception that
56/// unwinds through this frame. A non-unwind `extern "C"` ABI makes rustc
57/// insert an abort guard at the boundary, which turns the load-gate error
58/// into an `unreachable` trap that kills the whole wasm runtime (observed
59/// in the tier-3 testthat pass via `assert_utf8_locale_now()`). On native
60/// targets `longjmp` never touches landing pads, which is why this only
61/// bit on wasm32.
62#[unsafe(no_mangle)]
63pub extern "C-unwind" fn miniextendr_assert_utf8_locale() {
64    debug_assert!(
65        crate::worker::is_r_main_thread(),
66        "must be called from R main thread"
67    );
68    use crate::SexpExt;
69    use crate::sys::{R_BaseEnv, Rf_eval, Rf_install};
70
71    unsafe {
72        // Read `l10n_info()[["UTF-8"]]` inside a scope so the temporaries
73        // (call, info) are unprotected *before* the error branch below —
74        // matching the original ordering (a longjmp out of `Rf_error` would
75        // skip the scope's Drop).
76        let is_utf8 = {
77            let scope = crate::ProtectScope::new();
78            // Call l10n_info()
79            let call = scope.protect_raw(crate::sys::Rf_lang1(Rf_install(c"l10n_info".as_ptr())));
80            let info = scope.protect_raw(Rf_eval(call, R_BaseEnv));
81
82            // Find the "UTF-8" element by name
83            let names = info.get_names();
84            let n = info.xlength();
85            let mut is_utf8 = false;
86            for i in 0..n {
87                let name_charsxp = names.string_elt(i);
88                if name_charsxp.r_char_str() == Some("UTF-8") {
89                    let elt = info.vector_elt(i);
90                    is_utf8 = elt.logical_elt(0) != 0;
91                    break;
92                }
93            }
94            is_utf8
95        };
96
97        if !is_utf8 {
98            crate::sys::Rf_error_unchecked(
99                c"%s".as_ptr(),
100                c"miniextendr requires a UTF-8 locale (R >= 4.2.0 uses UTF-8 by default)".as_ptr(),
101            );
102        }
103    }
104}
105
106/// Initialize / snapshot R's encoding state.
107///
108/// Intended to be called once from `R_init_*` (package init).
109#[unsafe(no_mangle)]
110pub extern "C-unwind" fn miniextendr_encoding_init() {
111    let _ = ENCODING_INFO.get_or_init(|| {
112        #[cfg(feature = "nonapi")]
113        unsafe {
114            use crate::Rboolean;
115            use crate::sys::nonapi_encoding;
116
117            let utf8_locale = Some(nonapi_encoding::utf8locale != Rboolean::FALSE);
118            let mbcs_locale = Some(nonapi_encoding::mbcslocale != Rboolean::FALSE);
119            let known_to_be_latin1 = Some(nonapi_encoding::known_to_be_latin1 != Rboolean::FALSE);
120
121            let info = REncodingInfo {
122                utf8_locale,
123                mbcs_locale,
124                known_to_be_latin1,
125            };
126
127            if std::env::var_os("MINIEXTENDR_ENCODING_DEBUG").is_some() {
128                let msg = format!("[miniextendr] encoding init: {info:?}\n");
129                if let Ok(c_msg) = std::ffi::CString::new(msg) {
130                    crate::sys::REprintf_unchecked(c"%s".as_ptr(), c_msg.as_ptr());
131                }
132            }
133
134            info
135        }
136
137        #[cfg(not(feature = "nonapi"))]
138        {
139            REncodingInfo {}
140        }
141    });
142}