feat(compress): move workspace lifecycle orchestration to Rust

Route private workspace init, dynamic allocation, release, and move ordering
through a Rust lifecycle bridge. C continues to own the opaque workspace layout,
sanitizer-sensitive byte operations, custom allocator callbacks, and consistency
assertions; Rust owns publication order, allocation-before-initialization,
metadata-zero-before-release, and source invalidation. The checked projection
keeps the field-slot and callback ABI explicit and adds focused lifecycle tests
for successful and failed allocation, free ordering, and move semantics.

Test Plan:
- ulimit -v 41943040; CARGO_BUILD_JOBS=1 cargo check --manifest-path rust/Cargo.toml --tests
- ulimit -v 41943040; CARGO_BUILD_JOBS=1 cargo clippy --manifest-path rust/cli/Cargo.toml --all-targets -- -D warnings
- ulimit -v 41943040; CARGO_BUILD_JOBS=1 make -j1
- ulimit -v 41943040; make -j1 -C tests test
This commit is contained in:
2026-07-21 18:29:19 +02:00
parent 62eea1dd83
commit 4fc1a43977
2 changed files with 559 additions and 22 deletions
+404
View File
@@ -3247,6 +3247,214 @@ pub unsafe extern "C" fn ZSTD_rust_refThreadPool(
0
}
type CwkspAllocateFn = unsafe extern "C" fn(*mut c_void, usize) -> *mut c_void;
type CwkspReleaseFn = unsafe extern "C" fn(*mut c_void, *mut c_void, usize);
type CwkspCallbackFn = unsafe extern "C" fn(*mut c_void);
type CwkspCopyFn = unsafe extern "C" fn(*mut c_void, *const c_void);
const ZSTD_CWKSP_ALLOC_OBJECTS: c_int = 0;
const ZSTD_CWKSP_DYNAMIC_ALLOC: c_int = 0;
const ZSTD_CWKSP_ALIGNMENT_BYTES: usize = 64;
/// C-owned field slots and callbacks for the private compression workspace.
///
/// Rust owns the lifecycle ordering and state publication. C retains the
/// `ZSTD_cwksp` layout, sanitizer-sensitive clear/zero/copy operations, and
/// custom allocator implementation behind these callbacks. All fields are
/// pointer-sized on the C side; nullable function pointers are represented as
/// `Option` so reading the C-side NULL slots is well-defined in Rust.
#[repr(C)]
pub struct ZSTD_rust_cwkspState {
callback_context: *mut c_void,
workspace: *mut *mut c_void,
workspace_end: *mut *mut c_void,
object_end: *mut *mut c_void,
table_end: *mut *mut c_void,
table_valid_end: *mut *mut c_void,
alloc_start: *mut *mut c_void,
init_once_start: *mut *mut c_void,
alloc_failed: *mut u8,
workspace_oversized_duration: *mut c_int,
phase: *mut c_int,
is_static: *mut c_int,
allocator_context: *mut c_void,
allocate: Option<CwkspAllocateFn>,
release_context: *mut c_void,
release: Option<CwkspReleaseFn>,
clear: Option<CwkspCallbackFn>,
zero: Option<CwkspCallbackFn>,
}
const _: () = {
assert!(size_of::<CwkspAllocateFn>() == size_of::<usize>());
assert!(size_of::<CwkspReleaseFn>() == size_of::<usize>());
assert!(size_of::<CwkspCallbackFn>() == size_of::<usize>());
assert!(size_of::<CwkspCopyFn>() == size_of::<usize>());
assert!(offset_of!(ZSTD_rust_cwkspState, callback_context) == 0);
assert!(offset_of!(ZSTD_rust_cwkspState, workspace) == size_of::<usize>());
assert!(offset_of!(ZSTD_rust_cwkspState, workspace_end) == 2 * size_of::<usize>());
assert!(offset_of!(ZSTD_rust_cwkspState, object_end) == 3 * size_of::<usize>());
assert!(offset_of!(ZSTD_rust_cwkspState, table_end) == 4 * size_of::<usize>());
assert!(offset_of!(ZSTD_rust_cwkspState, table_valid_end) == 5 * size_of::<usize>());
assert!(offset_of!(ZSTD_rust_cwkspState, alloc_start) == 6 * size_of::<usize>());
assert!(offset_of!(ZSTD_rust_cwkspState, init_once_start) == 7 * size_of::<usize>());
assert!(offset_of!(ZSTD_rust_cwkspState, alloc_failed) == 8 * size_of::<usize>());
assert!(
offset_of!(ZSTD_rust_cwkspState, workspace_oversized_duration)
== 9 * size_of::<usize>()
);
assert!(offset_of!(ZSTD_rust_cwkspState, phase) == 10 * size_of::<usize>());
assert!(offset_of!(ZSTD_rust_cwkspState, is_static) == 11 * size_of::<usize>());
assert!(offset_of!(ZSTD_rust_cwkspState, allocator_context) == 12 * size_of::<usize>());
assert!(offset_of!(ZSTD_rust_cwkspState, allocate) == 13 * size_of::<usize>());
assert!(offset_of!(ZSTD_rust_cwkspState, release_context) == 14 * size_of::<usize>());
assert!(offset_of!(ZSTD_rust_cwkspState, release) == 15 * size_of::<usize>());
assert!(offset_of!(ZSTD_rust_cwkspState, clear) == 16 * size_of::<usize>());
assert!(offset_of!(ZSTD_rust_cwkspState, zero) == 17 * size_of::<usize>());
assert!(size_of::<ZSTD_rust_cwkspState>() == 18 * size_of::<usize>());
};
#[inline]
fn cwksp_initial_alloc_start(workspace_end: *mut c_void) -> *mut c_void {
((workspace_end as usize) & !(ZSTD_CWKSP_ALIGNMENT_BYTES - 1)) as *mut c_void
}
#[inline]
fn cwksp_state_has_storage(state: &ZSTD_rust_cwkspState) -> bool {
!state.callback_context.is_null()
&& !state.workspace.is_null()
&& !state.workspace_end.is_null()
&& !state.object_end.is_null()
&& !state.table_end.is_null()
&& !state.table_valid_end.is_null()
&& !state.alloc_start.is_null()
&& !state.init_once_start.is_null()
&& !state.alloc_failed.is_null()
&& !state.workspace_oversized_duration.is_null()
&& !state.phase.is_null()
&& !state.is_static.is_null()
}
unsafe fn cwksp_init_state(
state: &ZSTD_rust_cwkspState,
start: *mut c_void,
size: usize,
is_static: c_int,
clear: CwkspCallbackFn,
) {
let workspace_end = unsafe { start.cast::<u8>().add(size).cast::<c_void>() };
unsafe {
*state.workspace = start;
*state.workspace_end = workspace_end;
*state.object_end = start;
*state.table_valid_end = start;
*state.init_once_start = cwksp_initial_alloc_start(workspace_end);
*state.phase = ZSTD_CWKSP_ALLOC_OBJECTS;
*state.is_static = is_static;
clear(state.callback_context);
*state.workspace_oversized_duration = 0;
}
}
/// Initialize the private C workspace in the original field-publication order.
#[no_mangle]
pub unsafe extern "C" fn ZSTD_rust_cwkspInit(
state: *const ZSTD_rust_cwkspState,
start: *mut c_void,
size: usize,
is_static: c_int,
) -> usize {
let Some(state) = (unsafe { state.as_ref() }) else {
return ERROR(ZstdErrorCode::Generic);
};
let Some(clear) = state.clear else {
return ERROR(ZstdErrorCode::Generic);
};
if !cwksp_state_has_storage(state) {
return ERROR(ZstdErrorCode::Generic);
}
unsafe { cwksp_init_state(state, start, size, is_static, clear) };
0
}
/// Allocate and initialize a dynamic private C workspace through its custom
/// allocator callback. Allocation happens before any workspace field changes.
#[no_mangle]
pub unsafe extern "C" fn ZSTD_rust_cwkspCreate(
state: *const ZSTD_rust_cwkspState,
size: usize,
) -> usize {
let Some(state) = (unsafe { state.as_ref() }) else {
return ERROR(ZstdErrorCode::Generic);
};
let Some(allocate) = state.allocate else {
return ERROR(ZstdErrorCode::Generic);
};
let Some(clear) = state.clear else {
return ERROR(ZstdErrorCode::Generic);
};
if !cwksp_state_has_storage(state) || state.allocator_context.is_null() {
return ERROR(ZstdErrorCode::Generic);
}
let workspace = unsafe { allocate(state.allocator_context, size) };
if workspace.is_null() {
return ERROR(ZstdErrorCode::MemoryAllocation);
}
unsafe { cwksp_init_state(state, workspace, size, ZSTD_CWKSP_DYNAMIC_ALLOC, clear) };
0
}
/// Clear the C workspace metadata and release its owned allocation in order.
#[no_mangle]
pub unsafe extern "C" fn ZSTD_rust_cwkspFree(state: *const ZSTD_rust_cwkspState) {
let Some(state) = (unsafe { state.as_ref() }) else {
return;
};
let Some(zero) = state.zero else {
return;
};
let Some(release) = state.release else {
return;
};
if state.callback_context.is_null()
|| state.workspace.is_null()
|| state.workspace_end.is_null()
|| state.release_context.is_null()
{
return;
}
let workspace = unsafe { *state.workspace };
let workspace_size = if workspace.is_null() {
0
} else {
let workspace_end = unsafe { *state.workspace_end };
(workspace_end as usize).wrapping_sub(workspace as usize)
};
unsafe {
zero(state.callback_context);
release(state.release_context, workspace, workspace_size);
}
}
/// Move workspace metadata and leave the source in a zeroed invalid state.
#[no_mangle]
pub unsafe extern "C" fn ZSTD_rust_cwkspMove(
destination: *mut c_void,
source: *mut c_void,
copy: CwkspCopyFn,
zero: CwkspCallbackFn,
) {
if destination.is_null() || source.is_null() {
return;
}
unsafe {
copy(destination, source);
zero(source);
}
}
type InitCCtxCallbackFn = unsafe extern "C" fn(*mut c_void);
type InitCCtxSetCustomMemFn = unsafe extern "C" fn(*mut c_void, *const c_void);
type InitCCtxResetFn = unsafe extern "C" fn(*mut c_void) -> usize;
@@ -11897,6 +12105,202 @@ mod tests {
const ZSTD_BTULTRA2: c_int = 9;
const ZSTD_F_ZSTD1_MAGICLESS: c_int = 1;
#[derive(Default)]
struct CwkspTestContext {
events: Vec<&'static str>,
allocation: *mut c_void,
requested_size: usize,
released_workspace: *mut c_void,
released_size: usize,
}
#[derive(Default)]
struct CwkspTestSlots {
workspace: *mut c_void,
workspace_end: *mut c_void,
object_end: *mut c_void,
table_end: *mut c_void,
table_valid_end: *mut c_void,
alloc_start: *mut c_void,
init_once_start: *mut c_void,
alloc_failed: u8,
workspace_oversized_duration: c_int,
phase: c_int,
is_static: c_int,
}
unsafe extern "C" fn cwksp_test_clear(context: *mut c_void) {
let context = unsafe { &mut *context.cast::<CwkspTestContext>() };
context.events.push("clear");
}
unsafe extern "C" fn cwksp_test_allocate(context: *mut c_void, size: usize) -> *mut c_void {
let context = unsafe { &mut *context.cast::<CwkspTestContext>() };
context.events.push("allocate");
context.requested_size = size;
context.allocation
}
unsafe extern "C" fn cwksp_test_zero(context: *mut c_void) {
let context = unsafe { &mut *context.cast::<CwkspTestContext>() };
context.events.push("zero");
}
unsafe extern "C" fn cwksp_test_release(
context: *mut c_void,
workspace: *mut c_void,
workspace_size: usize,
) {
let context = unsafe { &mut *context.cast::<CwkspTestContext>() };
context.events.push("release");
context.released_workspace = workspace;
context.released_size = workspace_size;
}
fn cwksp_test_state(
context: &mut CwkspTestContext,
slots: &mut CwkspTestSlots,
) -> ZSTD_rust_cwkspState {
let context = (context as *mut CwkspTestContext).cast();
ZSTD_rust_cwkspState {
callback_context: context,
workspace: &mut slots.workspace,
workspace_end: &mut slots.workspace_end,
object_end: &mut slots.object_end,
table_end: &mut slots.table_end,
table_valid_end: &mut slots.table_valid_end,
alloc_start: &mut slots.alloc_start,
init_once_start: &mut slots.init_once_start,
alloc_failed: &mut slots.alloc_failed,
workspace_oversized_duration: &mut slots.workspace_oversized_duration,
phase: &mut slots.phase,
is_static: &mut slots.is_static,
allocator_context: context,
allocate: Some(cwksp_test_allocate),
release_context: context,
release: Some(cwksp_test_release),
clear: Some(cwksp_test_clear),
zero: Some(cwksp_test_zero),
}
}
#[test]
fn cwksp_init_publishes_bounds_before_clear_and_resets_duration_afterward() {
let mut context = CwkspTestContext::default();
let mut slots = CwkspTestSlots::default();
let mut backing = [0usize; 32];
let start = backing.as_mut_ptr().cast::<c_void>();
let size = backing.len() * size_of::<usize>();
let state = cwksp_test_state(&mut context, &mut slots);
let result = unsafe { ZSTD_rust_cwkspInit(&state, start, size, 1) };
let workspace_end = unsafe { start.cast::<u8>().add(size).cast::<c_void>() };
let initial_alloc_start =
((workspace_end as usize) & !(ZSTD_CWKSP_ALIGNMENT_BYTES - 1)) as *mut c_void;
assert_eq!(result, 0);
assert_eq!(context.events, ["clear"]);
assert_eq!(slots.workspace, start);
assert_eq!(slots.workspace_end, workspace_end);
assert_eq!(slots.object_end, start);
assert_eq!(slots.table_valid_end, start);
assert_eq!(slots.init_once_start, initial_alloc_start);
assert_eq!(slots.phase, ZSTD_CWKSP_ALLOC_OBJECTS);
assert_eq!(slots.is_static, 1);
assert_eq!(slots.workspace_oversized_duration, 0);
}
#[test]
fn cwksp_create_allocates_before_initializing_and_preserves_failure_order() {
let mut backing = [0usize; 16];
let allocation = backing.as_mut_ptr().cast::<c_void>();
let mut context = CwkspTestContext {
allocation,
..Default::default()
};
let mut slots = CwkspTestSlots::default();
let state = cwksp_test_state(&mut context, &mut slots);
let result = unsafe { ZSTD_rust_cwkspCreate(&state, 1234) };
assert_eq!(result, 0);
assert_eq!(context.events, ["allocate", "clear"]);
assert_eq!(context.requested_size, 1234);
assert_eq!(slots.workspace, allocation);
assert_eq!(slots.is_static, ZSTD_CWKSP_DYNAMIC_ALLOC);
let mut failure_context = CwkspTestContext::default();
let mut failure_slots = CwkspTestSlots::default();
let failure_state = cwksp_test_state(&mut failure_context, &mut failure_slots);
let result = unsafe { ZSTD_rust_cwkspCreate(&failure_state, 5678) };
assert_eq!(result, ERROR(ZstdErrorCode::MemoryAllocation));
assert_eq!(failure_context.events, ["allocate"]);
assert!(failure_slots.workspace.is_null());
}
#[test]
fn cwksp_free_zeroes_metadata_before_releasing_the_owned_range() {
let mut backing = [0usize; 16];
let workspace = backing.as_mut_ptr().cast::<c_void>();
let workspace_size = backing.len() * size_of::<usize>();
let workspace_end = unsafe { workspace.cast::<u8>().add(workspace_size).cast() };
let mut context = CwkspTestContext::default();
let mut slots = CwkspTestSlots {
workspace,
workspace_end,
..Default::default()
};
let state = cwksp_test_state(&mut context, &mut slots);
unsafe { ZSTD_rust_cwkspFree(&state) };
assert_eq!(context.events, ["zero", "release"]);
assert_eq!(context.released_workspace, workspace);
assert_eq!(context.released_size, workspace_size);
}
#[repr(C)]
struct CwkspMoveTestValue {
values: [usize; 2],
}
unsafe extern "C" fn cwksp_move_test_copy(destination: *mut c_void, source: *const c_void) {
unsafe {
ptr::copy_nonoverlapping(
source.cast::<u8>(),
destination.cast::<u8>(),
size_of::<CwkspMoveTestValue>(),
);
}
}
unsafe extern "C" fn cwksp_move_test_zero(source: *mut c_void) {
unsafe {
ptr::write_bytes(source.cast::<u8>(), 0, size_of::<CwkspMoveTestValue>());
}
}
#[test]
fn cwksp_move_copies_metadata_then_invalidates_the_source() {
let mut destination = CwkspMoveTestValue { values: [0, 0] };
let mut source = CwkspMoveTestValue {
values: [0x1234, 0x5678],
};
unsafe {
ZSTD_rust_cwkspMove(
(&mut destination as *mut CwkspMoveTestValue).cast(),
(&mut source as *mut CwkspMoveTestValue).cast(),
cwksp_move_test_copy,
cwksp_move_test_zero,
)
};
assert_eq!(destination.values, [0x1234, 0x5678]);
assert_eq!(source.values, [0, 0]);
}
struct EstimateCCtxSizeTestContext {
compression_levels: Vec<c_int>,
source_size_hints: Vec<u64>,