feat(mt): move output publication into Rust

Move the bounded multithreaded output-publication kernel into Rust.  Rust now
calculates the flush amount, copies the selected job bytes, and returns the
updated output and job offsets through an explicit C-layout result.  C retains
mutex and condition-variable handling, checksum insertion, job retirement,
progress accounting, and the terminal state machine.

The Rust helper validates the scalar bounds before copying and has focused
coverage for empty output space, offset partial flushes, repeated flushes,
complete flushes, sentinels, and invalid bounds.

Test Plan:
- cargo test --manifest-path rust/Cargo.toml --lib zstdmt_compress -- --test-threads=1
- cargo clippy --manifest-path rust/Cargo.toml --lib -- -D warnings
- cargo +nightly fmt --manifest-path rust/Cargo.toml -- --check (blocked by concurrent fileio worker formatting its uncommitted file)
- ZSTREAM_TESTTIME=-T2s make -B -C tests -j2 test-zstream (worker)
- git diff --cached --check
This commit is contained in:
2026-07-18 19:00:22 +02:00
parent 428dfbc4f5
commit fb6d6d3219
2 changed files with 252 additions and 9 deletions
+228
View File
@@ -56,6 +56,73 @@ pub struct ZSTDMT_chunkProcessResult {
pub lastBlockSize: usize,
}
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct ZSTDMT_flushPublicationResult {
pub status: c_int,
pub toFlush: usize,
pub outputPos: usize,
pub dstFlushed: usize,
}
const FLUSH_PUBLICATION_OK: c_int = 0;
const FLUSH_PUBLICATION_INVALID_BOUNDS: c_int = 1;
#[inline]
fn invalid_flush_publication(
output_pos: usize,
dst_flushed: usize,
) -> ZSTDMT_flushPublicationResult {
ZSTDMT_flushPublicationResult {
status: FLUSH_PUBLICATION_INVALID_BOUNDS,
toFlush: 0,
outputPos: output_pos,
dstFlushed: dst_flushed,
}
}
#[inline]
unsafe fn publish_job_output_with(
output_dst: *mut c_void,
output_size: usize,
output_pos: usize,
job_dst: *const c_void,
job_capacity: usize,
c_size: usize,
dst_flushed: usize,
) -> ZSTDMT_flushPublicationResult {
let Some(output_available) = output_size.checked_sub(output_pos) else {
return invalid_flush_publication(output_pos, dst_flushed);
};
let Some(job_available) = c_size.checked_sub(dst_flushed) else {
return invalid_flush_publication(output_pos, dst_flushed);
};
if c_size > job_capacity {
return invalid_flush_publication(output_pos, dst_flushed);
}
let to_flush = output_available.min(job_available);
if to_flush > 0 {
if output_dst.is_null() || job_dst.is_null() {
return invalid_flush_publication(output_pos, dst_flushed);
}
unsafe {
ptr::copy_nonoverlapping(
job_dst.cast::<u8>().add(dst_flushed),
output_dst.cast::<u8>().add(output_pos),
to_flush,
);
}
}
ZSTDMT_flushPublicationResult {
status: FLUSH_PUBLICATION_OK,
toFlush: to_flush,
outputPos: output_pos + to_flush,
dstFlushed: dst_flushed + to_flush,
}
}
#[cfg(not(test))]
unsafe extern "C" {
fn ZSTD_compressContinue_public(
@@ -185,6 +252,30 @@ pub unsafe extern "C" fn ZSTDMT_rust_compressJobChunks(
}
}
#[cfg(not(test))]
#[no_mangle]
pub unsafe extern "C" fn ZSTDMT_rust_publishJobOutput(
output_dst: *mut c_void,
output_size: usize,
output_pos: usize,
job_dst: *const c_void,
job_capacity: usize,
c_size: usize,
dst_flushed: usize,
) -> ZSTDMT_flushPublicationResult {
unsafe {
publish_job_output_with(
output_dst,
output_size,
output_pos,
job_dst,
job_capacity,
c_size,
dst_flushed,
)
}
}
#[inline]
fn cycle_log(chain_log: c_uint, strategy: c_int) -> c_uint {
chain_log.wrapping_sub((strategy >= ZSTD_BTLAZY2) as c_uint)
@@ -1442,6 +1533,143 @@ mod tests {
}
}
fn run_output_publication(
output: &mut [u8],
output_size: usize,
output_pos: usize,
job: &[u8],
job_capacity: usize,
c_size: usize,
dst_flushed: usize,
) -> ZSTDMT_flushPublicationResult {
unsafe {
publish_job_output_with(
output.as_mut_ptr().cast(),
output_size,
output_pos,
job.as_ptr().cast(),
job_capacity,
c_size,
dst_flushed,
)
}
}
#[test]
fn output_publication_handles_no_output_space() {
let mut output = [0xa5; 8];
let original = output;
let job = [1, 2, 3, 4];
let output_size = output.len();
let result = run_output_publication(
&mut output,
output_size,
output_size,
&job,
job.len(),
job.len(),
0,
);
assert_eq!(result.status, FLUSH_PUBLICATION_OK);
assert_eq!(result.toFlush, 0);
assert_eq!(result.outputPos, output.len());
assert_eq!(result.dstFlushed, 0);
assert_eq!(output, original);
}
#[test]
fn output_publication_handles_partial_flush_with_offsets() {
let mut output = [0xa5; 10];
let job = [0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17];
let result = run_output_publication(&mut output, 9, 2, &job, job.len(), 6, 1);
assert_eq!(result.status, FLUSH_PUBLICATION_OK);
assert_eq!(result.toFlush, 5);
assert_eq!(result.outputPos, 7);
assert_eq!(result.dstFlushed, 6);
assert_eq!(&output[..2], &[0xa5, 0xa5]);
assert_eq!(&output[2..7], &job[1..6]);
assert_eq!(&output[7..], &[0xa5, 0xa5, 0xa5]);
}
#[test]
fn output_publication_handles_complete_flush() {
let mut output = [0xa5; 8];
let job = [0x20, 0x21, 0x22, 0x23, 0x24, 0x25];
let output_size = output.len();
let result = run_output_publication(&mut output, output_size, 1, &job, job.len(), 6, 2);
assert_eq!(result.status, FLUSH_PUBLICATION_OK);
assert_eq!(result.toFlush, 4);
assert_eq!(result.outputPos, 5);
assert_eq!(result.dstFlushed, 6);
assert_eq!(&output[1..5], &job[2..6]);
assert_eq!(&output[..1], &[0xa5]);
assert_eq!(&output[5..], &[0xa5, 0xa5, 0xa5]);
}
#[test]
fn output_publication_handles_repeated_partial_flushes() {
let mut output = [0xa5; 8];
let job = [0x30, 0x31, 0x32, 0x33, 0x34, 0x35];
let first = run_output_publication(&mut output, 3, 0, &job, job.len(), 6, 0);
let second = run_output_publication(
&mut output,
6,
first.outputPos,
&job,
job.len(),
6,
first.dstFlushed,
);
assert_eq!(first.status, FLUSH_PUBLICATION_OK);
assert_eq!(first.toFlush, 3);
assert_eq!(first.outputPos, 3);
assert_eq!(first.dstFlushed, 3);
assert_eq!(second.status, FLUSH_PUBLICATION_OK);
assert_eq!(second.toFlush, 3);
assert_eq!(second.outputPos, 6);
assert_eq!(second.dstFlushed, 6);
assert_eq!(&output[..6], &job);
assert_eq!(&output[6..], &[0xa5, 0xa5]);
}
#[test]
fn output_publication_does_not_copy_trailing_sentinels() {
let mut output = [0xa5; 12];
let job = [0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0xde, 0xad];
let output_size = output.len();
let result = run_output_publication(&mut output, output_size, 4, &job, job.len(), 5, 2);
assert_eq!(result.status, FLUSH_PUBLICATION_OK);
assert_eq!(result.toFlush, 3);
assert_eq!(&output[..4], &[0xa5, 0xa5, 0xa5, 0xa5]);
assert_eq!(&output[4..7], &job[2..5]);
assert_eq!(&output[7..], &[0xa5, 0xa5, 0xa5, 0xa5, 0xa5]);
}
#[test]
fn output_publication_rejects_invalid_bounds_without_copy() {
let mut output = [0xa5; 8];
let original = output;
let job = [0x50, 0x51, 0x52, 0x53];
let result = run_output_publication(&mut output, 4, 5, &job, job.len(), 4, 0);
assert_eq!(result.status, FLUSH_PUBLICATION_INVALID_BOUNDS);
assert_eq!(result.toFlush, 0);
assert_eq!(result.outputPos, 5);
assert_eq!(result.dstFlushed, 0);
assert_eq!(output, original);
}
#[test]
fn chunk_loop_handles_empty_jobs_without_compression() {
let mut compressor = MockChunkCompressor {