feat(mt): move compression chunk loop into Rust

ZSTDMT_compressionJob kept the chunking kernel in C while Rust already owned
most of the MT pool and progress helpers. The loop mixed compression calls
with mutex and condition-variable publication, making it difficult to test
its exact final-chunk and partial-tail behavior independently.

Move only the chunk-processing kernel into Rust. It calls the existing C
compression entry points through an explicit ABI, reports the final block size
or error in a repr(C) result, and invokes a C progress callback after each
successful intermediate chunk. C retains worker setup, serial and raw-sequence
state, synchronization, error publication, cleanup, and final completion
reporting. Mock compressors cover empty, exact, partial, progress, and error
boundaries without mirroring private MT job state in Rust.

Test Plan:
- `cargo test --manifest-path rust/Cargo.toml --lib zstdmt_compress --
  --test-threads=1` -- passed (44 tests).
- `cargo clippy --manifest-path rust/Cargo.toml --lib -- -D warnings` -- passed.
- `cargo +nightly fmt --manifest-path rust/Cargo.toml -- --check` -- passed.
- `ZSTREAM_TESTTIME=-T2s make -B -C tests -j2 test-zstream` -- passed,
  including multi-threaded dictionary/checksum coverage.
This commit is contained in:
2026-07-18 18:46:13 +02:00
parent 1b85c430b1
commit 22bebab928
2 changed files with 368 additions and 30 deletions
+336 -1
View File
@@ -19,7 +19,7 @@ use std::os::raw::{c_int, c_uint, c_void};
use std::ptr;
use std::sync::Mutex;
use crate::errors::{ZstdErrorCode, ERROR};
use crate::errors::{ERR_isError, ZstdErrorCode, ERROR};
use crate::zstd_compress::ZSTD_frameProgression;
const ZSTDMT_JOBLOG_MAX: c_uint = if mem::size_of::<usize>() == 4 { 29 } else { 30 };
@@ -45,6 +45,146 @@ const PRIME8_BYTES: u64 = 0xCF1B_BCDC_B7A5_6463;
const ROLL_HASH_CHAR_OFFSET: u64 = 10;
const _: () = assert!(RSYNC_MIN_BLOCK_SIZE >= RSYNC_LENGTH);
type ZstdMtCompressFn =
unsafe extern "C" fn(*mut c_void, *mut c_void, usize, *const c_void, usize) -> usize;
pub type ZSTDMT_chunkProgressFn = unsafe extern "C" fn(*mut c_void, usize, usize);
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct ZSTDMT_chunkProcessResult {
pub error: usize,
pub lastBlockSize: usize,
}
#[cfg(not(test))]
unsafe extern "C" {
fn ZSTD_compressContinue_public(
cctx: *mut c_void,
dst: *mut c_void,
dst_capacity: usize,
src: *const c_void,
src_size: usize,
) -> usize;
fn ZSTD_compressEnd_public(
cctx: *mut c_void,
dst: *mut c_void,
dst_capacity: usize,
src: *const c_void,
src_size: usize,
) -> usize;
}
#[inline]
unsafe fn compress_job_chunks_with(
cctx: *mut c_void,
src: *const c_void,
src_size: usize,
dst: *mut c_void,
dst_capacity: usize,
chunk_size: usize,
last_job: c_uint,
progress_context: *mut c_void,
progress_callback: Option<ZSTDMT_chunkProgressFn>,
compress_continue: ZstdMtCompressFn,
compress_end: ZstdMtCompressFn,
) -> ZSTDMT_chunkProcessResult {
debug_assert!(chunk_size > 0);
debug_assert!(chunk_size.is_power_of_two());
let nb_chunks = src_size.div_ceil(chunk_size);
let mut input = src.cast::<u8>();
let mut produced = 0usize;
for chunk_number in 1..nb_chunks {
let c_size = unsafe {
compress_continue(
cctx,
dst.cast::<u8>().wrapping_add(produced).cast(),
dst_capacity.wrapping_sub(produced),
input.cast(),
chunk_size,
)
};
if ERR_isError(c_size) {
return ZSTDMT_chunkProcessResult {
error: c_size,
lastBlockSize: 0,
};
}
input = input.wrapping_add(chunk_size);
produced = produced.wrapping_add(c_size);
debug_assert!(produced < dst_capacity);
if let Some(callback) = progress_callback {
unsafe { callback(progress_context, c_size, chunk_size * chunk_number) };
}
}
if nb_chunks > 0 || last_job != 0 {
let last_block_size1 = src_size & (chunk_size - 1);
let last_block_size = if last_block_size1 == 0 && src_size >= chunk_size {
chunk_size
} else {
last_block_size1
};
let c_size = unsafe {
let dst = dst.cast::<u8>().wrapping_add(produced).cast();
let dst_capacity = dst_capacity.wrapping_sub(produced);
let compress = if last_job != 0 {
compress_end
} else {
compress_continue
};
compress(cctx, dst, dst_capacity, input.cast(), last_block_size)
};
if ERR_isError(c_size) {
return ZSTDMT_chunkProcessResult {
error: c_size,
lastBlockSize: 0,
};
}
return ZSTDMT_chunkProcessResult {
error: 0,
lastBlockSize: c_size,
};
}
ZSTDMT_chunkProcessResult {
error: 0,
lastBlockSize: 0,
}
}
#[cfg(not(test))]
#[no_mangle]
pub unsafe extern "C" fn ZSTDMT_rust_compressJobChunks(
cctx: *mut c_void,
src: *const c_void,
src_size: usize,
dst: *mut c_void,
dst_capacity: usize,
chunk_size: usize,
last_job: c_uint,
progress_context: *mut c_void,
progress_callback: Option<ZSTDMT_chunkProgressFn>,
) -> ZSTDMT_chunkProcessResult {
unsafe {
compress_job_chunks_with(
cctx,
src,
src_size,
dst,
dst_capacity,
chunk_size,
last_job,
progress_context,
progress_callback,
ZSTD_compressContinue_public,
ZSTD_compressEnd_public,
)
}
}
#[inline]
fn cycle_log(chain_log: c_uint, strategy: c_int) -> c_uint {
chain_log.wrapping_sub((strategy >= ZSTD_BTLAZY2) as c_uint)
@@ -1225,6 +1365,201 @@ mod tests {
static JOB_TABLE_FAIL_INIT: AtomicBool = AtomicBool::new(false);
static JOB_TABLE_TEST_LOCK: Mutex<()> = Mutex::new(());
struct MockChunkCompressor {
continue_inputs: Vec<usize>,
end_inputs: Vec<usize>,
continue_results: Vec<usize>,
end_results: Vec<usize>,
}
#[derive(Default)]
struct MockChunkProgress {
calls: Vec<(usize, usize)>,
}
unsafe extern "C" fn mock_compress_continue(
cctx: *mut c_void,
_dst: *mut c_void,
_dst_capacity: usize,
_src: *const c_void,
src_size: usize,
) -> usize {
let compressor = unsafe { &mut *cctx.cast::<MockChunkCompressor>() };
let call_number = compressor.continue_inputs.len();
compressor.continue_inputs.push(src_size);
compressor
.continue_results
.get(call_number)
.copied()
.unwrap_or(0)
}
unsafe extern "C" fn mock_compress_end(
cctx: *mut c_void,
_dst: *mut c_void,
_dst_capacity: usize,
_src: *const c_void,
src_size: usize,
) -> usize {
let compressor = unsafe { &mut *cctx.cast::<MockChunkCompressor>() };
let call_number = compressor.end_inputs.len();
compressor.end_inputs.push(src_size);
compressor
.end_results
.get(call_number)
.copied()
.unwrap_or(0)
}
unsafe extern "C" fn mock_chunk_progress(context: *mut c_void, c_size: usize, consumed: usize) {
let progress = unsafe { &mut *context.cast::<MockChunkProgress>() };
progress.calls.push((c_size, consumed));
}
fn run_mock_chunk_loop(
compressor: &mut MockChunkCompressor,
progress: &mut MockChunkProgress,
src_size: usize,
chunk_size: usize,
last_job: c_uint,
) -> ZSTDMT_chunkProcessResult {
let src = [0u8; 32];
let mut dst = [0u8; 64];
unsafe {
compress_job_chunks_with(
(compressor as *mut MockChunkCompressor).cast(),
src.as_ptr().cast(),
src_size,
dst.as_mut_ptr().cast(),
dst.len(),
chunk_size,
last_job,
(progress as *mut MockChunkProgress).cast(),
Some(mock_chunk_progress),
mock_compress_continue,
mock_compress_end,
)
}
}
#[test]
fn chunk_loop_handles_empty_jobs_without_compression() {
let mut compressor = MockChunkCompressor {
continue_inputs: Vec::new(),
end_inputs: Vec::new(),
continue_results: Vec::new(),
end_results: vec![9],
};
let mut progress = MockChunkProgress::default();
let result = run_mock_chunk_loop(&mut compressor, &mut progress, 0, 8, 0);
assert_eq!(result, ZSTDMT_chunkProcessResult::default());
assert!(compressor.continue_inputs.is_empty());
assert!(compressor.end_inputs.is_empty());
assert!(progress.calls.is_empty());
let result = run_mock_chunk_loop(&mut compressor, &mut progress, 0, 8, 1);
assert_eq!(result.error, 0);
assert_eq!(result.lastBlockSize, 9);
assert!(compressor.continue_inputs.is_empty());
assert_eq!(compressor.end_inputs, [0]);
assert!(progress.calls.is_empty());
}
#[test]
fn chunk_loop_handles_one_exact_chunk_and_last_job_dispatch() {
let mut compressor = MockChunkCompressor {
continue_inputs: Vec::new(),
end_inputs: Vec::new(),
continue_results: vec![4],
end_results: vec![6],
};
let mut progress = MockChunkProgress::default();
let result = run_mock_chunk_loop(&mut compressor, &mut progress, 8, 8, 0);
assert_eq!(result.lastBlockSize, 4);
assert_eq!(compressor.continue_inputs, [8]);
assert!(compressor.end_inputs.is_empty());
assert!(progress.calls.is_empty());
let result = run_mock_chunk_loop(&mut compressor, &mut progress, 8, 8, 1);
assert_eq!(result.lastBlockSize, 6);
assert_eq!(compressor.end_inputs, [8]);
assert!(progress.calls.is_empty());
}
#[test]
fn chunk_loop_handles_multiple_exact_chunks_and_orders_progress() {
let mut compressor = MockChunkCompressor {
continue_inputs: Vec::new(),
end_inputs: Vec::new(),
continue_results: vec![2, 3],
end_results: Vec::new(),
};
let mut progress = MockChunkProgress::default();
let result = run_mock_chunk_loop(&mut compressor, &mut progress, 16, 8, 0);
assert_eq!(result.lastBlockSize, 3);
assert_eq!(compressor.continue_inputs, [8, 8]);
assert!(compressor.end_inputs.is_empty());
assert_eq!(progress.calls, [(2, 8)]);
}
#[test]
fn chunk_loop_handles_partial_tail_and_final_dispatch() {
let mut compressor = MockChunkCompressor {
continue_inputs: Vec::new(),
end_inputs: Vec::new(),
continue_results: vec![2],
end_results: vec![5],
};
let mut progress = MockChunkProgress::default();
let result = run_mock_chunk_loop(&mut compressor, &mut progress, 11, 8, 1);
assert_eq!(result.lastBlockSize, 5);
assert_eq!(compressor.continue_inputs, [8]);
assert_eq!(compressor.end_inputs, [3]);
assert_eq!(progress.calls, [(2, 8)]);
}
#[test]
fn chunk_loop_propagates_intermediate_errors_after_prior_progress() {
let error = ERROR(ZstdErrorCode::DstSizeTooSmall);
let mut compressor = MockChunkCompressor {
continue_inputs: Vec::new(),
end_inputs: Vec::new(),
continue_results: vec![2, error],
end_results: vec![7],
};
let mut progress = MockChunkProgress::default();
let result = run_mock_chunk_loop(&mut compressor, &mut progress, 24, 8, 1);
assert_eq!(result.error, error);
assert_eq!(result.lastBlockSize, 0);
assert_eq!(compressor.continue_inputs, [8, 8]);
assert!(compressor.end_inputs.is_empty());
assert_eq!(progress.calls, [(2, 8)]);
}
#[test]
fn chunk_loop_propagates_final_errors_without_progress_callback() {
let error = ERROR(ZstdErrorCode::Generic);
let mut compressor = MockChunkCompressor {
continue_inputs: Vec::new(),
end_inputs: Vec::new(),
continue_results: Vec::new(),
end_results: vec![error],
};
let mut progress = MockChunkProgress::default();
let result = run_mock_chunk_loop(&mut compressor, &mut progress, 3, 8, 1);
assert_eq!(result.error, error);
assert_eq!(result.lastBlockSize, 0);
assert!(compressor.continue_inputs.is_empty());
assert_eq!(compressor.end_inputs, [3]);
assert!(progress.calls.is_empty());
}
unsafe extern "C" fn probe_job_table_init(
_job_table: *mut c_void,
_nb_jobs: c_uint,