feat(cli): move benchmark bridge into Rust

Move benchmark parameter normalization and dispatch into the Rust CLI archive while retaining the stable C launcher entry point. Keep compact CLI variants benchmark-free and add an explicit helpers feature for the C test generators.

Test Plan:

- cargo test --manifest-path rust/cli/Cargo.toml --no-default-features --features benchmark

- cargo test --manifest-path rust/cli/Cargo.toml --no-default-features --features helpers

- cargo clippy --manifest-path rust/cli/Cargo.toml --all-targets --no-default-features --features cli,compression,decompression,benchmark

- cargo clippy --manifest-path rust/cli/Cargo.toml --all-targets --no-default-features --features helpers

- make -B -C programs -j2 zstd zstd-small zstd-frugal

- make -B -C tests -j2 test-cli-tests

- programs/zstd -b1i1 -q
This commit is contained in:
2026-07-18 03:18:58 +02:00
parent 3a10d86c76
commit 96c7f3b900
6 changed files with 185 additions and 86 deletions
+162 -25
View File
@@ -10,11 +10,11 @@
//! writes, dictionary loading, streaming, and metadata preservation remain in
//! `programs/fileio.c` for this first migration step.
//!
//! Benchmark mode (`-b`) parses here and dispatches through the
//! `ZSTD_rust_cli_bench` bridge in `programs/zstdcli.c`: the run/timing loop
//! (benchfn, timefn) is Rust, while orchestration and result formatting
//! (`benchzstd.c`) remain C behind the preprocessor-gated bridge, so builds
//! with `ZSTD_NOBENCH` never reference benchmark symbols.
//! Benchmark mode (`-b`) parses and dispatches through the Rust CLI archive.
//! The bridge preserves the C CLI's argument normalization and uses the
//! existing Rust `benchzstd` APIs. Builds without the `benchmark` feature
//! return the same unavailable-build result as `ZSTD_NOBENCH` builds without
//! referencing benchmark symbols.
//!
//! Recursive expansion (`-r`), `--filelist`, the output-directory modes, and
//! `--list` reuse the C `FileNamesTable` helpers from `programs/util.c` and
@@ -104,7 +104,7 @@ struct FIO_ctx_t {
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq)]
struct ZSTD_compressionParameters {
pub(crate) struct ZSTD_compressionParameters {
windowLog: u32,
chainLog: u32,
hashLog: u32,
@@ -295,22 +295,6 @@ unsafe extern "C" {
display_level: c_int,
) -> c_int;
/// Benchmark bridge implemented by the `programs/zstdcli.c` shim, which
/// owns the `ZSTD_NOBENCH` preprocessor decision. Returns the benchmark
/// result (>= 0), or -1 when benchmarking is compiled out.
fn ZSTD_rust_cli_bench(
file_names: *const *const c_char,
nb_files: c_uint,
dict_file_name: *const c_char,
start_level: c_int,
end_level: c_int,
compression_params: *const ZSTD_compressionParameters,
display_level: c_int,
nb_seconds: c_uint,
block_size: usize,
nb_workers: c_int,
mode: c_int,
) -> c_int;
/// Narrow bridge to `programs/dibio.c`. The file loader and dictionary
/// algorithms remain on the C/Rust library side of this boundary.
#[cfg(feature = "compression")]
@@ -2374,9 +2358,122 @@ fn run_list(cli: &Cli) -> i32 {
unsafe { FIO_listMultipleFiles(inputs.len() as c_uint, inputs.as_ptr(), cli.display_level) }
}
/// Runs benchmark mode through the C bridge. Level clamping against
/// `ZSTD_maxCLevel()` happens on the C side, where the symbol is always
/// available when benchmarking is compiled in. No input file means a
#[cfg(feature = "benchmark")]
fn normalize_benchmark_levels(
start_c_level: c_int,
end_c_level: c_int,
mode: c_int,
max_c_level: c_int,
) -> (c_int, c_int) {
let mut start_level = start_c_level;
let mut end_level = end_c_level;
if mode == crate::benchzstd::BMK_decodeOnly {
start_level = 0;
end_level = 0;
}
if start_level > max_c_level {
start_level = max_c_level;
}
if end_level > max_c_level {
end_level = max_c_level;
}
if end_level < start_level {
end_level = start_level;
}
(start_level, end_level)
}
/// Benchmark bridge formerly implemented by `programs/zstdcli.c`.
///
/// Keep the exported name because it is a useful program-side ABI boundary,
/// but make the Rust CLI archive own both the normalization and dispatch.
#[cfg(feature = "benchmark")]
#[no_mangle]
pub unsafe extern "C" fn ZSTD_rust_cli_bench(
file_names: *const *const c_char,
nb_files: c_uint,
dict_file_name: *const c_char,
start_c_level: c_int,
end_c_level: c_int,
compression_params: *const ZSTD_compressionParameters,
display_level: c_int,
nb_seconds: c_uint,
block_size: usize,
nb_workers: c_int,
mode: c_int,
) -> c_int {
let mut advanced_params = crate::benchzstd::BMK_initAdvancedParams();
advanced_params.nbSeconds = nb_seconds;
advanced_params.blockSize = block_size;
advanced_params.nbWorkers = nb_workers;
advanced_params.mode = mode;
let (start_level, end_level) =
normalize_benchmark_levels(start_c_level, end_c_level, mode, unsafe {
ZSTD_maxCLevel()
});
let compression_params = compression_params.cast();
if nb_files == 0 {
/* No input file: benchmark a synthetic sample (lorem generator). */
unsafe {
crate::benchzstd::BMK_syntheticTest(
-1.0,
start_level,
end_level,
compression_params,
display_level,
&advanced_params,
)
}
} else {
unsafe {
crate::benchzstd::BMK_benchFilesAdvanced(
file_names,
nb_files,
dict_file_name,
start_level,
end_level,
compression_params,
display_level,
&advanced_params,
)
}
}
}
/// `ZSTD_NOBENCH` compatibility bridge for compact CLI archives.
#[cfg(not(feature = "benchmark"))]
#[no_mangle]
pub unsafe extern "C" fn ZSTD_rust_cli_bench(
file_names: *const *const c_char,
nb_files: c_uint,
dict_file_name: *const c_char,
start_c_level: c_int,
end_c_level: c_int,
compression_params: *const ZSTD_compressionParameters,
display_level: c_int,
nb_seconds: c_uint,
block_size: usize,
nb_workers: c_int,
mode: c_int,
) -> c_int {
let _ = (
file_names,
nb_files,
dict_file_name,
start_c_level,
end_c_level,
compression_params,
display_level,
nb_seconds,
block_size,
nb_workers,
mode,
);
-1
}
/// Runs benchmark mode through the Rust bridge. No input file means a
/// synthetic-sample benchmark, matching the C CLI.
fn run_bench(cli: &Cli) -> Result<i32, String> {
let inputs: Vec<*const c_char> = cli.inputs.iter().map(|value| value.as_ptr()).collect();
@@ -3359,4 +3456,44 @@ mod tests {
assert_eq!(cli.operation, Operation::Bench);
assert!(cli.bench_decode);
}
#[cfg(feature = "benchmark")]
#[test]
fn benchmark_level_normalization_matches_c_bridge() {
assert_eq!(
normalize_benchmark_levels(100, 4, crate::benchzstd::BMK_both, 22),
(22, 22)
);
assert_eq!(
normalize_benchmark_levels(7, 3, crate::benchzstd::BMK_both, 22),
(7, 7)
);
assert_eq!(
normalize_benchmark_levels(7, 3, crate::benchzstd::BMK_decodeOnly, 22),
(0, 0)
);
}
#[cfg(not(feature = "benchmark"))]
#[test]
fn benchmark_bridge_returns_unavailable_without_feature() {
assert_eq!(
unsafe {
ZSTD_rust_cli_bench(
ptr::null(),
0,
ptr::null(),
3,
3,
ptr::null(),
0,
3,
0,
1,
0,
)
},
-1
);
}
}