libxml_rs/xml/threads/mod.rs
1//! Threading support (§57, §93, §85 Phase 1).
2//!
3//! Thread-local state, concurrent parsing, concurrent transformation,
4//! shared immutable dictionaries, callback isolation.
5//!
6//! # UPSTREAM-PARITY
7//!
8//! libxml2's threading support provides:
9//!
10//! - `xmlInitThreads()` / `xmlCleanupThreads()` — lifecycle
11//! - `xmlLockLibrary()` / `xmlUnlockLibrary()` — global lock
12//! - Thread-local storage for error state and parser contexts
13//!
14//! In modern libxml2 (2.12+), threading is initialized automatically
15//! by `xmlInitParser`. The explicit thread functions exist for backward
16//! compatibility.
17//!
18//! In Rust, we use standard thread-safe primitives and `thread_local!`
19//! for thread-local storage. The global lock is a no-op since Rust's
20//! type system prevents data races at compile time.
21//!
22//! # Phase 1 status
23//!
24//! Complete — all threading support is implemented.
25//!
26//! # Upstream contract
27//!
28//! Mirrors upstream `threads.c` (`SRC-LIBXML2-2.15.0-THREADS-C`, parity
29//! target libxml2 2.15.3 oracle): `xmlInitThreads`, `xmlCleanupThreads`,
30//! `xmlLockLibrary`, `xmlUnlockLibrary`, `xmlNewMutex`/`xmlFreeMutex`/
31//! `xmlMutexLock`/`xmlMutexUnlock`, `xmlNewRMutex`/`xmlRMutexLock`/
32//! `xmlRMutexUnlock`, `xmlNewCond`/`xmlCondWaitSignal`, and the
33//! thread-local variants.
34//!
35//! # Conceptual behavior
36//!
37//! Implements the legacy explicit threading API. In modern libxml2 (2.12+)
38//! initialization is lazy — `xmlInitParser` calls the init path
39//! automatically, and the deprecated entry points exist for backward
40//! compatibility. Rust primitives (`thread_local!`, parking_lot, atomics)
41//! provide the same guarantees without upstream platform dispatch
42//! (HAVE_POSIX_THREADS / HAVE_WIN32_THREADS).
43//!
44//! # Ownership & safety invariants
45//!
46//! Mutex/rmutex/cond handles are heap objects owned by the caller and
47//! freed with the matching free function; thread-local error/parser state
48//! is owned per thread and never shared. Rust memory-safety guarantees
49//! replace the upstream data-race discipline — the SAFETY argument is the
50//! type system, not lock discipline.
51//!
52//! # Historical quirks & epochs
53//!
54//! Thread support predates the thread-local globals era: globals.c
55//! threading was integrated 2001-10-12/13 (commits b847864f, d0463560,
56//! LORE-0005). R-000138: the deprecated init/cleanup entry points are
57//! genuine no-ops in modern upstream (lazy init) and the candidate matches
58//! that; `xmlCheckThreadLocalStorage` always passes with Rust thread-locals.
59//!
60//! # Deliberate oddities
61//!
62//! The global library lock is a deliberate no-op: Rust prevents data races
63//! at compile time, and upstream xmlLockLibrary itself became vestigial
64//! after the thread-local rewrite. Deprecated entry points keep their
65//! no-op bodies to match the oracle byte-for-byte (R-000138).
66//!
67//! # Proving courts
68//!
69//! The globals-threading differential probe (tools/abi/globals_threading_
70//! probe.py + courts/suites/data-abi/globals-threading-probe.c) verifies
71//! handler-slot and error-global behavior byte-identical vs the oracle;
72//! the parallel lib suite (100/100 runs clean, R-000170/R-000171) and
73//! cargo test exercise the thread-local error model.
74//!
75//! # Tempting simplifications that would break parity
76//!
77//! Do not replace thread-locals with globals: per-thread parser error
78//! state and the exported xmlLastError mirror (R-000170) depend on the
79//! thread-local model. Do not make the deprecated entry points do real
80//! work: upstream bodies are empty and observable behavior must match.
81
82use core::ffi::c_void;
83use core::sync::atomic::{AtomicBool, Ordering};
84use std::os::raw::c_int;
85
86/// Whether threading has been initialized.
87static THREADS_INITIALIZED: AtomicBool = AtomicBool::new(false);
88
89/// Initialize threading support.
90///
91/// # UPSTREAM-PARITY
92///
93/// ```c
94/// int xmlInitThreads(void);
95/// ```
96///
97/// Returns 0 on success. In modern libxml2, this is called automatically
98/// by `xmlInitParser`.
99pub fn init_threads() -> c_int {
100 THREADS_INITIALIZED.store(true, Ordering::Release);
101 0
102}
103
104/// Clean up threading support.
105///
106/// # UPSTREAM-PARITY
107///
108/// ```c
109/// void xmlCleanupThreads(void);
110/// ```
111pub fn cleanup_threads() {
112 THREADS_INITIALIZED.store(false, Ordering::Release);
113}
114
115/// Check whether threading has been initialized.
116pub fn threads_initialized() -> bool {
117 THREADS_INITIALIZED.load(Ordering::Acquire)
118}
119
120/// Lock the library (global mutex).
121///
122/// # UPSTREAM-PARITY
123///
124/// ```c
125/// void xmlLockLibrary(void);
126/// ```
127///
128/// In upstream libxml2, this locks a global mutex. In Rust, this is
129/// a no-op because Rust's type system prevents data races. However,
130/// for FFI safety with C callers that may manipulate shared state,
131/// a real mutex would be needed. This will be enhanced in Phase 2+.
132pub const fn lock_library() {
133 // Phase 1: no-op — Rust's type system handles data races for internal code.
134 // For C callers going through FFI, this is a best-effort approach.
135}
136
137/// Unlock the library.
138///
139/// # UPSTREAM-PARITY
140///
141/// ```c
142/// void xmlUnlockLibrary(void);
143/// ```
144pub const fn unlock_library() {
145 // Phase 1: no-op
146}
147
148/// Get the number of active threads (for compatibility).
149///
150/// Returns the number of active threads, or 0 if unknown.
151/// This is a compatibility stub — upstream doesn't expose this directly.
152pub const fn get_thread_count() -> c_int {
153 1
154}
155
156// ═══════════════════════════════════════════════════════════════════════════════
157// Mutex / recursive-mutex API (upstream threads.h)
158// ═══════════════════════════════════════════════════════════════════════════════
159//
160// `xmlMutexPtr`/`xmlRMutexPtr` are opaque handles. The candidate boxes a
161// parking_lot mutex (a `Mutex<()>` for the simple mutex, a reentrant
162// `ReentrantMutex` for the recursive mutex) so lock/unlock round-trips
163// through the FFI boundary with real exclusion semantics.
164//
165// The `RawMutex` trait import brings the manual `lock`/`unlock` methods
166// into scope for the raw-mutex lock/unlock used below.
167use parking_lot::lock_api::RawMutex as _;
168
169/// Create a simple mutex (upstream threads.h `xmlNewMutex`).
170///
171/// Returns an opaque handle (free with `xmlFreeMutex`), or NULL on
172/// allocation failure.
173pub fn new_mutex() -> *mut c_void {
174 let m = Box::new(parking_lot::Mutex::new(()));
175 Box::into_raw(m) as *mut c_void
176}
177
178/// Free a simple mutex (upstream threads.h `xmlFreeMutex`).
179///
180/// # SAFETY
181///
182/// - `tok` must be a handle from `xmlNewMutex` (or NULL), and must not be
183/// locked by any thread when freed.
184pub unsafe fn free_mutex(tok: *mut c_void) {
185 if !tok.is_null() {
186 drop(Box::from_raw(tok as *mut parking_lot::Mutex<()>));
187 }
188}
189
190/// Lock a simple mutex (upstream threads.h `xmlMutexLock`).
191///
192/// # SAFETY
193///
194/// - `tok` must be a handle from `xmlNewMutex` or NULL.
195pub unsafe fn mutex_lock(tok: *mut c_void) {
196 if tok.is_null() {
197 return;
198 }
199 // SAFETY: tok is a valid handle from xmlNewMutex. The raw lock blocks
200 // until the mutex is acquired and stays held until the matching
201 // mutex_unlock (parking_lot lock_api RawMutex manual API). The previous
202 // guard-based code dropped the guard immediately, providing no
203 // exclusion; the 11.1-Z seal fixed this to real lock semantics.
204 unsafe { (*(tok as *mut parking_lot::Mutex<()>)).raw().lock() };
205}
206
207/// Unlock a simple mutex (upstream threads.h `xmlMutexUnlock`).
208///
209/// # SAFETY
210///
211/// - `tok` must be a handle from `xmlNewMutex` or NULL, and must be
212/// locked by the calling thread.
213pub unsafe fn mutex_unlock(tok: *mut c_void) {
214 if tok.is_null() {
215 return;
216 }
217 // SAFETY: tok must be a handle from xmlNewMutex locked by this thread
218 // via mutex_lock; unlocking releases the raw mutex.
219 unsafe { (*(tok as *mut parking_lot::Mutex<()>)).raw().unlock() };
220}
221
222/// Create a recursive mutex (upstream threads.h `xmlNewRMutex`).
223///
224/// Returns an opaque handle (free with `xmlFreeRMutex`), or NULL on
225/// allocation failure.
226pub fn new_rmutex() -> *mut c_void {
227 let m = Box::new(parking_lot::ReentrantMutex::new(()));
228 Box::into_raw(m) as *mut c_void
229}
230
231/// Free a recursive mutex (upstream threads.h `xmlFreeRMutex`).
232///
233/// # SAFETY
234///
235/// - `tok` must be a handle from `xmlNewRMutex` (or NULL).
236pub unsafe fn free_rmutex(tok: *mut c_void) {
237 if !tok.is_null() {
238 drop(Box::from_raw(tok as *mut parking_lot::ReentrantMutex<()>));
239 }
240}
241
242/// Lock a recursive mutex (upstream threads.h `xmlRMutexLock`).
243///
244/// # SAFETY
245///
246/// - `tok` must be a handle from `xmlNewRMutex` or NULL.
247pub unsafe fn rmutex_lock(tok: *mut c_void) {
248 if tok.is_null() {
249 return;
250 }
251 // SAFETY: tok is a valid handle from xmlNewRMutex; reentrant locking is
252 // permitted, and the raw lock stays held until rmutex_unlock.
253 unsafe {
254 (*(tok as *mut parking_lot::ReentrantMutex<()>))
255 .raw()
256 .lock()
257 };
258}
259
260/// Unlock a recursive mutex (upstream threads.h `xmlRMutexUnlock`).
261///
262/// # SAFETY
263///
264/// - `tok` must be a handle from `xmlNewRMutex` or NULL.
265pub unsafe fn rmutex_unlock(tok: *mut c_void) {
266 if tok.is_null() {
267 return;
268 }
269 // SAFETY: tok must be a handle from xmlNewRMutex locked by this thread
270 // via rmutex_lock; unlocking releases the raw reentrant mutex.
271 unsafe {
272 (*(tok as *mut parking_lot::ReentrantMutex<()>))
273 .raw()
274 .unlock()
275 };
276}
277
278// ═══════════════════════════════════════════════════════════════════════════════
279// Tests
280// ═══════════════════════════════════════════════════════════════════════════════
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285
286 #[test]
287 fn test_init_cleanup() {
288 assert_eq!(init_threads(), 0);
289 assert!(threads_initialized());
290 cleanup_threads();
291 assert!(!threads_initialized());
292 }
293
294 #[test]
295 fn test_lock_unlock_no_panic() {
296 lock_library();
297 unlock_library();
298 }
299}