Skip to main content

libxml_rs/xml/memory/
mod.rs

1//! Memory management (§58, §85 Phase 1).
2//!
3//! Wraps the allocator hooks from `crate::abi::allocator` for use by the XML
4//! implementation modules. This module provides the internal Rust interface
5//! to the allocator system.
6//!
7//! # UPSTREAM-PARITY
8//!
9//! libxml2 exposes several memory management APIs:
10//!
11//! - `xmlMemSetup` / `xmlMemGet` — set/get custom allocator hooks
12//! - `xmlGcMemSetup` / `xmlGcMemGet` — GC-aware variants (now identical)
13//! - `xmlMalloc` / `xmlMallocAtomic` / `xmlRealloc` / `xmlFree` / `xmlMemStrdup`
14//! - `xmlMallocZero` / `xmlMallocAtomicZero` / `xmlReallocZero`
15//! - `xmlMemUsed` / `xmlMemBlocks` — debugging statistics
16//! - `xmlMemDisplay` / `xmlMemShow` — debugging output
17//! - `xmlInitMemory` / `xmlCleanupMemory` — lifecycle
18//!
19//! All of these are implemented in `crate::abi::allocator`. This module
20//! re-exports them for internal use.
21//!
22//! # Phase 1 status
23//!
24//! Complete — all memory functions delegate to the ABI allocator layer.
25//!
26//! # Upstream contract
27//!
28//! Mirrors upstream xmlmemory.c (SRC-LIBXML2-2.15.0-XMLMEMORY-C): xmlMemSetup
29//! / xmlMemGet / xmlGcMemSetup / xmlMemUsed / xmlMemBlocks / xmlMemDisplay /
30//! xmlMemShow and the xmlMalloc* family. The actual implementation lives in
31//! `crate::abi::allocator`; this module is the internal Rust interface.
32//!
33//! # Conceptual behavior
34//!
35//! There are two allocation planes, exactly as in upstream 2.15.0. The five
36//! exported variables (`xmlMalloc`, `xmlMallocAtomic`, `xmlRealloc`,
37//! `xmlFree`, `xmlMemStrdup`) are the hook system: their default bodies are
38//! plain libc `malloc`/`realloc`/`free`/`strdup` wrappers and are UNTRACKED —
39//! with the default installed `xmlMemUsed()`/`xmlMemBlocks()`/`xmlMemSize()`
40//! all return 0, byte-identical with the oracle (R-000178). `xmlMemSetup` /
41//! direct variable assignment re-route the hooks, and custom allocators
42//! bypass accounting entirely, matching upstream's debug-allocator-only block
43//! table. The debug-named surface (`xmlMemMalloc`/`xmlMemFree`/`xmlMemRealloc`/
44//! `xmlMemoryStrdup` and the `*Loc` variants) is the second plane: always
45//! libc-backed and tracked by the per-block registry (R-000131), which is
46//! what `xmlMemSize` returns sizes from for those blocks. The display entry
47//! points (`xmlMemDisplay`, `xmlMemDisplayLast`, `xmlMemShow`,
48//! `xmlMemoryDump`) are no-ops matching upstream 2.15.0, which removed that
49//! feature.
50//!
51//! # Ownership & safety invariants
52//!
53//! Ownership rule (atlas/OWNERSHIP_ATLAS.md): a pointer returned by an xml*
54//! allocator must be freed with xmlFree; a pointer from libc::calloc inside
55//! the engine is freed internally and never escapes. SAFETY: `xmlFree` on a
56//! foreign/unknown pointer is a plain libc `free` — the default free body
57//! does not consult the registry at all (the registry is only consulted by
58//! the debug-named `xmlMemFree`), exactly like upstream's default
59//! `xmlFree = free` (R-000178).
60//!
61//! # Historical quirks & epochs
62//!
63//! R-000178 (11.1-Z.3): the pre-Z.3 default allocator routed through Rust's
64//! global allocator with fabricated `Layout`s — invalid-layout UB under the
65//! Rust allocator contract; replaced with plain libc
66//! `malloc`/`realloc`/`free`/`strdup` (C allocation semantics; no layout
67//! exists). The pre-Z.3 claim that `xmlFree` on a foreign pointer was a
68//! "no-op removal from the registry" is obsolete: the default free is now
69//! untracked libc `free`. R-000131 (11.1-J): `xmlMemSize` returns the
70//! recorded size for debug-surface blocks and the `*Loc` variants
71//! accept-and-ignore file/line exactly like upstream 2.15.0's
72//! `ATTRIBUTE_UNUSED` parameters. R-000133 (11.1-H): the legacy debug names
73//! were declared-but-unexported and had to be implemented for the
74//! honest-header rule.
75//!
76//! # Deliberate oddities
77//!
78//! Deliberate oddities: the exported allocator entry points are DATA
79//! function-pointer globals matching the oracle ABI (R-000162: upstream
80//! exports them as data variables so the `xmlMalloc = custom` override can
81//! link), and since 11.1-Z.2 they are the single source of truth —
82//! `xmlMemSetup` assigns them and every internal allocation reads them
83//! through the `*Impl` indirection, so `xmlMemSetup` and direct
84//! `xmlMalloc = custom` assignment share one override mechanism (R-000176).
85//! The debug-named functions deliberately do NOT route through the variables
86//! (upstream's debug allocator is independent of the hooks).
87//!
88//! # Proving courts
89//!
90//! ABI-DATA, ALLOCATOR, GLOBAL-STATE and THREADING court families;
91//! ALLOCATOR-DEFAULT-001 (default-allocator contract: many sizes, zero-size,
92//! grow/shrink realloc, realloc-to-zero, failure, strdup, direct
93//! exported-variable calls, long churn, `xmlMemSize`/`xmlMemUsed`/
94//! `xmlMemBlocks` exactness — byte-identical with the oracle, R-000178);
95//! ALLOCATOR-HOOK (custom-hook differential, byte-identical); DATA-GLOBALS-001
96//! (allocator globals byte-identical); DSO-LOADER (every exported symbol
97//! resolved from the built DSO); and `cargo test --lib` (counts generated
98//! into atlas/TEST_COUNTS.json by tools/evidence/test_counts.py).
99//!
100//! # Tempting simplifications that would break parity
101//!
102//! A tempting simplification is routing all allocation through the Rust
103//! global allocator — the pre-Z.3 `std::alloc` fabricated-Layout approach was
104//! invalid-layout UB (R-000178), and any Rust-allocator route would break
105//! xmlMemSetup overrides and the exported xmlMalloc data-symbol ABI
106//! (R-000162). Do not make the default tracked: returning nonzero
107//! `xmlMemUsed`/`xmlMemBlocks` under the default diverges from the oracle's
108//! 0s. Do not restore the display dumps: upstream 2.15.0 removed them, so a
109//! per-block dump would diverge.
110
111pub use crate::abi::allocator::{
112    xmlFreeImpl, xmlInitMemory, xmlMallocAtomicImpl, xmlMallocAtomicZero, xmlMallocImpl,
113    xmlMallocZero, xmlMemBlocks, xmlMemDisplay, xmlMemGet, xmlMemSetup, xmlMemShow,
114    xmlMemStrdupImpl, xmlMemUsed, xmlReallocImpl, xmlReallocZero,
115};
116
117/// Initialize the memory subsystem.
118///
119/// Called during `xmlInitParser`. Returns 0 on success.
120pub const fn init_memory() -> i32 {
121    xmlInitMemory()
122}
123
124/// Clean up the memory subsystem.
125pub const fn cleanup_memory() {
126    // Phase 1: no cleanup needed for the default allocator.
127}
128
129// ═══════════════════════════════════════════════════════════════════════════════
130// Tests
131// ═══════════════════════════════════════════════════════════════════════════════
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    /// `xmlMallocImpl`/`xmlFreeImpl` round trip through the ABI allocator.
138    ///
139    /// # Safety
140    ///
141    /// - `ptr` is non-NULL (asserted) and allocator-owned, valid for 100
142    ///   bytes, and freed with `xmlFreeImpl` exactly once.
143    #[test]
144    fn test_memory_module_delegates_to_allocator() {
145        unsafe {
146            let ptr = xmlMallocImpl(100);
147            assert!(!ptr.is_null());
148            xmlFreeImpl(ptr);
149        }
150    }
151
152    /// `xmlMallocZero` returns zero-initialized allocator memory.
153    ///
154    /// # Safety
155    ///
156    /// - `ptr` is non-NULL (asserted) and allocator-owned, valid for 64
157    ///   zeroed bytes while the slice is read, and freed with
158    ///   `xmlFreeImpl` exactly once.
159    #[test]
160    fn test_memory_zero_alloc() {
161        unsafe {
162            let ptr = xmlMallocZero(64);
163            assert!(!ptr.is_null());
164            // Verify zero-initialized
165            let bytes = core::slice::from_raw_parts(ptr as *const u8, 64);
166            assert!(bytes.iter().all(|&b| b == 0));
167            xmlFreeImpl(ptr);
168        }
169    }
170}