feat(mt): move empty-job emission into Rust

The multithreaded compressor still handled the terminal empty-job case in C,
including the decision to acquire a buffer, emit the final empty block, and
clear the consumed source range.  That left a small but stateful branch outside
the Rust job helpers and made allocation-failure behavior difficult to test.

Project the terminal job and result through a stable C ABI, then let Rust own
that branch's assertions, buffer callback, block-header serialization, and
failure-preserving result.  C remains responsible for its private job layout
and applies the returned buffer, source, and compressed-size fields exactly as
before.

Test Plan:
- `cargo test --manifest-path rust/Cargo.toml --all-targets -- --test-threads=1` -- 473 passed.
- Native multithreaded cases in `test-fuzzer`, `test-zstream`, `test-cli-tests`, and `test-zstd` -- passed.
- `make -B -C lib -j2 lib ZSTD_FORCE_DECOMPRESS_SEQUENCES_SHORT=1 ZSTD_FORCE_DECOMPRESS_SEQUENCES_LONG=0` -- passed.
- `make -B -C lib -j2 lib ZSTD_FORCE_DECOMPRESS_SEQUENCES_SHORT=0 ZSTD_FORCE_DECOMPRESS_SEQUENCES_LONG=1` -- passed.
This commit is contained in:
2026-07-18 22:40:56 +02:00
parent 217cc76e74
commit 0e0d87ddf5
2 changed files with 186 additions and 9 deletions
+51 -9
View File
@@ -140,6 +140,24 @@ ZSTDMT_flushPublicationResult ZSTDMT_rust_publishJobOutput(
const void* jobDst, size_t jobCapacity,
size_t cSize, size_t dstFlushed);
typedef struct {
unsigned lastJob;
size_t srcSize;
unsigned firstJob;
void* dstStart;
size_t dstCapacity;
size_t consumed;
} ZSTDMT_RustEmptyBlockJobProjection;
typedef struct {
ZSTDMT_RustBuffer buffer;
size_t cSize;
unsigned clearSource;
} ZSTDMT_RustEmptyBlockResult;
typedef ZSTDMT_RustBuffer (*ZSTDMT_bufferGetFn)(void* opaque);
ZSTDMT_RustEmptyBlockResult ZSTDMT_rust_writeLastEmptyBlock(
const ZSTDMT_RustEmptyBlockJobProjection* projection,
void* opaque, ZSTDMT_bufferGetFn getBuffer);
typedef struct {
rawSeq* seq;
size_t pos;
@@ -288,6 +306,13 @@ static Buffer ZSTDMT_getBuffer(ZSTDMT_bufferPool* bufPool)
return buffer;
}
static ZSTDMT_RustBuffer ZSTDMT_getBufferForRust(void* opaque)
{
Buffer const buffer = ZSTDMT_getBuffer((ZSTDMT_bufferPool*)opaque);
ZSTDMT_RustBuffer const result = { buffer.start, buffer.capacity };
return result;
}
#if ZSTD_RESIZE_SEQPOOL
/** ZSTDMT_resizeBuffer() :
* assumption : bufPool must be valid
@@ -1375,20 +1400,37 @@ size_t ZSTDMT_initCStream_internal(
*/
static void ZSTDMT_writeLastEmptyBlock(ZSTDMT_jobDescription* job)
{
ZSTDMT_RustEmptyBlockJobProjection projection;
ZSTDMT_RustEmptyBlockResult result;
assert(job->lastJob == 1);
assert(job->src.size == 0); /* last job is empty -> will be simplified into a last empty block */
assert(job->firstJob == 0); /* cannot be first job, as it also needs to create frame header */
assert(job->dstBuff.start == NULL); /* invoked from streaming variant only (otherwise, dstBuff might be user's output) */
job->dstBuff = ZSTDMT_getBuffer(job->bufPool);
if (job->dstBuff.start == NULL) {
job->cSize = ERROR(memory_allocation);
return;
}
assert(job->dstBuff.capacity >= ZSTD_blockHeaderSize); /* no buffer should ever be that small */
job->src = kNullRange;
job->cSize = ZSTD_writeLastEmptyBlock(job->dstBuff.start, job->dstBuff.capacity);
assert(!ZSTD_isError(job->cSize));
assert(job->consumed == 0);
projection = (ZSTDMT_RustEmptyBlockJobProjection){
job->lastJob,
job->src.size,
job->firstJob,
job->dstBuff.start,
job->dstBuff.capacity,
job->consumed
};
result = ZSTDMT_rust_writeLastEmptyBlock(
&projection, job->bufPool, ZSTDMT_getBufferForRust);
job->dstBuff = (Buffer){ result.buffer.start, result.buffer.capacity };
if (job->dstBuff.start == NULL) {
assert(!result.clearSource);
job->cSize = result.cSize;
return;
}
assert(result.clearSource);
assert(job->dstBuff.capacity >= ZSTD_blockHeaderSize); /* no buffer should ever be that small */
if (result.clearSource) job->src = kNullRange;
job->cSize = result.cSize;
assert(!ZSTD_isError(job->cSize));
}
static size_t ZSTDMT_createCompressionJob(ZSTDMT_CCtx* mtctx, size_t srcSize, ZSTD_EndDirective endOp)