Files
zstd-rs/rust/src/threading.rs
T
ddidderr 26b5e202ee feat(rust): port worker pool and pthread wrappers
Replace the common worker pool and debug pthread wrappers with ABI-compatible
Rust implementations. The pool preserves bounded-queue, resize, drain-on-free,
custom-allocator, and no-thread behavior while C supplies only the build-time
multithreading configuration bit.

This moves runtime support without changing C tests or public headers. Codec
and CLI implementations remain C while their dependencies are migrated.

Test Plan:
- cargo fmt --check
- cargo test --all-targets
- cargo clippy --all-targets -- -D warnings
- cargo build --release
- make -C tests poolTests
- ./tests/poolTests
- compile pool and threading shims with -Werror in MT and no-thread modes

Refs: rust/README.md
2026-07-10 20:05:35 +02:00

283 lines
7.8 KiB
Rust

use std::os::raw::{c_int, c_void};
use std::ptr;
#[cfg(unix)]
use libc::{
pthread_cond_destroy, pthread_cond_init, pthread_cond_t, pthread_condattr_t, pthread_create,
pthread_join, pthread_mutex_destroy, pthread_mutex_init, pthread_mutex_t, pthread_mutexattr_t,
pthread_t,
};
/// Allocating mutex wrapper used by threading.h when `DEBUGLEVEL >= 1`.
#[cfg(unix)]
#[no_mangle]
pub unsafe extern "C" fn ZSTD_pthread_mutex_init(
mutex: *mut *mut pthread_mutex_t,
attr: *const pthread_mutexattr_t,
) -> c_int {
if mutex.is_null() {
return libc::EINVAL;
}
let allocation =
unsafe { libc::malloc(std::mem::size_of::<pthread_mutex_t>()) }.cast::<pthread_mutex_t>();
unsafe { mutex.write(allocation) };
if allocation.is_null() {
return 1;
}
unsafe { pthread_mutex_init(allocation, attr) }
}
/// Destroying mutex wrapper used by threading.h when `DEBUGLEVEL >= 1`.
#[cfg(unix)]
#[no_mangle]
pub unsafe extern "C" fn ZSTD_pthread_mutex_destroy(mutex: *mut *mut pthread_mutex_t) -> c_int {
if mutex.is_null() {
return libc::EINVAL;
}
let allocation = unsafe { mutex.read() };
if allocation.is_null() {
return 0;
}
let result = unsafe { pthread_mutex_destroy(allocation) };
unsafe { libc::free(allocation.cast()) };
result
}
/// Allocating condition-variable wrapper used by threading.h when
/// `DEBUGLEVEL >= 1`.
#[cfg(unix)]
#[no_mangle]
pub unsafe extern "C" fn ZSTD_pthread_cond_init(
cond: *mut *mut pthread_cond_t,
attr: *const pthread_condattr_t,
) -> c_int {
if cond.is_null() {
return libc::EINVAL;
}
let allocation =
unsafe { libc::malloc(std::mem::size_of::<pthread_cond_t>()) }.cast::<pthread_cond_t>();
unsafe { cond.write(allocation) };
if allocation.is_null() {
return 1;
}
unsafe { pthread_cond_init(allocation, attr) }
}
/// Destroying condition-variable wrapper used by threading.h when
/// `DEBUGLEVEL >= 1`.
#[cfg(unix)]
#[no_mangle]
pub unsafe extern "C" fn ZSTD_pthread_cond_destroy(cond: *mut *mut pthread_cond_t) -> c_int {
if cond.is_null() {
return libc::EINVAL;
}
let allocation = unsafe { cond.read() };
if allocation.is_null() {
return 0;
}
let result = unsafe { pthread_cond_destroy(allocation) };
unsafe { libc::free(allocation.cast()) };
result
}
/// POSIX-compatible thread creation entry point. On POSIX this symbol is
/// normally bypassed by threading.h's macro, but keeping it ABI-correct makes
/// the Rust object usable by configurations which reference the wrapper.
#[cfg(unix)]
#[no_mangle]
pub unsafe extern "C" fn ZSTD_pthread_create(
thread: *mut pthread_t,
attr: *const libc::pthread_attr_t,
start_routine: extern "C" fn(*mut c_void) -> *mut c_void,
arg: *mut c_void,
) -> c_int {
if thread.is_null() {
return -1;
}
unsafe { pthread_create(thread, attr, start_routine, arg) }
}
/// POSIX-compatible join which intentionally discards the callback result,
/// matching `ZSTD_pthread_join` in threading.h.
#[cfg(unix)]
#[no_mangle]
pub unsafe extern "C" fn ZSTD_pthread_join(thread: pthread_t) -> c_int {
unsafe { pthread_join(thread, ptr::null_mut()) }
}
#[cfg(windows)]
type Handle = *mut c_void;
#[cfg(windows)]
struct WindowsThreadParams {
start_routine: extern "C" fn(*mut c_void) -> *mut c_void,
arg: *mut c_void,
}
#[cfg(windows)]
unsafe impl Send for WindowsThreadParams {}
#[cfg(windows)]
unsafe extern "system" fn windows_worker(arg: *mut c_void) -> u32 {
let params = unsafe { Box::from_raw(arg.cast::<WindowsThreadParams>()) };
(params.start_routine)(params.arg);
0
}
#[cfg(windows)]
#[link(name = "msvcrt")]
unsafe extern "C" {
fn _beginthreadex(
security: *mut c_void,
stack_size: u32,
start_address: Option<unsafe extern "system" fn(*mut c_void) -> u32>,
arglist: *mut c_void,
initflag: u32,
thread_address: *mut u32,
) -> usize;
fn _errno() -> *mut c_int;
}
#[cfg(windows)]
#[link(name = "kernel32")]
unsafe extern "system" {
fn WaitForSingleObject(handle: Handle, milliseconds: u32) -> u32;
fn CloseHandle(handle: Handle) -> c_int;
fn GetLastError() -> u32;
}
#[cfg(windows)]
#[no_mangle]
pub unsafe extern "C" fn ZSTD_pthread_create(
thread: *mut Handle,
_unused: *const c_void,
start_routine: extern "C" fn(*mut c_void) -> *mut c_void,
arg: *mut c_void,
) -> c_int {
if thread.is_null() {
return -1;
}
unsafe { thread.write(ptr::null_mut()) };
let params = Box::new(WindowsThreadParams { start_routine, arg });
let params = Box::into_raw(params);
let handle = unsafe {
_beginthreadex(
ptr::null_mut(),
0,
Some(windows_worker),
params.cast(),
0,
ptr::null_mut(),
)
};
if handle == 0 {
unsafe { drop(Box::from_raw(params)) };
let errno = unsafe { _errno() };
return if errno.is_null() {
-1
} else {
unsafe { *errno }
};
}
unsafe { thread.write(handle as Handle) };
0
}
#[cfg(windows)]
#[no_mangle]
pub unsafe extern "C" fn ZSTD_pthread_join(thread: Handle) -> c_int {
const INFINITE: u32 = u32::MAX;
const WAIT_OBJECT_0: u32 = 0;
const WAIT_ABANDONED: u32 = 0x80;
if thread.is_null() {
return 0;
}
let result = unsafe { WaitForSingleObject(thread, INFINITE) };
unsafe { CloseHandle(thread) };
match result {
WAIT_OBJECT_0 => 0,
WAIT_ABANDONED => libc::EINVAL,
_ => unsafe { GetLastError() as c_int },
}
}
#[cfg(all(test, unix))]
mod tests {
use super::*;
use std::mem::MaybeUninit;
use std::sync::atomic::{AtomicUsize, Ordering};
static THREAD_CALLS: AtomicUsize = AtomicUsize::new(0);
extern "C" fn thread_main(arg: *mut c_void) -> *mut c_void {
THREAD_CALLS.fetch_add(1, Ordering::Relaxed);
arg
}
#[test]
fn debug_mutex_wrapper_allocates_initializes_and_frees() {
let mut mutex: *mut pthread_mutex_t = ptr::null_mut();
unsafe {
assert_eq!(ZSTD_pthread_mutex_init(&mut mutex, ptr::null()), 0);
assert!(!mutex.is_null());
assert_eq!(libc::pthread_mutex_lock(mutex), 0);
assert_eq!(libc::pthread_mutex_unlock(mutex), 0);
assert_eq!(ZSTD_pthread_mutex_destroy(&mut mutex), 0);
}
}
#[test]
fn debug_condition_wrapper_allocates_initializes_and_frees() {
let mut cond: *mut pthread_cond_t = ptr::null_mut();
unsafe {
assert_eq!(ZSTD_pthread_cond_init(&mut cond, ptr::null()), 0);
assert!(!cond.is_null());
assert_eq!(ZSTD_pthread_cond_destroy(&mut cond), 0);
}
}
#[test]
fn pthread_wrapper_runs_and_joins_callback() {
THREAD_CALLS.store(0, Ordering::Relaxed);
let mut thread = MaybeUninit::<pthread_t>::uninit();
unsafe {
assert_eq!(
ZSTD_pthread_create(
thread.as_mut_ptr(),
ptr::null(),
thread_main,
ptr::null_mut()
),
0
);
assert_eq!(ZSTD_pthread_join(thread.assume_init()), 0);
}
assert_eq!(THREAD_CALLS.load(Ordering::Relaxed), 1);
}
#[test]
fn null_output_pointers_are_rejected() {
unsafe {
assert_eq!(
ZSTD_pthread_mutex_init(ptr::null_mut(), ptr::null()),
libc::EINVAL
);
assert_eq!(
ZSTD_pthread_cond_init(ptr::null_mut(), ptr::null()),
libc::EINVAL
);
assert_eq!(
ZSTD_pthread_create(ptr::null_mut(), ptr::null(), thread_main, ptr::null_mut()),
-1
);
}
}
}