feat(compress): move stream2 fallback orchestration to Rust

The public ZSTD_compressStream2_c entry point still contained the complete
fallback state machine even though its validation, initialization policy,
serial-versus-MT dispatch, and result accounting were already represented by
Rust policy helpers. That left the main streaming entry point as a large C
orchestration island and made the Rust rewrite boundary misleading.

Project the scalar stream state and private CCtx operations through a checked
C ABI, then let Rust own the fallback ordering. Rust now validates buffers and
the end directive, handles transparent one-shot completion and stable-input
deferral, performs the initialization and stability decisions, selects the
serial or MT path, and publishes the result policy. C retains the private
context layout, stream adapter, MT operations, checksum/epilogue side effects,
and trace/reset callbacks. ABI offset assertions and focused callback-order
and stable-input tests protect the projection without requiring a standalone
Rust test binary to link every C bridge symbol.

Test Plan:
- `ulimit -v 41943040; CARGO_BUILD_JOBS=1 cargo +nightly fmt --manifest-path rust/Cargo.toml --all` -- passed.
- `ulimit -v 41943040; CARGO_BUILD_JOBS=1 cargo clippy --manifest-path rust/Cargo.toml --all-targets -- -D warnings` -- passed before and after formatting.
- `git diff --cached --check` -- passed.
- Native `make -j1` and the original suite remain the next post-commit verification gates.
This commit is contained in:
2026-07-21 13:08:28 +02:00
parent 5c6302a3c3
commit 6cf4898ab1
3 changed files with 762 additions and 150 deletions
+577
View File
@@ -895,6 +895,373 @@ pub unsafe extern "C" fn ZSTD_rust_compressStream2MTCoordinator(
unsafe { compress_stream2_mt_coordinator(state) }
}
type CompressStream2InitFn = unsafe extern "C" fn(*mut c_void, c_int, usize) -> usize;
type CompressStream2CheckBufferFn =
unsafe extern "C" fn(*mut c_void, *const c_void, *const c_void) -> usize;
type CompressStream2StreamFn =
unsafe extern "C" fn(*mut c_void, *mut ZSTD_outBuffer, *mut ZSTD_inBuffer, c_int) -> usize;
type CompressStream2UpdateMTFn = unsafe extern "C" fn(*mut c_void);
/// Scalar and private-state projections for the fallback half of the public
/// `ZSTD_compressStream2` entry point. The C shim retains context layout and
/// codec calls behind callbacks; Rust owns validation, initialization staging,
/// and the MT-versus-ST dispatch order.
#[repr(C)]
pub struct ZSTD_rust_compressStream2Fields {
callback_context: *mut c_void,
output: *mut ZSTD_outBuffer,
input: *mut ZSTD_inBuffer,
stream_stage: *mut c_int,
stable_in_not_consumed: *mut usize,
expected_in_buffer: *mut ZSTD_inBuffer,
simple_compress2_completed: *mut c_uint,
c_params_changed: *mut c_int,
out_buff_content_size: *const usize,
out_buff_flushed_size: *const usize,
pledged_src_size_plus_one: u64,
in_buffer_mode: c_int,
out_buffer_mode: c_int,
format: c_int,
nb_workers: c_int,
}
#[repr(C)]
pub struct ZSTD_rust_compressStream2Callbacks {
init_compress_stream2: Option<CompressStream2InitFn>,
check_buffer_stability: Option<CompressStream2CheckBufferFn>,
set_buffer_expectations: Option<CompressStream2SetBufferExpectationsFn>,
compress_stream: Option<CompressStream2StreamFn>,
update_mt_cparams: Option<CompressStream2UpdateMTFn>,
mt_step: Option<CompressStream2MTStepFn>,
mt_trace: Option<CompressStream2MTTraceFn>,
mt_reset: Option<CompressStream2MTResetFn>,
}
#[repr(C)]
pub struct ZSTD_rust_compressStream2State {
fields: *const ZSTD_rust_compressStream2Fields,
callbacks: *const ZSTD_rust_compressStream2Callbacks,
end_op: c_int,
}
const _: () = {
assert!(offset_of!(ZSTD_rust_compressStream2Fields, callback_context) == 0);
assert!(offset_of!(ZSTD_rust_compressStream2Fields, output) == size_of::<usize>());
assert!(offset_of!(ZSTD_rust_compressStream2Fields, input) == 2 * size_of::<usize>());
assert!(offset_of!(ZSTD_rust_compressStream2Fields, stream_stage) == 3 * size_of::<usize>());
assert!(
offset_of!(ZSTD_rust_compressStream2Fields, stable_in_not_consumed)
== 4 * size_of::<usize>()
);
assert!(
offset_of!(ZSTD_rust_compressStream2Fields, expected_in_buffer) == 5 * size_of::<usize>()
);
assert!(
offset_of!(ZSTD_rust_compressStream2Fields, simple_compress2_completed)
== 6 * size_of::<usize>()
);
assert!(
offset_of!(ZSTD_rust_compressStream2Fields, c_params_changed) == 7 * size_of::<usize>()
);
assert!(
offset_of!(ZSTD_rust_compressStream2Fields, out_buff_content_size)
== size_of::<[usize; 8]>()
);
assert!(
offset_of!(ZSTD_rust_compressStream2Fields, out_buff_flushed_size)
== 9 * size_of::<usize>()
);
assert!(
offset_of!(ZSTD_rust_compressStream2Fields, pledged_src_size_plus_one)
== 10 * size_of::<usize>()
);
assert!(
offset_of!(ZSTD_rust_compressStream2Fields, in_buffer_mode)
== 10 * size_of::<usize>() + size_of::<u64>()
);
assert!(
offset_of!(ZSTD_rust_compressStream2Fields, out_buffer_mode)
== 10 * size_of::<usize>() + size_of::<u64>() + size_of::<c_int>()
);
assert!(
offset_of!(ZSTD_rust_compressStream2Fields, format)
== 10 * size_of::<usize>() + size_of::<u64>() + 2 * size_of::<c_int>()
);
assert!(
offset_of!(ZSTD_rust_compressStream2Fields, nb_workers)
== 10 * size_of::<usize>() + size_of::<u64>() + 3 * size_of::<c_int>()
);
assert!(
size_of::<ZSTD_rust_compressStream2Fields>()
== (10 * size_of::<usize>() + size_of::<u64>() + 4 * size_of::<c_int>())
.div_ceil(size_of::<usize>())
* size_of::<usize>()
);
assert!(size_of::<CompressStream2InitFn>() == size_of::<usize>());
assert!(size_of::<CompressStream2CheckBufferFn>() == size_of::<usize>());
assert!(size_of::<CompressStream2StreamFn>() == size_of::<usize>());
assert!(size_of::<CompressStream2UpdateMTFn>() == size_of::<usize>());
assert!(size_of::<Option<CompressStream2InitFn>>() == size_of::<usize>());
assert!(size_of::<Option<CompressStream2CheckBufferFn>>() == size_of::<usize>());
assert!(size_of::<Option<CompressStream2StreamFn>>() == size_of::<usize>());
assert!(size_of::<Option<CompressStream2UpdateMTFn>>() == size_of::<usize>());
assert!(offset_of!(ZSTD_rust_compressStream2State, fields) == 0);
assert!(offset_of!(ZSTD_rust_compressStream2State, callbacks) == size_of::<usize>());
assert!(offset_of!(ZSTD_rust_compressStream2State, end_op) == 2 * size_of::<usize>());
assert!(
size_of::<ZSTD_rust_compressStream2State>()
== (2 * size_of::<usize>() + size_of::<c_int>()).div_ceil(size_of::<usize>())
* size_of::<usize>()
);
};
#[cfg(not(test))]
#[inline]
unsafe fn simple_compress2_level(cctx: *const c_void, src_size: usize) -> c_int {
unsafe { ZSTD_rust_simpleCompress2Level(cctx, src_size) }
}
#[cfg(test)]
#[inline]
unsafe fn simple_compress2_level(_cctx: *const c_void, _src_size: usize) -> c_int {
c_int::MIN
}
#[inline]
unsafe fn compress_stream2_fallback(state: &ZSTD_rust_compressStream2State) -> usize {
let Some(fields) = (unsafe { state.fields.as_ref() }) else {
return ERROR(ZstdErrorCode::Generic);
};
let Some(callbacks) = (unsafe { state.callbacks.as_ref() }) else {
return ERROR(ZstdErrorCode::Generic);
};
let Some(output) = (unsafe { fields.output.as_mut() }) else {
return ERROR(ZstdErrorCode::Generic);
};
let Some(input) = (unsafe { fields.input.as_mut() }) else {
return ERROR(ZstdErrorCode::Generic);
};
let Some(stream_stage) = (unsafe { fields.stream_stage.as_mut() }) else {
return ERROR(ZstdErrorCode::Generic);
};
let Some(stable_in_not_consumed) = (unsafe { fields.stable_in_not_consumed.as_mut() }) else {
return ERROR(ZstdErrorCode::Generic);
};
let Some(expected_in_buffer) = (unsafe { fields.expected_in_buffer.as_mut() }) else {
return ERROR(ZstdErrorCode::Generic);
};
let Some(simple_compress2_completed) = (unsafe { fields.simple_compress2_completed.as_mut() })
else {
return ERROR(ZstdErrorCode::Generic);
};
let Some(out_buff_content_size) = (unsafe { fields.out_buff_content_size.as_ref() }) else {
return ERROR(ZstdErrorCode::Generic);
};
let Some(out_buff_flushed_size) = (unsafe { fields.out_buff_flushed_size.as_ref() }) else {
return ERROR(ZstdErrorCode::Generic);
};
let Some(init_compress_stream2) = callbacks.init_compress_stream2 else {
return ERROR(ZstdErrorCode::Generic);
};
let Some(check_buffer_stability) = callbacks.check_buffer_stability else {
return ERROR(ZstdErrorCode::Generic);
};
let Some(set_buffer_expectations) = callbacks.set_buffer_expectations else {
return ERROR(ZstdErrorCode::Generic);
};
if fields.callback_context.is_null() {
return ERROR(ZstdErrorCode::Generic);
}
if output.pos > output.size {
return ERROR(ZstdErrorCode::DstSizeTooSmall);
}
if input.pos > input.size {
return ERROR(ZstdErrorCode::SrcSizeWrong);
}
if compress_stream2_policy(state.end_op) == 0 {
return ERROR(ZstdErrorCode::ParameterOutOfBound);
}
if *simple_compress2_completed != 0 {
if state.end_op == ZSTD_E_END
&& *stream_stage == ZSTD_CSTREAM_STAGE_INIT
&& fields.pledged_src_size_plus_one == 0
{
let src_size = input.size - input.pos;
let level =
unsafe { simple_compress2_level(fields.callback_context.cast_const(), src_size) };
if level != c_int::MIN {
let dst = if output.dst.is_null() {
ptr::null_mut()
} else {
unsafe { output.dst.cast::<u8>().add(output.pos).cast() }
};
let src = if input.src.is_null() {
ptr::null()
} else {
unsafe { input.src.cast::<u8>().add(input.pos).cast() }
};
let result = unsafe {
ZSTD_compress2(
fields.callback_context,
dst,
output.size - output.pos,
src,
src_size,
)
};
*simple_compress2_completed = 0;
if !ERR_isError(result) {
output.pos += result;
input.pos = input.size;
return 0;
}
}
}
*simple_compress2_completed = 0;
}
if *stream_stage == ZSTD_CSTREAM_STAGE_INIT {
let input_size = input.size - input.pos;
let init_policy = compress_stream2_init_policy(&ZSTD_rust_compressStream2InitPolicyState {
in_buffer_mode: fields.in_buffer_mode,
end_op: state.end_op,
input_size,
stable_in_not_consumed: *stable_in_not_consumed,
input_src: input.src,
input_pos: input.pos,
expected_input_src: expected_in_buffer.src,
expected_input_size: expected_in_buffer.size,
format: fields.format,
});
match init_policy {
ZSTD_RUST_COMPRESS_STREAM2_INIT_POLICY_STABLE_SRC_INVALID => {
return ERROR(ZstdErrorCode::StabilityConditionNotRespected)
}
ZSTD_RUST_COMPRESS_STREAM2_INIT_POLICY_STABLE_POS_INVALID => {
return ERROR(ZstdErrorCode::StabilityConditionNotRespected)
}
ZSTD_RUST_COMPRESS_STREAM2_INIT_POLICY_DEFER_MAGICLESS
| ZSTD_RUST_COMPRESS_STREAM2_INIT_POLICY_DEFER_ZSTD => {
input.pos = input.size;
expected_in_buffer.src = input.src;
expected_in_buffer.size = input.size;
expected_in_buffer.pos = input.pos;
*stable_in_not_consumed = stable_in_not_consumed.wrapping_add(input_size);
return init_policy as usize;
}
_ => {}
}
let init_result = unsafe {
init_compress_stream2(
fields.callback_context,
state.end_op,
input_size.wrapping_add(*stable_in_not_consumed),
)
};
if ERR_isError(init_result) {
return init_result;
}
unsafe {
set_buffer_expectations(
fields.callback_context,
output as *const ZSTD_outBuffer as *const c_void,
input as *const ZSTD_inBuffer as *const c_void,
)
};
}
let stability_result = unsafe {
check_buffer_stability(
fields.callback_context,
output as *const ZSTD_outBuffer as *const c_void,
input as *const ZSTD_inBuffer as *const c_void,
)
};
if ERR_isError(stability_result) {
return stability_result;
}
if fields.nb_workers > 0 {
if !fields.c_params_changed.is_null() {
let c_params_changed = unsafe { &mut *fields.c_params_changed };
if *c_params_changed != 0 {
let Some(update_mt_cparams) = callbacks.update_mt_cparams else {
return ERROR(ZstdErrorCode::Generic);
};
unsafe { update_mt_cparams(fields.callback_context) };
*c_params_changed = 0;
}
}
if *stable_in_not_consumed != 0 {
if input.pos < *stable_in_not_consumed {
return ERROR(ZstdErrorCode::Generic);
}
input.pos -= *stable_in_not_consumed;
*stable_in_not_consumed = 0;
}
let (Some(mt_step), Some(mt_trace), Some(mt_reset)) =
(callbacks.mt_step, callbacks.mt_trace, callbacks.mt_reset)
else {
return ERROR(ZstdErrorCode::Generic);
};
let completion_callbacks = ZSTD_rust_compressStream2MTCompletionCallbacks {
callback_context: fields.callback_context,
trace: Some(mt_trace),
reset: Some(mt_reset),
};
let coordinator_state = ZSTD_rust_compressStream2MTCoordinatorState {
callback_context: fields.callback_context,
step: Some(mt_step),
output,
input,
completion_callbacks: &completion_callbacks,
end_op: state.end_op,
};
let flush_min = unsafe { ZSTD_rust_compressStream2MTCoordinator(&coordinator_state) };
if ERR_isError(flush_min) {
return flush_min;
}
unsafe {
set_buffer_expectations(
fields.callback_context,
output as *const ZSTD_outBuffer as *const c_void,
input as *const ZSTD_inBuffer as *const c_void,
)
};
return flush_min;
}
let Some(compress_stream) = callbacks.compress_stream else {
return ERROR(ZstdErrorCode::Generic);
};
let compress_result =
unsafe { compress_stream(fields.callback_context, output, input, state.end_op) };
let result_state = ZSTD_rust_compressStream2ResultPolicyState {
callback_context: fields.callback_context,
set_buffer_expectations: Some(set_buffer_expectations),
output: output as *const ZSTD_outBuffer as *const c_void,
input: input as *const ZSTD_inBuffer as *const c_void,
compress_result,
out_buff_content_size: *out_buff_content_size,
out_buff_flushed_size: *out_buff_flushed_size,
};
unsafe { compress_stream2_result_policy(&result_state) }
}
/// Run the public stream fallback while keeping private CCtx operations behind
/// the C projection callbacks.
#[no_mangle]
pub unsafe extern "C" fn ZSTD_rust_compressStream2(
state: *const ZSTD_rust_compressStream2State,
) -> usize {
let Some(state) = (unsafe { state.as_ref() }) else {
return ERROR(ZstdErrorCode::Generic);
};
unsafe { compress_stream2_fallback(state) }
}
const ZSTD_C_WINDOW_LOG: c_int = 101;
const ZSTD_C_HASH_LOG: c_int = 102;
const ZSTD_C_CHAIN_LOG: c_int = 103;
@@ -22557,4 +22924,214 @@ mod tests {
ERROR(ZstdErrorCode::SrcSizeWrong)
);
}
struct Stream2FallbackProbe {
events: Vec<&'static str>,
compress_result: usize,
}
unsafe extern "C" fn stream2_test_init(
context: *mut c_void,
_end_op: c_int,
_input_size: usize,
) -> usize {
unsafe { &mut *context.cast::<Stream2FallbackProbe>() }
.events
.push("init");
0
}
unsafe extern "C" fn stream2_test_check(
context: *mut c_void,
_output: *const c_void,
_input: *const c_void,
) -> usize {
unsafe { &mut *context.cast::<Stream2FallbackProbe>() }
.events
.push("check");
0
}
unsafe extern "C" fn stream2_test_set(
context: *mut c_void,
_output: *const c_void,
_input: *const c_void,
) {
unsafe { &mut *context.cast::<Stream2FallbackProbe>() }
.events
.push("set");
}
unsafe extern "C" fn stream2_test_compress(
context: *mut c_void,
output: *mut ZSTD_outBuffer,
input: *mut ZSTD_inBuffer,
_end_op: c_int,
) -> usize {
let probe = unsafe { &mut *context.cast::<Stream2FallbackProbe>() };
probe.events.push("compress");
unsafe {
(*input).pos = (*input).size;
(*output).pos += 5;
}
probe.compress_result
}
fn stream2_test_state<'a>(
probe: &'a mut Stream2FallbackProbe,
output: &'a mut ZSTD_outBuffer,
input: &'a mut ZSTD_inBuffer,
stage: &'a mut c_int,
stable_in_not_consumed: &'a mut usize,
expected_in_buffer: &'a mut ZSTD_inBuffer,
simple_compress2_completed: &'a mut c_uint,
out_buff_content_size: &'a usize,
out_buff_flushed_size: &'a usize,
) -> ZSTD_rust_compressStream2State {
let fields = Box::new(ZSTD_rust_compressStream2Fields {
callback_context: probe as *mut Stream2FallbackProbe as *mut c_void,
output,
input,
stream_stage: stage,
stable_in_not_consumed,
expected_in_buffer,
simple_compress2_completed,
c_params_changed: ptr::null_mut(),
out_buff_content_size,
out_buff_flushed_size,
pledged_src_size_plus_one: 0,
in_buffer_mode: ZSTD_BM_BUFFERED,
out_buffer_mode: ZSTD_BM_BUFFERED,
format: ZSTD_F_ZSTD1,
nb_workers: 0,
});
let callbacks = Box::new(ZSTD_rust_compressStream2Callbacks {
init_compress_stream2: Some(stream2_test_init),
check_buffer_stability: Some(stream2_test_check),
set_buffer_expectations: Some(stream2_test_set),
compress_stream: Some(stream2_test_compress),
update_mt_cparams: None,
mt_step: None,
mt_trace: None,
mt_reset: None,
});
ZSTD_rust_compressStream2State {
fields: Box::into_raw(fields),
callbacks: Box::into_raw(callbacks),
end_op: ZSTD_E_CONTINUE,
}
}
#[test]
fn stream2_fallback_preserves_serial_callback_order_and_result_policy() {
let mut probe = Stream2FallbackProbe {
events: Vec::new(),
compress_result: 7,
};
let mut output_bytes = [0u8; 32];
let input_bytes = [1u8; 8];
let mut output = ZSTD_outBuffer {
dst: output_bytes.as_mut_ptr().cast(),
size: output_bytes.len(),
pos: 0,
};
let mut input = ZSTD_inBuffer {
src: input_bytes.as_ptr().cast(),
size: input_bytes.len(),
pos: 0,
};
let mut stage = ZSTD_CSTREAM_STAGE_LOAD;
let mut stable_in_not_consumed = 0;
let mut expected_in_buffer = ZSTD_inBuffer {
src: ptr::null(),
size: 0,
pos: 0,
};
let mut simple_compress2_completed = 0;
let out_buff_content_size = 19;
let out_buff_flushed_size = 4;
let state = stream2_test_state(
&mut probe,
&mut output,
&mut input,
&mut stage,
&mut stable_in_not_consumed,
&mut expected_in_buffer,
&mut simple_compress2_completed,
&out_buff_content_size,
&out_buff_flushed_size,
);
let result = unsafe { ZSTD_rust_compressStream2(&state) };
assert_eq!(result, 15);
assert_eq!(probe.events, ["check", "compress", "set"]);
assert_eq!(input.pos, input.size);
assert_eq!(output.pos, 5);
unsafe {
drop(Box::from_raw(state.fields.cast_mut()));
drop(Box::from_raw(state.callbacks.cast_mut()));
}
}
#[test]
fn stream2_fallback_defers_small_stable_input_without_callbacks() {
let mut probe = Stream2FallbackProbe {
events: Vec::new(),
compress_result: 0,
};
let input_bytes = [1u8; 16];
let mut output = ZSTD_outBuffer {
dst: ptr::null_mut(),
size: 0,
pos: 0,
};
let mut input = ZSTD_inBuffer {
src: input_bytes.as_ptr().cast(),
size: input_bytes.len(),
pos: 0,
};
let mut stage = ZSTD_CSTREAM_STAGE_INIT;
let mut stable_in_not_consumed = 0;
let mut expected_in_buffer = ZSTD_inBuffer {
src: ptr::null(),
size: 0,
pos: 0,
};
let mut simple_compress2_completed = 0;
let out_buff_content_size = 0;
let out_buff_flushed_size = 0;
let state = stream2_test_state(
&mut probe,
&mut output,
&mut input,
&mut stage,
&mut stable_in_not_consumed,
&mut expected_in_buffer,
&mut simple_compress2_completed,
&out_buff_content_size,
&out_buff_flushed_size,
);
unsafe {
(*state.fields.cast_mut()).in_buffer_mode = ZSTD_BM_STABLE;
(*state.fields.cast_mut()).format = ZSTD_F_ZSTD1;
}
let result = unsafe { ZSTD_rust_compressStream2(&state) };
assert_eq!(
result,
ZSTD_RUST_COMPRESS_STREAM2_INIT_POLICY_DEFER_ZSTD as usize
);
assert_eq!(input.pos, input.size);
assert_eq!(stable_in_not_consumed, input.size);
assert_eq!(expected_in_buffer.src, input.src);
assert_eq!(expected_in_buffer.size, input.size);
assert_eq!(expected_in_buffer.pos, input.size);
assert!(probe.events.is_empty());
unsafe {
drop(Box::from_raw(state.fields.cast_mut()));
drop(Box::from_raw(state.callbacks.cast_mut()));
}
}
}