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#[unsafe(no_mangle)]
53pub extern "C" fn miniextendr_assert_utf8_locale() {
54    debug_assert!(
55        crate::worker::is_r_main_thread(),
56        "must be called from R main thread"
57    );
58    use crate::SexpExt;
59    use crate::sys::{R_BaseEnv, Rf_eval, Rf_install, Rf_protect, Rf_unprotect};
60
61    unsafe {
62        // Call l10n_info()
63        let call = crate::sys::Rf_lang1(Rf_install(c"l10n_info".as_ptr()));
64        Rf_protect(call);
65        let info = Rf_eval(call, R_BaseEnv);
66        Rf_protect(info);
67
68        // Find the "UTF-8" element by name
69        let names = info.get_names();
70        let n = info.xlength();
71        let mut is_utf8 = false;
72        for i in 0..n {
73            let name_charsxp = names.string_elt(i);
74            if name_charsxp.r_char_str() == Some("UTF-8") {
75                let elt = info.vector_elt(i);
76                is_utf8 = elt.logical_elt(0) != 0;
77                break;
78            }
79        }
80
81        Rf_unprotect(2);
82
83        if !is_utf8 {
84            crate::sys::Rf_error_unchecked(
85                c"%s".as_ptr(),
86                c"miniextendr requires a UTF-8 locale (R >= 4.2.0 uses UTF-8 by default)".as_ptr(),
87            );
88        }
89    }
90}
91
92/// Initialize / snapshot R's encoding state.
93///
94/// Intended to be called once from `R_init_*` (package init).
95#[unsafe(no_mangle)]
96pub extern "C-unwind" fn miniextendr_encoding_init() {
97    let _ = ENCODING_INFO.get_or_init(|| {
98        #[cfg(feature = "nonapi")]
99        unsafe {
100            use crate::Rboolean;
101            use crate::sys::nonapi_encoding;
102
103            let utf8_locale = Some(nonapi_encoding::utf8locale != Rboolean::FALSE);
104            let mbcs_locale = Some(nonapi_encoding::mbcslocale != Rboolean::FALSE);
105            let known_to_be_latin1 = Some(nonapi_encoding::known_to_be_latin1 != Rboolean::FALSE);
106
107            let info = REncodingInfo {
108                utf8_locale,
109                mbcs_locale,
110                known_to_be_latin1,
111            };
112
113            if std::env::var_os("MINIEXTENDR_ENCODING_DEBUG").is_some() {
114                let msg = format!("[miniextendr] encoding init: {info:?}\n");
115                if let Ok(c_msg) = std::ffi::CString::new(msg) {
116                    crate::sys::REprintf_unchecked(c"%s".as_ptr(), c_msg.as_ptr());
117                }
118            }
119
120            info
121        }
122
123        #[cfg(not(feature = "nonapi"))]
124        {
125            REncodingInfo {}
126        }
127    });
128}