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}