Skip to main content

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
26use core::ffi::c_void;
27use core::sync::atomic::{AtomicBool, Ordering};
28use std::os::raw::c_int;
29
30/// Whether threading has been initialized.
31static THREADS_INITIALIZED: AtomicBool = AtomicBool::new(false);
32
33/// Initialize threading support.
34///
35/// # UPSTREAM-PARITY
36///
37/// ```c
38/// int xmlInitThreads(void);
39/// ```
40///
41/// Returns 0 on success. In modern libxml2, this is called automatically
42/// by `xmlInitParser`.
43pub fn init_threads() -> c_int {
44    THREADS_INITIALIZED.store(true, Ordering::Release);
45    0
46}
47
48/// Clean up threading support.
49///
50/// # UPSTREAM-PARITY
51///
52/// ```c
53/// void xmlCleanupThreads(void);
54/// ```
55pub fn cleanup_threads() {
56    THREADS_INITIALIZED.store(false, Ordering::Release);
57}
58
59/// Check whether threading has been initialized.
60pub fn threads_initialized() -> bool {
61    THREADS_INITIALIZED.load(Ordering::Acquire)
62}
63
64/// Lock the library (global mutex).
65///
66/// # UPSTREAM-PARITY
67///
68/// ```c
69/// void xmlLockLibrary(void);
70/// ```
71///
72/// In upstream libxml2, this locks a global mutex. In Rust, this is
73/// a no-op because Rust's type system prevents data races. However,
74/// for FFI safety with C callers that may manipulate shared state,
75/// a real mutex would be needed. This will be enhanced in Phase 2+.
76pub const fn lock_library() {
77    // Phase 1: no-op — Rust's type system handles data races for internal code.
78    // For C callers going through FFI, this is a best-effort approach.
79}
80
81/// Unlock the library.
82///
83/// # UPSTREAM-PARITY
84///
85/// ```c
86/// void xmlUnlockLibrary(void);
87/// ```
88pub const fn unlock_library() {
89    // Phase 1: no-op
90}
91
92/// Get the number of active threads (for compatibility).
93///
94/// Returns the number of active threads, or 0 if unknown.
95/// This is a compatibility stub — upstream doesn't expose this directly.
96pub const fn get_thread_count() -> c_int {
97    1
98}
99
100// ═══════════════════════════════════════════════════════════════════════════════
101// Mutex / recursive-mutex API (upstream threads.h)
102// ═══════════════════════════════════════════════════════════════════════════════
103//
104// `xmlMutexPtr`/`xmlRMutexPtr` are opaque handles. The candidate boxes a
105// parking_lot mutex (a `Mutex<()>` for the simple mutex, a reentrant
106// `ReentrantMutex` for the recursive mutex) so lock/unlock round-trips
107// through the FFI boundary with real exclusion semantics.
108//
109// The `RawMutex` trait import brings the manual `lock`/`unlock` methods
110// into scope for the raw-mutex lock/unlock used below.
111use parking_lot::lock_api::RawMutex as _;
112
113/// Create a simple mutex (upstream threads.h `xmlNewMutex`).
114///
115/// Returns an opaque handle (free with `xmlFreeMutex`), or NULL on
116/// allocation failure.
117pub fn new_mutex() -> *mut c_void {
118    let m = Box::new(parking_lot::Mutex::new(()));
119    Box::into_raw(m) as *mut c_void
120}
121
122/// Free a simple mutex (upstream threads.h `xmlFreeMutex`).
123///
124/// # SAFETY
125///
126/// - `tok` must be a handle from `xmlNewMutex` (or NULL), and must not be
127///   locked by any thread when freed.
128pub unsafe fn free_mutex(tok: *mut c_void) {
129    if !tok.is_null() {
130        drop(Box::from_raw(tok as *mut parking_lot::Mutex<()>));
131    }
132}
133
134/// Lock a simple mutex (upstream threads.h `xmlMutexLock`).
135///
136/// # SAFETY
137///
138/// - `tok` must be a handle from `xmlNewMutex` or NULL.
139pub unsafe fn mutex_lock(tok: *mut c_void) {
140    if tok.is_null() {
141        return;
142    }
143    // SAFETY: tok is a valid handle from xmlNewMutex. The raw lock blocks
144    // until the mutex is acquired and stays held until the matching
145    // mutex_unlock (parking_lot lock_api RawMutex manual API). The previous
146    // guard-based code dropped the guard immediately, providing no
147    // exclusion; the 11.1-Z seal fixed this to real lock semantics.
148    unsafe { (*(tok as *mut parking_lot::Mutex<()>)).raw().lock() };
149}
150
151/// Unlock a simple mutex (upstream threads.h `xmlMutexUnlock`).
152///
153/// # SAFETY
154///
155/// - `tok` must be a handle from `xmlNewMutex` or NULL, and must be
156///   locked by the calling thread.
157pub unsafe fn mutex_unlock(tok: *mut c_void) {
158    if tok.is_null() {
159        return;
160    }
161    // SAFETY: tok must be a handle from xmlNewMutex locked by this thread
162    // via mutex_lock; unlocking releases the raw mutex.
163    unsafe { (*(tok as *mut parking_lot::Mutex<()>)).raw().unlock() };
164}
165
166/// Create a recursive mutex (upstream threads.h `xmlNewRMutex`).
167///
168/// Returns an opaque handle (free with `xmlFreeRMutex`), or NULL on
169/// allocation failure.
170pub fn new_rmutex() -> *mut c_void {
171    let m = Box::new(parking_lot::ReentrantMutex::new(()));
172    Box::into_raw(m) as *mut c_void
173}
174
175/// Free a recursive mutex (upstream threads.h `xmlFreeRMutex`).
176///
177/// # SAFETY
178///
179/// - `tok` must be a handle from `xmlNewRMutex` (or NULL).
180pub unsafe fn free_rmutex(tok: *mut c_void) {
181    if !tok.is_null() {
182        drop(Box::from_raw(tok as *mut parking_lot::ReentrantMutex<()>));
183    }
184}
185
186/// Lock a recursive mutex (upstream threads.h `xmlRMutexLock`).
187///
188/// # SAFETY
189///
190/// - `tok` must be a handle from `xmlNewRMutex` or NULL.
191pub unsafe fn rmutex_lock(tok: *mut c_void) {
192    if tok.is_null() {
193        return;
194    }
195    // SAFETY: tok is a valid handle from xmlNewRMutex; reentrant locking is
196    // permitted, and the raw lock stays held until rmutex_unlock.
197    unsafe {
198        (*(tok as *mut parking_lot::ReentrantMutex<()>))
199            .raw()
200            .lock()
201    };
202}
203
204/// Unlock a recursive mutex (upstream threads.h `xmlRMutexUnlock`).
205///
206/// # SAFETY
207///
208/// - `tok` must be a handle from `xmlNewRMutex` or NULL.
209pub unsafe fn rmutex_unlock(tok: *mut c_void) {
210    if tok.is_null() {
211        return;
212    }
213    // SAFETY: tok must be a handle from xmlNewRMutex locked by this thread
214    // via rmutex_lock; unlocking releases the raw reentrant mutex.
215    unsafe {
216        (*(tok as *mut parking_lot::ReentrantMutex<()>))
217            .raw()
218            .unlock()
219    };
220}
221
222// ═══════════════════════════════════════════════════════════════════════════════
223// Tests
224// ═══════════════════════════════════════════════════════════════════════════════
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229
230    #[test]
231    fn test_init_cleanup() {
232        assert_eq!(init_threads(), 0);
233        assert!(threads_initialized());
234        cleanup_threads();
235        assert!(!threads_initialized());
236    }
237
238    #[test]
239    fn test_lock_unlock_no_panic() {
240        lock_library();
241        unlock_library();
242    }
243}