feat(compress): port multithreaded resource pools

Move the reusable buffer and compression-context pools behind a Rust
implementation with a narrow C ABI adapter. The scheduler, job table, serial
LDM state, and stream orchestration remain in C until their private layouts
are ported.

Keep context types opaque across Rust modules so the pool bridge does not
depend on private C layout declarations. This also keeps the existing custom
allocator and pool replacement contracts intact.

Test Plan:
- RUSTC_WRAPPER= CARGO_BUILD_RUSTC_WRAPPER= cargo clippy --manifest-path
  rust/Cargo.toml --all-targets -- -D warnings
- RUSTC_WRAPPER= CARGO_BUILD_RUSTC_WRAPPER= cargo test --manifest-path
  rust/Cargo.toml zstdmt_compress
- make -B -C lib lib-mt
- git diff --cached --check
This commit is contained in:
2026-07-12 10:13:41 +02:00
parent e75574690b
commit 45cb1f509e
4 changed files with 681 additions and 179 deletions
+2 -9
View File
@@ -596,15 +596,8 @@ fn best_snapshot(slot: &BestSlot) -> BestSnapshot {
}
}
#[repr(C)]
struct ZSTD_CCtx {
_private: [u8; 0],
}
#[repr(C)]
struct ZSTD_CDict {
_private: [u8; 0],
}
type ZSTD_CCtx = c_void;
type ZSTD_CDict = c_void;
unsafe extern "C" {
fn ZDICT_finalizeDictionary(
+2
View File
@@ -63,3 +63,5 @@ pub mod zstd_opt;
pub mod zstd_opt_tree;
#[cfg(feature = "compression")]
pub mod zstd_presplit;
#[cfg(feature = "compression")]
pub mod zstdmt_compress;
+604
View File
@@ -0,0 +1,604 @@
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
#![allow(clippy::missing_safety_doc)]
#![allow(clippy::too_many_arguments)]
//! Rust-owned resource pools used by the multithreaded compressor.
//!
//! The job table, serial LDM state, and streaming state still use private C
//! layouts. `zstdmt_compress.c` therefore keeps those parts and projects only
//! the allocation pools into this module. The pool entry points below are a
//! narrow C ABI: buffers and `ZSTD_CCtx *` values remain opaque to Rust, while
//! allocation, reuse, expansion, and synchronization are Rust-owned.
use std::mem::{self, MaybeUninit};
use std::os::raw::{c_uint, c_void};
use std::ptr;
use std::sync::Mutex;
type ZstdAllocFunction = unsafe extern "C" fn(*mut c_void, usize) -> *mut c_void;
type ZstdFreeFunction = unsafe extern "C" fn(*mut c_void, *mut c_void);
/// ABI-compatible representation of `ZSTD_customMem` from `zstd.h`.
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ZstdCustomMem {
pub customAlloc: Option<ZstdAllocFunction>,
pub customFree: Option<ZstdFreeFunction>,
pub opaque: *mut c_void,
}
/// ABI-compatible buffer returned to the C adapter.
#[repr(C)]
#[derive(Clone, Copy, Default)]
pub struct ZstdMtBuffer {
pub start: *mut c_void,
pub capacity: usize,
}
#[derive(Default)]
struct BufferPoolState {
buffer_size: usize,
nb_buffers: usize,
}
/// The object is allocated with the caller's `ZSTD_customMem`; its mutex
/// protects all mutable pool state and the raw reusable-buffer array.
pub struct RustBufferPool {
custom_mem: ZstdCustomMem,
total_buffers: usize,
buffers: *mut MaybeUninit<ZstdMtBuffer>,
state: Mutex<BufferPoolState>,
}
// The raw array is only accessed while `state` is held. The pool itself is
// passed between C worker threads as an opaque pointer.
unsafe impl Send for RustBufferPool {}
unsafe impl Sync for RustBufferPool {}
/// The `ZSTD_CCtx *` values are created and destroyed by the existing C API;
/// Rust owns only the synchronized reusable-pointer pool.
pub struct RustCCtxPool {
custom_mem: ZstdCustomMem,
total_cctx: usize,
cctxs: *mut MaybeUninit<*mut c_void>,
state: Mutex<usize>,
}
unsafe impl Send for RustCCtxPool {}
unsafe impl Sync for RustCCtxPool {}
unsafe extern "C" {
fn ZSTD_createCCtx_advanced(custom_mem: ZstdCustomMem) -> *mut c_void;
fn ZSTD_freeCCtx(cctx: *mut c_void) -> usize;
fn ZSTD_sizeof_CCtx(cctx: *const c_void) -> usize;
}
unsafe fn custom_calloc(size: usize, custom_mem: ZstdCustomMem) -> *mut c_void {
if let Some(alloc) = custom_mem.customAlloc {
let allocation = unsafe { alloc(custom_mem.opaque, size) };
if !allocation.is_null() {
unsafe { ptr::write_bytes(allocation, 0, size) };
}
allocation
} else {
unsafe { libc::calloc(1, size) }
}
}
unsafe fn custom_malloc(size: usize, custom_mem: ZstdCustomMem) -> *mut c_void {
if let Some(alloc) = custom_mem.customAlloc {
unsafe { alloc(custom_mem.opaque, size) }
} else {
unsafe { libc::malloc(size) }
}
}
unsafe fn custom_free(allocation: *mut c_void, custom_mem: ZstdCustomMem) {
if allocation.is_null() {
return;
}
if let Some(free) = custom_mem.customFree {
unsafe { free(custom_mem.opaque, allocation) };
} else {
unsafe { libc::free(allocation) };
}
}
fn checked_array_size<T>(len: usize) -> Option<usize> {
mem::size_of::<T>().checked_mul(len)
}
unsafe fn create_buffer_pool(
max_nb_buffers: usize,
custom_mem: ZstdCustomMem,
) -> *mut RustBufferPool {
if max_nb_buffers == 0 {
return ptr::null_mut();
}
let Some(buffer_bytes) = checked_array_size::<MaybeUninit<ZstdMtBuffer>>(max_nb_buffers) else {
return ptr::null_mut();
};
let pool = unsafe { custom_calloc(mem::size_of::<RustBufferPool>(), custom_mem) }
.cast::<RustBufferPool>();
if pool.is_null() {
return ptr::null_mut();
}
let buffers =
unsafe { custom_calloc(buffer_bytes, custom_mem) }.cast::<MaybeUninit<ZstdMtBuffer>>();
if buffers.is_null() {
unsafe { custom_free(pool.cast(), custom_mem) };
return ptr::null_mut();
}
unsafe {
pool.write(RustBufferPool {
custom_mem,
total_buffers: max_nb_buffers,
buffers,
state: Mutex::new(BufferPoolState {
buffer_size: 64 << 10,
nb_buffers: 0,
}),
});
}
pool
}
unsafe fn destroy_buffer_pool(pool: *mut RustBufferPool) {
if pool.is_null() {
return;
}
let custom_mem = unsafe { (*pool).custom_mem };
let buffers = unsafe { (*pool).buffers };
let total_buffers = unsafe { (*pool).total_buffers };
for index in 0..total_buffers {
let buffer = unsafe { buffers.add(index).read().assume_init() };
unsafe { custom_free(buffer.start, custom_mem) };
}
unsafe { ptr::drop_in_place(pool) };
unsafe {
custom_free(buffers.cast(), custom_mem);
custom_free(pool.cast(), custom_mem);
}
}
#[no_mangle]
pub unsafe extern "C" fn ZSTDMT_rust_buffer_pool_create(
max_nb_buffers: c_uint,
custom_mem: ZstdCustomMem,
) -> *mut RustBufferPool {
unsafe { create_buffer_pool(max_nb_buffers as usize, custom_mem) }
}
#[no_mangle]
pub unsafe extern "C" fn ZSTDMT_rust_buffer_pool_free(pool: *mut RustBufferPool) {
unsafe { destroy_buffer_pool(pool) }
}
#[no_mangle]
pub unsafe extern "C" fn ZSTDMT_rust_buffer_pool_sizeof(pool: *const RustBufferPool) -> usize {
if pool.is_null() {
return 0;
}
let pool_ref = unsafe { &*pool };
let _state = pool_ref
.state
.lock()
.unwrap_or_else(|error| error.into_inner());
let mut total_buffer_size = 0usize;
for index in 0..pool_ref.total_buffers {
let buffer = unsafe { pool_ref.buffers.add(index).read().assume_init() };
total_buffer_size = total_buffer_size.saturating_add(buffer.capacity);
}
mem::size_of::<RustBufferPool>()
.saturating_add(
pool_ref
.total_buffers
.saturating_mul(mem::size_of::<MaybeUninit<ZstdMtBuffer>>()),
)
.saturating_add(total_buffer_size)
}
#[no_mangle]
pub unsafe extern "C" fn ZSTDMT_rust_buffer_pool_set_size(
pool: *mut RustBufferPool,
buffer_size: usize,
) {
if pool.is_null() {
return;
}
let pool_ref = unsafe { &*pool };
let mut state = pool_ref
.state
.lock()
.unwrap_or_else(|error| error.into_inner());
state.buffer_size = buffer_size;
}
#[no_mangle]
pub unsafe extern "C" fn ZSTDMT_rust_buffer_pool_expand(
pool: *mut RustBufferPool,
max_nb_buffers: c_uint,
) -> *mut RustBufferPool {
if pool.is_null() {
return ptr::null_mut();
}
let max_nb_buffers = max_nb_buffers as usize;
let pool_ref = unsafe { &*pool };
let (total_buffers, buffer_size, custom_mem) = {
let state = pool_ref
.state
.lock()
.unwrap_or_else(|error| error.into_inner());
(
pool_ref.total_buffers,
state.buffer_size,
pool_ref.custom_mem,
)
};
if total_buffers >= max_nb_buffers {
return pool;
}
// This matches the original resize contract: the old pool is consumed
// before creating the larger replacement.
unsafe { destroy_buffer_pool(pool) };
let replacement = unsafe { create_buffer_pool(max_nb_buffers, custom_mem) };
if !replacement.is_null() {
unsafe { ZSTDMT_rust_buffer_pool_set_size(replacement, buffer_size) };
}
replacement
}
#[no_mangle]
pub unsafe extern "C" fn ZSTDMT_rust_buffer_pool_get(pool: *mut RustBufferPool) -> ZstdMtBuffer {
if pool.is_null() {
return ZstdMtBuffer::default();
}
let pool_ref = unsafe { &*pool };
let (buffer_size, reusable) = {
let mut state = pool_ref
.state
.lock()
.unwrap_or_else(|error| error.into_inner());
if state.buffer_size == 0 {
return ZstdMtBuffer::default();
}
if state.nb_buffers == 0 {
(state.buffer_size, ZstdMtBuffer::default())
} else {
state.nb_buffers -= 1;
let index = state.nb_buffers;
let buffer = unsafe { pool_ref.buffers.add(index).read().assume_init() };
unsafe {
pool_ref
.buffers
.add(index)
.write(MaybeUninit::new(ZstdMtBuffer::default()));
}
(state.buffer_size, buffer)
}
};
if !reusable.start.is_null()
&& reusable.capacity >= buffer_size
&& (reusable.capacity >> 3) <= buffer_size
{
return reusable;
}
if !reusable.start.is_null() {
unsafe { custom_free(reusable.start, pool_ref.custom_mem) };
}
let start = unsafe { custom_malloc(buffer_size, pool_ref.custom_mem) };
ZstdMtBuffer {
start,
capacity: if start.is_null() { 0 } else { buffer_size },
}
}
#[no_mangle]
pub unsafe extern "C" fn ZSTDMT_rust_buffer_pool_release(
pool: *mut RustBufferPool,
buffer: ZstdMtBuffer,
) {
if pool.is_null() || buffer.start.is_null() {
return;
}
let pool_ref = unsafe { &*pool };
let mut state = pool_ref
.state
.lock()
.unwrap_or_else(|error| error.into_inner());
if state.nb_buffers < pool_ref.total_buffers {
let index = state.nb_buffers;
unsafe {
pool_ref.buffers.add(index).write(MaybeUninit::new(buffer));
}
state.nb_buffers += 1;
return;
}
drop(state);
unsafe { custom_free(buffer.start, pool_ref.custom_mem) };
}
#[no_mangle]
pub unsafe extern "C" fn ZSTDMT_rust_buffer_pool_resize(
pool: *mut RustBufferPool,
buffer: ZstdMtBuffer,
) -> ZstdMtBuffer {
if pool.is_null() || buffer.start.is_null() {
return buffer;
}
let pool_ref = unsafe { &*pool };
let buffer_size = {
let state = pool_ref
.state
.lock()
.unwrap_or_else(|error| error.into_inner());
state.buffer_size
};
if buffer.capacity >= buffer_size {
return buffer;
}
let start = unsafe { custom_malloc(buffer_size, pool_ref.custom_mem) };
if start.is_null() {
return buffer;
}
unsafe {
ptr::copy_nonoverlapping(
buffer.start.cast::<u8>(),
start.cast::<u8>(),
buffer.capacity,
);
custom_free(buffer.start, pool_ref.custom_mem);
}
ZstdMtBuffer {
start,
capacity: buffer_size,
}
}
unsafe fn create_cctx_pool(nb_workers: usize, custom_mem: ZstdCustomMem) -> *mut RustCCtxPool {
if nb_workers == 0 {
return ptr::null_mut();
}
let Some(cctx_bytes) = checked_array_size::<MaybeUninit<*mut c_void>>(nb_workers) else {
return ptr::null_mut();
};
let pool =
unsafe { custom_calloc(mem::size_of::<RustCCtxPool>(), custom_mem) }.cast::<RustCCtxPool>();
if pool.is_null() {
return ptr::null_mut();
}
let cctxs = unsafe { custom_calloc(cctx_bytes, custom_mem) }.cast::<MaybeUninit<*mut c_void>>();
if cctxs.is_null() {
unsafe { custom_free(pool.cast(), custom_mem) };
return ptr::null_mut();
}
let first = unsafe { ZSTD_createCCtx_advanced(custom_mem) };
if first.is_null() {
unsafe {
custom_free(cctxs.cast(), custom_mem);
custom_free(pool.cast(), custom_mem);
}
return ptr::null_mut();
}
unsafe { cctxs.write(MaybeUninit::new(first)) };
unsafe {
pool.write(RustCCtxPool {
custom_mem,
total_cctx: nb_workers,
cctxs,
state: Mutex::new(1),
});
}
pool
}
unsafe fn destroy_cctx_pool(pool: *mut RustCCtxPool) {
if pool.is_null() {
return;
}
let custom_mem = unsafe { (*pool).custom_mem };
let cctxs = unsafe { (*pool).cctxs };
let total_cctx = unsafe { (*pool).total_cctx };
for index in 0..total_cctx {
let cctx = unsafe { cctxs.add(index).read().assume_init() };
unsafe { ZSTD_freeCCtx(cctx) };
}
unsafe { ptr::drop_in_place(pool) };
unsafe {
custom_free(cctxs.cast(), custom_mem);
custom_free(pool.cast(), custom_mem);
}
}
#[no_mangle]
pub unsafe extern "C" fn ZSTDMT_rust_cctx_pool_create(
nb_workers: c_uint,
custom_mem: ZstdCustomMem,
) -> *mut RustCCtxPool {
unsafe { create_cctx_pool(nb_workers as usize, custom_mem) }
}
#[no_mangle]
pub unsafe extern "C" fn ZSTDMT_rust_cctx_pool_free(pool: *mut RustCCtxPool) {
unsafe { destroy_cctx_pool(pool) }
}
#[no_mangle]
pub unsafe extern "C" fn ZSTDMT_rust_cctx_pool_sizeof(pool: *const RustCCtxPool) -> usize {
if pool.is_null() {
return 0;
}
let pool_ref = unsafe { &*pool };
let _state = pool_ref
.state
.lock()
.unwrap_or_else(|error| error.into_inner());
let mut total_cctx_size = 0usize;
for index in 0..pool_ref.total_cctx {
let cctx = unsafe { pool_ref.cctxs.add(index).read().assume_init() };
total_cctx_size =
total_cctx_size.saturating_add(unsafe { ZSTD_sizeof_CCtx(cctx.cast_const()) });
}
mem::size_of::<RustCCtxPool>()
.saturating_add(
pool_ref
.total_cctx
.saturating_mul(mem::size_of::<MaybeUninit<*mut c_void>>()),
)
.saturating_add(total_cctx_size)
}
#[no_mangle]
pub unsafe extern "C" fn ZSTDMT_rust_cctx_pool_expand(
pool: *mut RustCCtxPool,
nb_workers: c_uint,
) -> *mut RustCCtxPool {
if pool.is_null() {
return ptr::null_mut();
}
let nb_workers = nb_workers as usize;
let pool_ref = unsafe { &*pool };
if pool_ref.total_cctx >= nb_workers {
return pool;
}
let custom_mem = pool_ref.custom_mem;
unsafe { destroy_cctx_pool(pool) };
unsafe { create_cctx_pool(nb_workers, custom_mem) }
}
#[no_mangle]
pub unsafe extern "C" fn ZSTDMT_rust_cctx_pool_get(pool: *mut RustCCtxPool) -> *mut c_void {
if pool.is_null() {
return ptr::null_mut();
}
let pool_ref = unsafe { &*pool };
let cctx = {
let mut available = pool_ref
.state
.lock()
.unwrap_or_else(|error| error.into_inner());
if *available == 0 {
ptr::null_mut()
} else {
*available -= 1;
let index = *available;
unsafe { pool_ref.cctxs.add(index).read().assume_init() }
}
};
if !cctx.is_null() {
cctx
} else {
unsafe { ZSTD_createCCtx_advanced(pool_ref.custom_mem) }
}
}
#[no_mangle]
pub unsafe extern "C" fn ZSTDMT_rust_cctx_pool_release(pool: *mut RustCCtxPool, cctx: *mut c_void) {
if pool.is_null() || cctx.is_null() {
return;
}
let pool_ref = unsafe { &*pool };
let mut available = pool_ref
.state
.lock()
.unwrap_or_else(|error| error.into_inner());
if *available < pool_ref.total_cctx {
let index = *available;
unsafe {
pool_ref.cctxs.add(index).write(MaybeUninit::new(cctx));
}
*available += 1;
return;
}
drop(available);
unsafe { ZSTD_freeCCtx(cctx) };
}
#[cfg(test)]
mod tests {
use super::*;
const DEFAULT_MEM: ZstdCustomMem = ZstdCustomMem {
customAlloc: None,
customFree: None,
opaque: ptr::null_mut(),
};
#[test]
fn buffer_pool_reuses_and_resizes_buffers() {
let pool = unsafe { ZSTDMT_rust_buffer_pool_create(2, DEFAULT_MEM) };
assert!(!pool.is_null());
let first = unsafe { ZSTDMT_rust_buffer_pool_get(pool) };
assert!(!first.start.is_null());
assert_eq!(first.capacity, 64 << 10);
unsafe { ZSTDMT_rust_buffer_pool_release(pool, first) };
let reused = unsafe { ZSTDMT_rust_buffer_pool_get(pool) };
assert_eq!(reused.start, first.start);
assert_eq!(reused.capacity, first.capacity);
unsafe { ZSTDMT_rust_buffer_pool_release(pool, reused) };
unsafe { ZSTDMT_rust_buffer_pool_set_size(pool, 128 << 10) };
let resized = unsafe { ZSTDMT_rust_buffer_pool_get(pool) };
assert_eq!(resized.capacity, 128 << 10);
unsafe {
ZSTDMT_rust_buffer_pool_release(pool, resized);
ZSTDMT_rust_buffer_pool_free(pool);
}
}
#[test]
fn zero_size_pool_does_not_allocate_sequence_storage() {
let pool = unsafe { ZSTDMT_rust_buffer_pool_create(1, DEFAULT_MEM) };
assert!(!pool.is_null());
unsafe { ZSTDMT_rust_buffer_pool_set_size(pool, 0) };
let buffer = unsafe { ZSTDMT_rust_buffer_pool_get(pool) };
assert!(buffer.start.is_null());
assert_eq!(buffer.capacity, 0);
unsafe { ZSTDMT_rust_buffer_pool_free(pool) };
}
#[test]
fn expansion_preserves_requested_buffer_size() {
let pool = unsafe { ZSTDMT_rust_buffer_pool_create(1, DEFAULT_MEM) };
assert!(!pool.is_null());
unsafe { ZSTDMT_rust_buffer_pool_set_size(pool, 96 << 10) };
let expanded = unsafe { ZSTDMT_rust_buffer_pool_expand(pool, 3) };
assert!(!expanded.is_null());
let buffer = unsafe { ZSTDMT_rust_buffer_pool_get(expanded) };
assert_eq!(buffer.capacity, 96 << 10);
unsafe {
ZSTDMT_rust_buffer_pool_release(expanded, buffer);
ZSTDMT_rust_buffer_pool_free(expanded);
}
}
#[test]
fn resize_preserves_existing_buffer_contents() {
let pool = unsafe { ZSTDMT_rust_buffer_pool_create(1, DEFAULT_MEM) };
assert!(!pool.is_null());
let buffer = unsafe { ZSTDMT_rust_buffer_pool_get(pool) };
assert!(!buffer.start.is_null());
let sample = b"multithreaded-buffer-pool";
unsafe {
ptr::copy_nonoverlapping(sample.as_ptr(), buffer.start.cast(), sample.len());
ZSTDMT_rust_buffer_pool_set_size(pool, buffer.capacity + 1);
}
let resized = unsafe { ZSTDMT_rust_buffer_pool_resize(pool, buffer) };
assert_eq!(resized.capacity, buffer.capacity + 1);
let contents =
unsafe { std::slice::from_raw_parts(resized.start.cast::<u8>(), sample.len()) };
assert_eq!(contents, sample);
unsafe {
ZSTDMT_rust_buffer_pool_release(pool, resized);
ZSTDMT_rust_buffer_pool_free(pool);
}
}
}