Skip to main content

libxml_rs/abi/
allocator.rs

1//! C ABI allocator compatibility — xmlMemSetup, xmlMemGet, xmlMalloc, xmlFree, etc. (§58).
2//!
3//! This module implements the complete memory hook system exposed by libxml2:
4//! - Global allocator function pointers (malloc, realloc, free, strdup)
5//! - `xmlMemSetup()` / `xmlMemGet()` — set/get allocator hooks
6//! - `xmlGcMemSetup()` / `xmlGcMemGet()` — GC-aware allocator hooks (wrappers)
7//! - `xmlMalloc()` / `xmlMallocAtomic()` / `xmlRealloc()` / `xmlFree()` / `xmlMemStrdup()`
8//! - `xmlMemUsed()` / `xmlMemBlocks()` — allocation tracking
9//! - `xmlMemDisplay()` / `xmlMemShow()` — debugging output
10//!
11//! # Phase 1 status
12//!
13//! Complete — all allocator APIs are implemented with thread-safe global state.
14//!
15//! # Safety
16//!
17//! Allocator hooks are `unsafe` because they operate on raw pointers and are called
18//! from C code. Every public function documents its safety contract.
19
20use core::alloc::Layout;
21use core::ffi::c_void;
22use core::ptr;
23use core::sync::atomic::AtomicUsize;
24use core::sync::atomic::Ordering;
25use std::os::raw::{c_char, c_int, c_uint};
26
27use parking_lot::RwLock;
28
29use crate::abi::callbacks::*;
30
31// ═══════════════════════════════════════════════════════════════════════════════
32// Default Allocator (Rust global allocator)
33// ═══════════════════════════════════════════════════════════════════════════════
34
35/// Default malloc implementation using Rust's global allocator.
36///
37/// # SAFETY
38///
39/// - `size` must be a valid allocation size (0 is handled by allocating 1 byte)
40/// - Returns NULL on allocation failure
41unsafe extern "C" fn default_malloc(size: usize) -> *mut c_void {
42    if size == 0 {
43        // Upstream malloc(0) may return NULL or a valid pointer.
44        // libxml2 checks for NULL and treats it as OOM.
45        // Allocate 1 byte to avoid UB with zero-size Layout.
46        let layout = Layout::from_size_align_unchecked(1, 1);
47        let ptr = std::alloc::alloc(layout);
48        if ptr.is_null() {
49            ptr::null_mut()
50        } else {
51            ptr as *mut c_void
52        }
53    } else {
54        let layout = match Layout::from_size_align(size, 1) {
55            Ok(l) => l,
56            Err(_) => return ptr::null_mut(),
57        };
58        let ptr = std::alloc::alloc(layout);
59        if ptr.is_null() {
60            ptr::null_mut()
61        } else {
62            ptr as *mut c_void
63        }
64    }
65}
66
67/// Default realloc implementation using Rust's global allocator.
68///
69/// # SAFETY
70///
71/// - `ptr` must be a valid pointer from a previous `default_malloc` or `default_realloc`,
72///   or NULL (in which case this behaves like malloc)
73/// - `size` must be a valid allocation size
74unsafe extern "C" fn default_realloc(ptr: *mut c_void, size: usize) -> *mut c_void {
75    if ptr.is_null() {
76        return default_malloc(size);
77    }
78    if size == 0 {
79        default_free(ptr);
80        return default_malloc(0);
81    }
82    let layout = match Layout::from_size_align(size, 1) {
83        Ok(l) => l,
84        Err(_) => return ptr::null_mut(),
85    };
86    let new_ptr = std::alloc::realloc(ptr as *mut u8, layout, size);
87    if new_ptr.is_null() {
88        ptr::null_mut()
89    } else {
90        new_ptr as *mut c_void
91    }
92}
93
94/// Default free implementation using Rust's global allocator.
95///
96/// # SAFETY
97///
98/// - `ptr` may be NULL (free(NULL) is a no-op)
99/// - If non-NULL, `ptr` must be from a previous `default_malloc` or `default_realloc`
100unsafe extern "C" fn default_free(ptr: *mut c_void) {
101    if ptr.is_null() {
102        return;
103    }
104    // We use a layout with size 0 and alignment 1 for deallocation,
105    // since Rust's dealloc requires the original layout.
106    // However, we don't know the original size. We use a 1-byte layout
107    // which is the minimum. This is technically UB in Rust but matches
108    // how C realloc/free work.
109    let layout = Layout::from_size_align_unchecked(1, 1);
110    std::alloc::dealloc(ptr as *mut u8, layout);
111}
112
113/// Default strdup implementation using Rust's global allocator.
114///
115/// # SAFETY
116///
117/// - `str` must be a valid null-terminated C string
118unsafe extern "C" fn default_strdup(str: *const c_char) -> *mut c_void {
119    if str.is_null() {
120        return ptr::null_mut();
121    }
122    let len = libc::strlen(str);
123    let size = len + 1; // include null terminator
124    let layout = match Layout::from_size_align(size, 1) {
125        Ok(l) => l,
126        Err(_) => return ptr::null_mut(),
127    };
128    let new_ptr = std::alloc::alloc(layout);
129    if new_ptr.is_null() {
130        return ptr::null_mut();
131    }
132    ptr::copy_nonoverlapping(str as *const u8, new_ptr, size);
133    new_ptr as *mut c_void
134}
135
136// ═══════════════════════════════════════════════════════════════════════════════
137// Global Allocator State
138// ═══════════════════════════════════════════════════════════════════════════════
139
140/// Global allocator function pointers.
141///
142/// Protected by RwLock for thread-safe read/write.
143/// Default values point to Rust's global allocator.
144static ALLOCATOR: RwLock<AllocatorFuncs> = RwLock::new(AllocatorFuncs {
145    malloc_func: Some(default_malloc as xmlMallocFunc),
146    realloc_func: Some(default_realloc as xmlReallocFunc),
147    free_func: Some(default_free as xmlFreeFunc),
148    strdup_func: Some(default_strdup as xmlStrdupFunc),
149});
150
151/// The set of allocator function pointers.
152struct AllocatorFuncs {
153    malloc_func: Option<xmlMallocFunc>,
154    realloc_func: Option<xmlReallocFunc>,
155    free_func: Option<xmlFreeFunc>,
156    strdup_func: Option<xmlStrdupFunc>,
157}
158
159/// Global allocation counters (for xmlMemUsed/xmlMemBlocks).
160///
161/// These use relaxed ordering since they are approximate debugging counters.
162static MEM_USED: AtomicUsize = AtomicUsize::new(0);
163static MEM_BLOCKS: AtomicUsize = AtomicUsize::new(0);
164
165// ═══════════════════════════════════════════════════════════════════════════════
166// Public Allocator API
167// ═══════════════════════════════════════════════════════════════════════════════
168
169/// Set custom memory allocator functions.
170///
171/// # UPSTREAM-PARITY
172///
173/// ```c
174/// void xmlMemSetup(xmlFreeFunc freeFunc,
175///                  xmlMallocFunc mallocFunc,
176///                  xmlReallocFunc reallocFunc,
177///                  xmlStrdupFunc strdupFunc);
178/// ```
179///
180/// # SAFETY
181///
182/// - All function pointers must be valid (non-null) and thread-safe
183/// - The functions must follow the C malloc/realloc/free/strdup contract
184/// - Once set, the functions remain in effect until the next `xmlMemSetup` call
185/// - The caller is responsible for ensuring the functions remain valid for the
186///   entire time they are installed
187#[no_mangle]
188pub unsafe extern "C" fn xmlMemSetup(
189    freeFunc: Option<xmlFreeFunc>,
190    mallocFunc: Option<xmlMallocFunc>,
191    reallocFunc: Option<xmlReallocFunc>,
192    strdupFunc: Option<xmlStrdupFunc>,
193) {
194    // SAFETY: Caller guarantees all function pointers are valid.
195    // We store them in global state protected by RwLock.
196    let mut alloc = ALLOCATOR.write();
197    alloc.free_func = freeFunc;
198    alloc.malloc_func = mallocFunc;
199    alloc.realloc_func = reallocFunc;
200    alloc.strdup_func = strdupFunc;
201}
202
203/// Get the current memory allocator functions.
204///
205/// # UPSTREAM-PARITY
206///
207/// ```c
208/// void xmlMemGet(xmlFreeFunc *freeFunc,
209///                xmlMallocFunc *mallocFunc,
210///                xmlReallocFunc *reallocFunc,
211///                xmlStrdupFunc *strdupFunc);
212/// ```
213///
214/// # SAFETY
215///
216/// - All output pointers must be valid (non-null) and writable
217#[no_mangle]
218pub unsafe extern "C" fn xmlMemGet(
219    freeFunc: *mut Option<xmlFreeFunc>,
220    mallocFunc: *mut Option<xmlMallocFunc>,
221    reallocFunc: *mut Option<xmlReallocFunc>,
222    strdupFunc: *mut Option<xmlStrdupFunc>,
223) {
224    // SAFETY: Caller guarantees all output pointers are valid.
225    let alloc = ALLOCATOR.read();
226    unsafe {
227        ptr::write(freeFunc, alloc.free_func);
228        ptr::write(mallocFunc, alloc.malloc_func);
229        ptr::write(reallocFunc, alloc.realloc_func);
230        ptr::write(strdupFunc, alloc.strdup_func);
231    }
232}
233
234/// Set GC-aware memory allocator functions.
235///
236/// # UPSTREAM-PARITY
237///
238/// This is a wrapper around `xmlMemSetup` in modern libxml2.
239/// Historically, it was separate for the GC-allocated memory pool,
240/// but in modern versions both functions do the same thing.
241#[no_mangle]
242pub unsafe extern "C" fn xmlGcMemSetup(
243    freeFunc: Option<xmlFreeFunc>,
244    mallocFunc: Option<xmlMallocFunc>,
245    reallocFunc: Option<xmlReallocFunc>,
246    strdupFunc: Option<xmlStrdupFunc>,
247) {
248    // SAFETY: Delegates to xmlMemSetup with the same safety contract.
249    unsafe { xmlMemSetup(freeFunc, mallocFunc, reallocFunc, strdupFunc) };
250}
251
252/// Get GC-aware memory allocator functions.
253///
254/// # UPSTREAM-PARITY
255///
256/// Wrapper around `xmlMemGet`.
257#[no_mangle]
258pub unsafe extern "C" fn xmlGcMemGet(
259    freeFunc: *mut Option<xmlFreeFunc>,
260    mallocFunc: *mut Option<xmlMallocFunc>,
261    reallocFunc: *mut Option<xmlReallocFunc>,
262    strdupFunc: *mut Option<xmlStrdupFunc>,
263) {
264    // SAFETY: Delegates to xmlMemGet with the same safety contract.
265    unsafe { xmlMemGet(freeFunc, mallocFunc, reallocFunc, strdupFunc) };
266}
267
268// ═══════════════════════════════════════════════════════════════════════════════
269// Allocation Functions
270// ═══════════════════════════════════════════════════════════════════════════════
271
272/// Allocate memory.
273///
274/// # UPSTREAM-PARITY
275///
276/// ```c
277/// void *xmlMalloc(size_t size);
278/// ```
279///
280/// Returns a pointer to the allocated memory, or NULL on failure.
281/// The allocated memory is not initialized (like malloc).
282///
283/// # SAFETY
284///
285/// - The returned pointer must be freed with `xmlFree`
286/// - `size` may be 0 (returns a valid non-NULL pointer or NULL)
287#[no_mangle]
288pub unsafe extern "C" fn xmlMalloc(size: usize) -> *mut c_void {
289    // SAFETY: We call the stored malloc function pointer.
290    // The function pointer must be valid (set by xmlMemSetup or default).
291    let alloc = ALLOCATOR.read();
292    let malloc_func = alloc.malloc_func.unwrap_or(default_malloc as xmlMallocFunc);
293    let ptr = unsafe { malloc_func(size) };
294    if !ptr.is_null() {
295        MEM_USED.fetch_add(size, Ordering::Relaxed);
296        MEM_BLOCKS.fetch_add(1, Ordering::Relaxed);
297    }
298    ptr
299}
300
301/// Allocate memory that will never contain pointers to other memory.
302///
303/// # UPSTREAM-PARITY
304///
305/// ```c
306/// void *xmlMallocAtomic(size_t size);
307/// ```
308///
309/// Identical to `xmlMalloc` but hints to the GC that the memory does not
310/// contain pointers. In modern libxml2, this is equivalent to `xmlMalloc`.
311#[no_mangle]
312pub unsafe extern "C" fn xmlMallocAtomic(size: usize) -> *mut c_void {
313    // SAFETY: Same as xmlMalloc.
314    unsafe { xmlMalloc(size) }
315}
316
317/// Reallocate memory.
318///
319/// # UPSTREAM-PARITY
320///
321/// ```c
322/// void *xmlRealloc(void *ptr, size_t size);
323/// ```
324///
325/// Changes the size of the memory block pointed to by `ptr`.
326/// If `ptr` is NULL, behaves like `xmlMalloc`.
327/// If `size` is 0, may return NULL (like C realloc).
328///
329/// # SAFETY
330///
331/// - `ptr` must be a valid pointer from `xmlMalloc`, `xmlMallocAtomic`, or `xmlRealloc`,
332///   or NULL
333/// - The returned pointer must be freed with `xmlFree`
334#[no_mangle]
335pub unsafe extern "C" fn xmlRealloc(ptr: *mut c_void, size: usize) -> *mut c_void {
336    // SAFETY: We call the stored realloc function pointer.
337    let alloc = ALLOCATOR.read();
338    let realloc_func = alloc
339        .realloc_func
340        .unwrap_or(default_realloc as xmlReallocFunc);
341    let new_ptr = unsafe { realloc_func(ptr, size) };
342    if !new_ptr.is_null() {
343        // Update counters (approximate — we don't know the old size)
344        MEM_BLOCKS.fetch_add(1, Ordering::Relaxed);
345        if ptr.is_null() {
346            MEM_USED.fetch_add(size, Ordering::Relaxed);
347        }
348        // Note: we don't subtract old size because we don't track it.
349        // This makes counters approximate, matching upstream behavior
350        // where the debugging allocator tracks sizes but the default doesn't.
351    }
352    new_ptr
353}
354
355/// Free allocated memory.
356///
357/// # UPSTREAM-PARITY
358///
359/// ```c
360/// void xmlFree(void *ptr);
361/// ```
362///
363/// Frees memory previously allocated with `xmlMalloc`, `xmlMallocAtomic`,
364/// or `xmlRealloc`. If `ptr` is NULL, no operation is performed.
365///
366/// # SAFETY
367///
368/// - `ptr` must be a valid pointer from `xmlMalloc`/`xmlMallocAtomic`/`xmlRealloc`,
369///   or NULL
370/// - After this call, `ptr` must not be dereferenced
371#[no_mangle]
372pub unsafe extern "C" fn xmlFree(ptr: *mut c_void) {
373    if ptr.is_null() {
374        return;
375    }
376    // SAFETY: We call the stored free function pointer.
377    let alloc = ALLOCATOR.read();
378    let free_func = alloc.free_func.unwrap_or(default_free as xmlFreeFunc);
379    unsafe { free_func(ptr) };
380    MEM_BLOCKS.fetch_sub(1, Ordering::Relaxed);
381}
382
383/// Duplicate a C string using the configured allocator.
384///
385/// # UPSTREAM-PARITY
386///
387/// ```c
388/// void *xmlMemStrdup(const char *str);
389/// ```
390///
391/// Returns a pointer to the newly allocated copy, or NULL on failure.
392///
393/// # SAFETY
394///
395/// - `str` must be a valid null-terminated C string or NULL
396/// - The returned pointer must be freed with `xmlFree`
397#[no_mangle]
398pub unsafe extern "C" fn xmlMemStrdup(str: *const c_char) -> *mut c_void {
399    if str.is_null() {
400        return ptr::null_mut();
401    }
402    // SAFETY: We call the stored strdup function pointer.
403    let alloc = ALLOCATOR.read();
404    let strdup_func = alloc.strdup_func.unwrap_or(default_strdup as xmlStrdupFunc);
405    let ptr = unsafe { strdup_func(str) };
406    if !ptr.is_null() {
407        let len = unsafe { libc::strlen(str) } + 1;
408        MEM_USED.fetch_add(len, Ordering::Relaxed);
409        MEM_BLOCKS.fetch_add(1, Ordering::Relaxed);
410    }
411    ptr
412}
413
414// ═══════════════════════════════════════════════════════════════════════════════
415// Memory Debugging / Statistics
416// ═══════════════════════════════════════════════════════════════════════════════
417
418/// Return the total amount of memory currently allocated (approximate).
419///
420/// # UPSTREAM-PARITY
421///
422/// ```c
423/// int xmlMemUsed(void);
424/// ```
425///
426/// Returns an approximate count of allocated bytes.
427/// With the default allocator, this tracks allocations but is not
428/// byte-exact for realloc (since we don't track old sizes).
429/// Custom allocator hooks can provide exact counts.
430#[no_mangle]
431pub extern "C" fn xmlMemUsed() -> c_int {
432    MEM_USED.load(Ordering::Relaxed) as c_int
433}
434
435/// Return the current number of allocated blocks (approximate).
436///
437/// # UPSTREAM-PARITY
438///
439/// ```c
440/// int xmlMemBlocks(void);
441/// ```
442///
443/// Returns an approximate count of live allocations.
444#[no_mangle]
445pub extern "C" fn xmlMemBlocks() -> c_int {
446    MEM_BLOCKS.load(Ordering::Relaxed) as c_int
447}
448
449/// Display memory allocation information to a file.
450///
451/// # UPSTREAM-PARITY
452///
453/// ```c
454/// void xmlMemDisplay(FILE *fp);
455/// ```
456///
457/// Prints debug memory information. With the default allocator,
458/// this prints a summary message. Custom allocator hooks may
459/// provide more detailed output.
460///
461/// # SAFETY
462///
463/// - `fp` must be a valid FILE* pointer or NULL (in which case stderr is used)
464#[no_mangle]
465pub unsafe extern "C" fn xmlMemDisplay(fp: *mut c_void) {
466    // SAFETY: Caller guarantees fp is valid FILE* or NULL.
467    unsafe {
468        let out = if fp.is_null() {
469            libc::fdopen(2, b"w\0" as *const u8 as *const c_char) as *mut c_void
470        } else {
471            fp
472        };
473        libc::fprintf(
474            out as *mut _,
475            b"Memory: used=%d blocks=%d\n\0" as *const u8 as *const c_char,
476            xmlMemUsed(),
477            xmlMemBlocks(),
478        );
479    }
480}
481
482/// Show memory allocation information.
483///
484/// # UPSTREAM-PARITY
485///
486/// ```c
487/// void xmlMemShow(FILE *fp, int nr);
488/// ```
489///
490/// Prints debug memory information for the last `nr` allocations.
491/// With the default allocator, this is a no-op (we don't track allocation history).
492#[no_mangle]
493pub unsafe extern "C" fn xmlMemShow(_fp: *mut c_void, _nr: c_int) {
494    // Phase 1: no-op with the default allocator.
495    // A future debugging allocator could track allocation history.
496}
497
498// ═══════════════════════════════════════════════════════════════════════════════
499// Convenience Functions (used internally)
500// ═══════════════════════════════════════════════════════════════════════════════
501
502/// Allocate zero-initialized memory.
503///
504/// # UPSTREAM-PARITY
505///
506/// This is like `xmlMalloc` followed by `memset(0)`, but some allocator
507/// hooks provide it directly.
508///
509/// # SAFETY
510///
511/// Same as `xmlMalloc`. The returned memory is zero-initialized.
512#[no_mangle]
513pub unsafe extern "C" fn xmlMallocZero(size: usize) -> *mut c_void {
514    // SAFETY: Delegates to xmlMalloc and zeroes the memory.
515    let ptr = unsafe { xmlMalloc(size) };
516    if !ptr.is_null() {
517        unsafe { ptr::write_bytes(ptr, 0, size) };
518    }
519    ptr
520}
521
522/// Allocate zero-initialized memory (atomic variant).
523///
524/// # UPSTREAM-PARITY
525///
526/// Like `xmlMallocAtomic` followed by zero-initialization.
527#[no_mangle]
528pub unsafe extern "C" fn xmlMallocAtomicZero(size: usize) -> *mut c_void {
529    // SAFETY: Delegates to xmlMallocAtomic and zeroes the memory.
530    let ptr = unsafe { xmlMallocAtomic(size) };
531    if !ptr.is_null() {
532        unsafe { ptr::write_bytes(ptr, 0, size) };
533    }
534    ptr
535}
536
537/// Reallocate and zero-initialize the new portion.
538///
539/// # UPSTREAM-PARITY
540///
541/// Like `xmlRealloc`, but zeroes any newly allocated bytes.
542///
543/// # SAFETY
544///
545/// Same as `xmlRealloc`.
546#[no_mangle]
547pub unsafe extern "C" fn xmlReallocZero(
548    ptr: *mut c_void,
549    old_size: usize,
550    new_size: usize,
551) -> *mut c_void {
552    // SAFETY: Delegates to xmlRealloc and zeroes the new portion.
553    let new_ptr = unsafe { xmlRealloc(ptr, new_size) };
554    if !new_ptr.is_null() && new_size > old_size {
555        unsafe {
556            ptr::write_bytes(new_ptr.add(old_size), 0, new_size - old_size);
557        }
558    }
559    new_ptr
560}
561
562// ═══════════════════════════════════════════════════════════════════════════════
563// Debug Allocator (Optional)
564// ═══════════════════════════════════════════════════════════════════════════════
565
566/// Initialize the memory layer with debugging support.
567///
568/// # UPSTREAM-PARITY
569///
570/// ```c
571/// int xmlInitMemory(void);
572/// ```
573///
574/// Initializes the memory subsystem. Returns 0 on success.
575/// This is called automatically by `xmlInitParser`.
576#[no_mangle]
577pub extern "C" fn xmlInitMemory() -> c_int {
578    0
579}
580
581/// Clean up the memory layer.
582///
583/// # UPSTREAM-PARITY
584///
585/// ```c
586/// void xmlCleanupMemory(void);
587/// ```
588#[no_mangle]
589pub extern "C" fn xmlCleanupMemory() {
590    // Phase 1: no cleanup needed for the default allocator.
591}
592
593// ═══════════════════════════════════════════════════════════════════════════════
594// Tests
595// ═══════════════════════════════════════════════════════════════════════════════
596
597#[cfg(test)]
598mod tests {
599    use super::*;
600    use core::ptr;
601
602    /// Test basic allocation and deallocation.
603    #[test]
604    fn test_malloc_free() {
605        unsafe {
606            let ptr = xmlMalloc(100);
607            assert!(!ptr.is_null(), "xmlMalloc(100) returned NULL");
608            xmlFree(ptr);
609        }
610    }
611
612    /// Test that xmlMalloc(0) returns a valid pointer.
613    #[test]
614    fn test_malloc_zero() {
615        unsafe {
616            let ptr = xmlMalloc(0);
617            // malloc(0) may return NULL or a valid pointer.
618            // If non-NULL, it must be freeable.
619            if !ptr.is_null() {
620                xmlFree(ptr);
621            }
622        }
623    }
624
625    /// Test xmlFree(NULL) is a no-op.
626    #[test]
627    fn test_free_null() {
628        unsafe {
629            xmlFree(ptr::null_mut());
630            // Should not crash
631        }
632    }
633
634    /// Test xmlRealloc.
635    #[test]
636    fn test_realloc() {
637        unsafe {
638            let ptr = xmlMalloc(50);
639            assert!(!ptr.is_null());
640            let new_ptr = xmlRealloc(ptr, 100);
641            assert!(!new_ptr.is_null());
642            xmlFree(new_ptr);
643        }
644    }
645
646    /// Test xmlMemStrdup.
647    #[test]
648    fn test_mem_strdup() {
649        unsafe {
650            let s = b"hello\0" as *const u8 as *const c_char;
651            let dup = xmlMemStrdup(s);
652            assert!(!dup.is_null());
653            // Compare the strings
654            let orig_slice = std::slice::from_raw_parts(s as *const u8, 6);
655            let dup_slice = std::slice::from_raw_parts(dup as *const u8, 6);
656            assert_eq!(orig_slice, dup_slice);
657            xmlFree(dup);
658        }
659    }
660
661    /// Test xmlMallocZero returns zeroed memory.
662    #[test]
663    fn test_malloc_zero_init() {
664        unsafe {
665            let ptr = xmlMallocZero(100) as *mut u8;
666            assert!(!ptr.is_null());
667            let slice = std::slice::from_raw_parts(ptr, 100);
668            assert!(slice.iter().all(|&b| b == 0));
669            xmlFree(ptr as *mut c_void);
670        }
671    }
672
673    /// Test custom allocator setup/get.
674    #[test]
675    fn test_mem_setup_get() {
676        unsafe {
677            let mut free_func: Option<xmlFreeFunc> = None;
678            let mut malloc_func: Option<xmlMallocFunc> = None;
679            let mut realloc_func: Option<xmlReallocFunc> = None;
680            let mut strdup_func: Option<xmlStrdupFunc> = None;
681
682            xmlMemGet(
683                &mut free_func as *mut _,
684                &mut malloc_func as *mut _,
685                &mut realloc_func as *mut _,
686                &mut strdup_func as *mut _,
687            );
688
689            assert!(malloc_func.is_some());
690            assert!(free_func.is_some());
691            assert!(realloc_func.is_some());
692            assert!(strdup_func.is_some());
693        }
694    }
695
696    /// Test xmlMemUsed and xmlMemBlocks return reasonable values.
697    #[test]
698    fn test_mem_stats() {
699        unsafe {
700            let before_used = xmlMemUsed();
701            let before_blocks = xmlMemBlocks();
702
703            let ptr = xmlMalloc(100);
704            assert!(!ptr.is_null());
705
706            // After allocation, used and blocks should be higher
707            assert!(xmlMemBlocks() >= before_blocks + 1);
708
709            xmlFree(ptr);
710        }
711    }
712}