feat(cli): move dictionary mmap lifecycle into Rust

Move the platform-specific dictionary mapping and release operations behind the
Rust CLI archive while keeping size policy, diagnostics, and FIO_Dict_t
ownership decisions in C. POSIX descriptors and Windows handles are cleaned up
on every failure path, and zero-length dictionaries use an owned sentinel so
the existing non-null dictionary invariant remains valid.

Test Plan:
- cargo test --manifest-path rust/cli/Cargo.toml --lib fileio_backend (24 passed)
- cargo clippy --manifest-path rust/cli/Cargo.toml --lib -- -D warnings
- make -C programs -j2 zstd
- --mmap-dict compression/decompression round trip with programs/README.md
- programs/zstd --mmap-dict --test on the generated frame
- rustfmt +nightly --edition 2021 rust/src/fileio_backend.rs --check
- git diff --check
This commit is contained in:
2026-07-18 17:34:32 +02:00
parent c057bcbbdf
commit f7a2576761
2 changed files with 378 additions and 46 deletions
+67 -46
View File
@@ -366,6 +366,22 @@ int FIO_rust_setDictBufferMalloc(const char* fileName,
size_t maxSize,
void** buffer,
size_t* loadedSize);
#if (PLATFORM_POSIX_VERSION > 0) || defined(_MSC_VER) || defined(_WIN32)
enum {
FIO_DICT_MMAP_SUCCESS = 0,
FIO_DICT_MMAP_OPEN_FAILED = 1,
FIO_DICT_MMAP_TOO_LARGE = 2,
FIO_DICT_MMAP_FAILED = 3,
FIO_DICT_MMAP_VIEW_FAILED = 4,
};
int FIO_rust_setDictBufferMMap(const char* fileName,
unsigned long long expectedFileSize,
size_t maxSize,
void** buffer,
size_t* mappedSize,
void** dictHandle);
void FIO_rust_munmapDict(void** buffer, size_t* bufferSize, void** dictHandle);
#endif
int FIO_rust_removeFile(const char* path);
int FIO_rust_passThrough(ReadPoolCtx_t* readCtx, WritePoolCtx_t* writeCtx);
#ifdef ZSTD_LZ4COMPRESS
@@ -640,18 +656,16 @@ static size_t FIO_setDictBufferMalloc(FIO_Dict_t* dict, const char* fileName, FI
}
#if (PLATFORM_POSIX_VERSION > 0)
#include <sys/mman.h>
static void FIO_munmap(FIO_Dict_t* dict)
{
munmap(dict->dictBuffer, dict->dictBufferSize);
dict->dictBuffer = NULL;
dict->dictBufferSize = 0;
FIO_rust_munmapDict(&dict->dictBuffer, &dict->dictBufferSize, NULL);
}
static size_t FIO_setDictBufferMMap(FIO_Dict_t* dict, const char* fileName, FIO_prefs_t* const prefs, stat_t* dictFileStat)
{
int fileHandle;
U64 fileSize;
size_t const dictSizeMax = prefs->patchFromMode ? prefs->memLimit : DICTSIZE_MAX;
void** bufferPtr = &dict->dictBuffer;
int status;
assert(bufferPtr != NULL);
assert(dictFileStat != NULL);
@@ -660,41 +674,45 @@ static size_t FIO_setDictBufferMMap(FIO_Dict_t* dict, const char* fileName, FIO_
DISPLAYLEVEL(4,"Loading %s as dictionary \n", fileName);
fileHandle = open(fileName, O_RDONLY);
fileSize = UTIL_getFileSizeStat(dictFileStat);
if (fileSize > dictSizeMax) {
EXM_THROW(34, "Dictionary file %s is too large (> %u bytes)",
fileName, (unsigned)dictSizeMax); /* avoid extreme cases */
}
if (fileHandle == -1) {
status = FIO_rust_setDictBufferMMap(fileName,
(unsigned long long)fileSize,
dictSizeMax,
bufferPtr,
&dict->dictBufferSize,
NULL);
if (status == FIO_DICT_MMAP_SUCCESS) return dict->dictBufferSize;
if (status == FIO_DICT_MMAP_OPEN_FAILED) {
EXM_THROW(33, "Couldn't open dictionary %s: %s", fileName, strerror(errno));
}
fileSize = UTIL_getFileSizeStat(dictFileStat);
{
size_t const dictSizeMax = prefs->patchFromMode ? prefs->memLimit : DICTSIZE_MAX;
if (fileSize > dictSizeMax) {
EXM_THROW(34, "Dictionary file %s is too large (> %u bytes)",
fileName, (unsigned)dictSizeMax); /* avoid extreme cases */
}
if (status == FIO_DICT_MMAP_TOO_LARGE) {
EXM_THROW(34, "Dictionary file %s is too large (> %u bytes)",
fileName, (unsigned)dictSizeMax); /* avoid extreme cases */
}
*bufferPtr = mmap(NULL, (size_t)fileSize, PROT_READ, MAP_PRIVATE, fileHandle, 0);
if (*bufferPtr==NULL) EXM_THROW(34, "%s", strerror(errno));
close(fileHandle);
return (size_t)fileSize;
if (status == FIO_DICT_MMAP_FAILED) {
EXM_THROW(34, "%s", strerror(errno));
}
assert(0); /* unexpected Rust status */
return 0;
}
#elif defined(_MSC_VER) || defined(_WIN32)
#include <windows.h>
static void FIO_munmap(FIO_Dict_t* dict)
{
UnmapViewOfFile(dict->dictBuffer);
CloseHandle(dict->dictHandle);
dict->dictBuffer = NULL;
dict->dictBufferSize = 0;
FIO_rust_munmapDict(&dict->dictBuffer,
&dict->dictBufferSize,
(void**)&dict->dictHandle);
}
static size_t FIO_setDictBufferMMap(FIO_Dict_t* dict, const char* fileName, FIO_prefs_t* const prefs, stat_t* dictFileStat)
{
HANDLE fileHandle, mapping;
U64 fileSize;
size_t const dictSizeMax = prefs->patchFromMode ? prefs->memLimit : DICTSIZE_MAX;
void** bufferPtr = &dict->dictBuffer;
int status;
assert(bufferPtr != NULL);
assert(dictFileStat != NULL);
@@ -703,31 +721,34 @@ static size_t FIO_setDictBufferMMap(FIO_Dict_t* dict, const char* fileName, FIO_
DISPLAYLEVEL(4,"Loading %s as dictionary \n", fileName);
fileHandle = CreateFileA(fileName, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_READONLY, NULL);
fileSize = UTIL_getFileSizeStat(dictFileStat);
if (fileSize > dictSizeMax) {
EXM_THROW(34, "Dictionary file %s is too large (> %u bytes)",
fileName, (unsigned)dictSizeMax); /* avoid extreme cases */
}
if (fileHandle == INVALID_HANDLE_VALUE) {
status = FIO_rust_setDictBufferMMap(fileName,
(unsigned long long)fileSize,
dictSizeMax,
bufferPtr,
&dict->dictBufferSize,
(void**)&dict->dictHandle);
if (status == FIO_DICT_MMAP_SUCCESS) return dict->dictBufferSize;
if (status == FIO_DICT_MMAP_OPEN_FAILED) {
EXM_THROW(33, "Couldn't open dictionary %s: %s", fileName, strerror(errno));
}
fileSize = UTIL_getFileSizeStat(dictFileStat);
{
size_t const dictSizeMax = prefs->patchFromMode ? prefs->memLimit : DICTSIZE_MAX;
if (fileSize > dictSizeMax) {
EXM_THROW(34, "Dictionary file %s is too large (> %u bytes)",
fileName, (unsigned)dictSizeMax); /* avoid extreme cases */
}
if (status == FIO_DICT_MMAP_TOO_LARGE) {
EXM_THROW(34, "Dictionary file %s is too large (> %u bytes)",
fileName, (unsigned)dictSizeMax); /* avoid extreme cases */
}
mapping = CreateFileMapping(fileHandle, NULL, PAGE_READONLY, 0, 0, NULL);
if (mapping == NULL) {
if (status == FIO_DICT_MMAP_FAILED) {
EXM_THROW(35, "Couldn't map dictionary %s: %s", fileName, strerror(errno));
}
*bufferPtr = MapViewOfFile(mapping, FILE_MAP_READ, 0, 0, (DWORD)fileSize); /* we can only cast to DWORD here because dictSize <= 2GB */
if (*bufferPtr==NULL) EXM_THROW(36, "%s", strerror(errno));
dict->dictHandle = fileHandle;
return (size_t)fileSize;
if (status == FIO_DICT_MMAP_VIEW_FAILED) {
EXM_THROW(36, "%s", strerror(errno));
}
assert(0); /* unexpected Rust status */
return 0;
}
#else
static size_t FIO_setDictBufferMMap(FIO_Dict_t* dict, const char* fileName, FIO_prefs_t* const prefs, stat_t* dictFileStat)
+311
View File
@@ -27,6 +27,14 @@ const FIO_DICT_LOAD_TOO_LARGE: c_int = 2;
const FIO_DICT_LOAD_ALLOCATION_FAILED: c_int = 3;
const FIO_DICT_LOAD_READ_FAILED: c_int = 4;
const FIO_DICT_MMAP_SUCCESS: c_int = 0;
const FIO_DICT_MMAP_OPEN_FAILED: c_int = 1;
const FIO_DICT_MMAP_TOO_LARGE: c_int = 2;
const FIO_DICT_MMAP_FAILED: c_int = 3;
#[cfg(windows)]
const FIO_DICT_MMAP_VIEW_FAILED: c_int = 4;
const FIO_DICT_MMAP_EMPTY_ALLOCATION_SIZE: usize = 1;
const FIO_DICT_STAT_SUCCESS: c_int = 0;
const FIO_DICT_STAT_FAILED: c_int = 1;
const FIO_DICT_STAT_NON_REGULAR: c_int = 2;
@@ -44,6 +52,65 @@ unsafe extern "C" {
fn fopen(file_name: *const c_char, mode: *const c_char) -> *mut c_void;
}
#[cfg(windows)]
const INVALID_HANDLE_VALUE: *mut c_void = -1isize as *mut c_void;
#[cfg(windows)]
const GENERIC_READ: u32 = 0x8000_0000;
#[cfg(windows)]
const FILE_SHARE_READ: u32 = 0x0000_0001;
#[cfg(windows)]
const OPEN_EXISTING: u32 = 3;
#[cfg(windows)]
const FILE_ATTRIBUTE_READONLY: u32 = 0x0000_0001;
#[cfg(windows)]
const PAGE_READONLY: u32 = 0x02;
#[cfg(windows)]
const FILE_MAP_READ: u32 = 0x0004;
#[cfg(windows)]
#[link(name = "kernel32")]
unsafe extern "system" {
fn CreateFileA(
file_name: *const c_char,
desired_access: u32,
share_mode: u32,
security_attributes: *mut c_void,
creation_disposition: u32,
flags_and_attributes: u32,
template_file: *mut c_void,
) -> *mut c_void;
fn CreateFileMappingA(
file: *mut c_void,
attributes: *mut c_void,
protect: u32,
maximum_size_high: u32,
maximum_size_low: u32,
name: *const c_char,
) -> *mut c_void;
fn MapViewOfFile(
mapping: *mut c_void,
desired_access: u32,
file_offset_high: u32,
file_offset_low: u32,
number_of_bytes_to_map: usize,
) -> *mut c_void;
fn UnmapViewOfFile(address: *const c_void) -> i32;
fn CloseHandle(handle: *mut c_void) -> i32;
}
unsafe fn publish_empty_dict_mapping(buffer: *mut *mut c_void, mapped_size: *mut usize) -> c_int {
let allocation = unsafe { libc::malloc(FIO_DICT_MMAP_EMPTY_ALLOCATION_SIZE) };
if allocation.is_null() {
return FIO_DICT_MMAP_FAILED;
}
unsafe {
*buffer = allocation;
*mapped_size = 0;
}
FIO_DICT_MMAP_SUCCESS
}
fn path_from_c(path: *const c_char) -> Option<PathBuf> {
if path.is_null() {
return None;
@@ -212,6 +279,190 @@ pub unsafe extern "C" fn FIO_rust_setDictBufferMalloc(
FIO_DICT_LOAD_SUCCESS
}
/// Maps a dictionary file while leaving policy and diagnostics in the C CLI.
///
/// `mapped_size` is the file size, not the allocation size used for an empty
/// dictionary. The release leaf treats a non-null zero-sized mapping as that
/// one-byte owned allocation and frees it instead of calling `munmap()` with a
/// zero length.
#[no_mangle]
pub unsafe extern "C" fn FIO_rust_setDictBufferMMap(
file_name: *const c_char,
expected_file_size: u64,
max_size: usize,
buffer: *mut *mut c_void,
mapped_size: *mut usize,
dict_handle: *mut *mut c_void,
) -> c_int {
if buffer.is_null() || mapped_size.is_null() {
return FIO_DICT_MMAP_FAILED;
}
unsafe {
*buffer = ptr::null_mut();
*mapped_size = 0;
if !dict_handle.is_null() {
*dict_handle = ptr::null_mut();
}
}
let Some(expected_size) = usize::try_from(expected_file_size).ok() else {
return FIO_DICT_MMAP_TOO_LARGE;
};
if expected_size > max_size {
return FIO_DICT_MMAP_TOO_LARGE;
}
if file_name.is_null() {
return FIO_DICT_MMAP_SUCCESS;
}
#[cfg(unix)]
{
let file_descriptor = unsafe { libc::open(file_name, libc::O_RDONLY) };
if file_descriptor < 0 {
return FIO_DICT_MMAP_OPEN_FAILED;
}
if expected_size == 0 {
unsafe { libc::close(file_descriptor) };
return unsafe { publish_empty_dict_mapping(buffer, mapped_size) };
}
let mapping = unsafe {
libc::mmap(
ptr::null_mut(),
expected_size,
libc::PROT_READ,
libc::MAP_PRIVATE,
file_descriptor,
0,
)
};
unsafe { libc::close(file_descriptor) };
if mapping == (-1isize as *mut c_void) {
return FIO_DICT_MMAP_FAILED;
}
unsafe {
*buffer = mapping;
*mapped_size = expected_size;
}
FIO_DICT_MMAP_SUCCESS
}
#[cfg(windows)]
{
let file_handle = unsafe {
CreateFileA(
file_name,
GENERIC_READ,
FILE_SHARE_READ,
ptr::null_mut(),
OPEN_EXISTING,
FILE_ATTRIBUTE_READONLY,
ptr::null_mut(),
)
};
if file_handle.is_null() || file_handle == INVALID_HANDLE_VALUE {
return FIO_DICT_MMAP_OPEN_FAILED;
}
if expected_size == 0 {
unsafe { CloseHandle(file_handle) };
return unsafe { publish_empty_dict_mapping(buffer, mapped_size) };
}
let mapping = unsafe {
CreateFileMappingA(
file_handle,
ptr::null_mut(),
PAGE_READONLY,
0,
0,
ptr::null(),
)
};
if mapping.is_null() {
unsafe { CloseHandle(file_handle) };
return FIO_DICT_MMAP_FAILED;
}
let view = unsafe { MapViewOfFile(mapping, FILE_MAP_READ, 0, 0, expected_size) };
unsafe { CloseHandle(mapping) };
if view.is_null() {
unsafe { CloseHandle(file_handle) };
return FIO_DICT_MMAP_VIEW_FAILED;
}
if dict_handle.is_null() {
unsafe { CloseHandle(file_handle) };
return FIO_DICT_MMAP_FAILED;
}
unsafe {
*buffer = view;
*mapped_size = expected_size;
*dict_handle = file_handle;
}
FIO_DICT_MMAP_SUCCESS
}
#[cfg(not(any(unix, windows)))]
{
FIO_DICT_MMAP_FAILED
}
}
/// Releases a dictionary mapping and clears all C-visible ownership fields.
#[no_mangle]
pub unsafe extern "C" fn FIO_rust_munmapDict(
buffer: *mut *mut c_void,
buffer_size: *mut usize,
dict_handle: *mut *mut c_void,
) {
if buffer.is_null() || buffer_size.is_null() {
return;
}
let mapping = unsafe { *buffer };
let size = unsafe { *buffer_size };
#[cfg(unix)]
{
if !mapping.is_null() {
if size == 0 {
unsafe { libc::free(mapping) };
} else {
unsafe { libc::munmap(mapping, size) };
}
}
}
#[cfg(windows)]
{
if !mapping.is_null() {
if size == 0 {
unsafe { libc::free(mapping) };
} else {
unsafe { UnmapViewOfFile(mapping) };
}
}
if !dict_handle.is_null() {
let handle = unsafe { *dict_handle };
if !handle.is_null() && handle != INVALID_HANDLE_VALUE {
unsafe { CloseHandle(handle) };
}
}
}
unsafe {
*buffer = ptr::null_mut();
*buffer_size = 0;
if !dict_handle.is_null() {
*dict_handle = ptr::null_mut();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -388,6 +639,66 @@ mod tests {
fs::remove_file(&path).expect("remove temporary file");
}
#[cfg(unix)]
#[test]
fn maps_and_releases_dictionary() {
let path = temp_path("mapped-dictionary");
let payload = b"mapped dictionary payload";
fs::write(&path, payload).expect("create temporary file");
let c_path = c_path(&path);
let mut buffer = ptr::null_mut();
let mut mapped_size = usize::MAX;
let status = unsafe {
FIO_rust_setDictBufferMMap(
c_path.as_ptr(),
payload.len() as u64,
payload.len(),
&mut buffer,
&mut mapped_size,
ptr::null_mut(),
)
};
assert_eq!(status, FIO_DICT_MMAP_SUCCESS);
assert_eq!(mapped_size, payload.len());
let mapped = unsafe { std::slice::from_raw_parts(buffer.cast::<u8>(), mapped_size) };
assert_eq!(mapped, payload);
unsafe { FIO_rust_munmapDict(&mut buffer, &mut mapped_size, ptr::null_mut()) };
assert!(buffer.is_null());
assert_eq!(mapped_size, 0);
fs::remove_file(path).expect("remove temporary file");
}
#[cfg(unix)]
#[test]
fn maps_empty_dictionary_with_owned_zero_size_sentinel() {
let path = temp_path("mapped-empty-dictionary");
fs::write(&path, b"").expect("create empty temporary file");
let c_path = c_path(&path);
let mut buffer = ptr::null_mut();
let mut mapped_size = usize::MAX;
let status = unsafe {
FIO_rust_setDictBufferMMap(
c_path.as_ptr(),
0,
0,
&mut buffer,
&mut mapped_size,
ptr::null_mut(),
)
};
assert_eq!(status, FIO_DICT_MMAP_SUCCESS);
assert!(!buffer.is_null());
assert_eq!(mapped_size, 0);
unsafe { FIO_rust_munmapDict(&mut buffer, &mut mapped_size, ptr::null_mut()) };
assert!(buffer.is_null());
assert_eq!(mapped_size, 0);
fs::remove_file(path).expect("remove temporary file");
}
#[test]
fn accepts_regular_dictionary_file() {
let path = temp_path("regular-dictionary-stat");