From f7a2576761b266ecf98f4a39354dfdc2ff028154 Mon Sep 17 00:00:00 2001 From: ddidderr Date: Sat, 18 Jul 2026 17:34:32 +0200 Subject: [PATCH] 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 --- programs/fileio.c | 113 ++++++++------ rust/src/fileio_backend.rs | 311 +++++++++++++++++++++++++++++++++++++ 2 files changed, 378 insertions(+), 46 deletions(-) diff --git a/programs/fileio.c b/programs/fileio.c index d056e6c9f..c3754504e 100644 --- a/programs/fileio.c +++ b/programs/fileio.c @@ -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 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 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) diff --git a/rust/src/fileio_backend.rs b/rust/src/fileio_backend.rs index 3a89d224e..26f1e62f4 100644 --- a/rust/src/fileio_backend.rs +++ b/rust/src/fileio_backend.rs @@ -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 { 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::(), 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");