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}