From be40c862cd819783f99d48767cb3cfb513ce2c49 Mon Sep 17 00:00:00 2001 From: Brian Koropoff Date: Mon, 23 Feb 2026 20:19:04 -0800 Subject: [PATCH] Add shared memory support This requires a new associated type for mapped regions which would break backward compatibility without default associated types, which is currently unstable in Rust. Instead, a new `ShmVfs` trait is introduced with alternate registration methods. Most of the code is shared between the two except for the open/close glue functions for `ShmVfs`, which must initialize and tear down a derived file handle structure. The lifetime semantics of returned mapped regions is a little odd: the region is only accessed once by sqlite immediately upon return, but we don't know for sure when it's done until it explicitly requests a new region, unmaps all regions, or closes the handle. Therefore the API glue obtains a region from the trait as an owned `AsMut<[u8]>` and holds on to it until its known to no longer be referenced, at which point its `Drop` impl can release resources. The previous code which cast the `sqlite3_file` directly to `&mut` looked like it could lead to undefined behavior (it's UB to create an `&mut` to uninitialized memory, and even if sqlite zeroes the memory first, if there were any embedded `NonZero` you're still in UB territory). I instead `ptr::write` and `ptr::read` the whole thing. --- src/flags.rs | 26 ++++ src/vfs.rs | 395 ++++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 356 insertions(+), 65 deletions(-) diff --git a/src/flags.rs b/src/flags.rs index b5254ae..3699b74 100644 --- a/src/flags.rs +++ b/src/flags.rs @@ -178,3 +178,29 @@ impl From for LockLevel { } } } + +#[derive(Copy, Clone, Debug)] +pub enum ShmLockMode { + LockShared, + LockExclusive, + UnlockShared, + UnlockExclusive, +} + +impl TryFrom for ShmLockMode { + type Error = i32; + + fn try_from(flags: i32) -> Result { + const LOCK_SHARED: i32 = vars::SQLITE_SHM_LOCK | vars::SQLITE_SHM_SHARED; + const LOCK_EXCLUSIVE: i32 = vars::SQLITE_SHM_LOCK | vars::SQLITE_SHM_EXCLUSIVE; + const UNLOCK_SHARED: i32 = vars::SQLITE_SHM_UNLOCK | vars::SQLITE_SHM_SHARED; + const UNLOCK_EXCLUSIVE: i32 = vars::SQLITE_SHM_UNLOCK | vars::SQLITE_SHM_EXCLUSIVE; + Ok(match flags { + LOCK_SHARED => Self::LockShared, + LOCK_EXCLUSIVE => Self::LockExclusive, + UNLOCK_SHARED => Self::UnlockShared, + UNLOCK_EXCLUSIVE => Self::UnlockExclusive, + _ => return Err(vars::SQLITE_IOERR), + }) + } +} diff --git a/src/vfs.rs b/src/vfs.rs index 186e432..0f03d58 100644 --- a/src/vfs.rs +++ b/src/vfs.rs @@ -1,4 +1,4 @@ -use crate::flags::{AccessFlags, LockLevel, OpenOpts}; +use crate::flags::{AccessFlags, LockLevel, OpenOpts, ShmLockMode}; use crate::logger::SqliteLogger; use crate::vars::SQLITE_ERROR; use crate::{ffi, vars}; @@ -7,7 +7,7 @@ use alloc::boxed::Box; use alloc::ffi::CString; use alloc::format; use alloc::string::String; -use core::mem::{self, ManuallyDrop, MaybeUninit, size_of}; +use core::mem::{ManuallyDrop, size_of}; use core::slice; use core::{ ffi::{CStr, c_char, c_int, c_void}, @@ -45,7 +45,15 @@ pub type VfsResult = Result; struct FileWrapper { file: ffi::sqlite3_file, vfs: *mut ffi::sqlite3_vfs, - handle: MaybeUninit, + handle: Handle, +} + +// ShmFileWrapper needs to be repr(C) as it extends `FileWrapper` +#[repr(C)] +struct ShmFileWrapper { + base: FileWrapper, + // Last region mapped from VFS + last_region: Option, } struct AppData { @@ -133,6 +141,18 @@ macro_rules! unwrap_file { }; } +macro_rules! unwrap_shm_file { + ($p_file:expr, $t_vfs:ty) => { + unsafe { + let out: VfsResult<&mut ShmFileWrapper<<$t_vfs>::Handle, <$t_vfs>::Region>> = $p_file + .cast::::Handle, <$t_vfs>::Region>>() + .as_mut() + .ok_or(vars::SQLITE_INTERNAL); + out + } + }; +} + pub trait VfsHandle: Send { fn readonly(&self) -> bool; fn in_memory(&self) -> bool; @@ -188,6 +208,30 @@ pub trait Vfs: Send + Sync { } } +pub trait ShmVfs: Vfs { + type Region: AsMut<[u8]> + Send; + + fn shm_map( + &self, + handle: &mut Self::Handle, + region_idx: usize, + region_size: usize, + extend: bool, + ) -> VfsResult>; + + fn shm_lock( + &self, + handle: &mut Self::Handle, + offset: u32, + count: u32, + mode: ShmLockMode, + ) -> VfsResult<()>; + + fn shm_barrier(&self, handle: &mut Self::Handle); + + fn shm_unmap(&self, handle: &mut Self::Handle, delete: bool) -> VfsResult<()>; +} + #[derive(Clone)] pub struct SqliteApi { register: unsafe extern "C" fn(arg1: *mut ffi::sqlite3_vfs, arg2: c_int) -> c_int, @@ -253,7 +297,30 @@ pub fn register_static( vfs: T, opts: RegisterOpts, ) -> VfsResult { - register_inner(SqliteApi::new_static(), name, vfs, opts) + register_inner( + SqliteApi::new_static(), + name, + vfs, + opts, + io_methods::(), + vfs_methods::(), + ) +} + +#[cfg(feature = "static")] +pub fn register_shm_static( + name: CString, + vfs: T, + opts: RegisterOpts, +) -> VfsResult { + register_inner( + SqliteApi::new_static(), + name, + vfs, + opts, + shm_io_methods::(), + shm_vfs_methods::(), + ) } /// Register a vfs with `SQLite` using the dynamic API. This API is available when @@ -269,7 +336,37 @@ pub unsafe fn register_dynamic( ) -> VfsResult { let api = unsafe { p_api.as_ref() }.ok_or(vars::SQLITE_INTERNAL)?; let sqlite_api = unsafe { SqliteApi::new_dynamic(api)? }; - register_inner(sqlite_api, name, vfs, opts) + register_inner( + sqlite_api, + name, + vfs, + opts, + io_methods::(), + vfs_methods::(), + ) +} + +/// Register a SHM vfs with `SQLite` using the dynamic API. This API is available when +/// `SQLite` is initializing extensions. +/// # Safety +/// `p_api` must be a valid, aligned pointer to a `sqlite3_api_routines` struct +#[cfg(feature = "dynamic")] +pub unsafe fn register_shm_dynamic( + p_api: *mut ffi::sqlite3_api_routines, + name: CString, + vfs: T, + opts: RegisterOpts, +) -> VfsResult { + let api = unsafe { p_api.as_ref() }.ok_or(vars::SQLITE_INTERNAL)?; + let sqlite_api = unsafe { SqliteApi::new_dynamic(api)? }; + register_inner( + sqlite_api, + name, + vfs, + opts, + shm_io_methods::(), + shm_vfs_methods::(), + ) } fn register_inner( @@ -277,6 +374,8 @@ fn register_inner( name: CString, vfs: T, opts: RegisterOpts, + io_methods: ffi::sqlite3_io_methods, + vfs_methods: ffi::sqlite3_vfs, ) -> VfsResult { let version = unsafe { (sqlite_api.libversion_number)() }; if version < MIN_SQLITE_VERSION_NUMBER { @@ -286,28 +385,6 @@ fn register_inner( ); } - let io_methods = ffi::sqlite3_io_methods { - iVersion: 3, - xClose: Some(x_close::), - xRead: Some(x_read::), - xWrite: Some(x_write::), - xTruncate: Some(x_truncate::), - xSync: Some(x_sync::), - xFileSize: Some(x_file_size::), - xLock: Some(x_lock::), - xUnlock: Some(x_unlock::), - xCheckReservedLock: Some(x_check_reserved_lock::), - xFileControl: Some(x_file_control::), - xSectorSize: Some(x_sector_size::), - xDeviceCharacteristics: Some(x_device_characteristics::), - xShmMap: None, - xShmLock: None, - xShmBarrier: None, - xShmUnmap: None, - xFetch: None, - xUnfetch: None, - }; - let logger = SqliteLogger::new(sqlite_api.log); let p_name = ManuallyDrop::new(name).as_ptr(); @@ -315,17 +392,38 @@ fn register_inner( let vfs_register = sqlite_api.register; let p_appdata = Box::into_raw(Box::new(AppData { base_vfs, vfs, io_methods, sqlite_api })); - let filewrapper_size: c_int = size_of::>() - .try_into() - .map_err(|_| vars::SQLITE_INTERNAL)?; - let p_vfs = Box::into_raw(Box::new(ffi::sqlite3_vfs { + zName: p_name, + pAppData: p_appdata.cast(), + ..vfs_methods + })); + + let result = unsafe { vfs_register(p_vfs, opts.make_default.into()) }; + if result != vars::SQLITE_OK { + // cleanup memory + unsafe { + drop(Box::from_raw(p_vfs)); + drop(Box::from_raw(p_appdata)); + drop(CString::from_raw(p_name as *mut c_char)); + }; + Err(result) + } else { + Ok(logger) + } +} + +fn vfs_methods() -> ffi::sqlite3_vfs { + ffi::sqlite3_vfs { iVersion: 3, - szOsFile: filewrapper_size, + szOsFile: const { + let size = size_of::>(); + assert!(size < i32::MAX as usize); + size as i32 + }, mxPathname: DEFAULT_MAX_PATH_LEN, pNext: null_mut(), - zName: p_name, - pAppData: p_appdata.cast(), + zName: null_mut(), + pAppData: null_mut(), xOpen: Some(x_open::), xDelete: Some(x_delete::), xAccess: Some(x_access::), @@ -342,19 +440,53 @@ fn register_inner( xSetSystemCall: None, xGetSystemCall: None, xNextSystemCall: None, - })); + } +} - let result = unsafe { vfs_register(p_vfs, opts.make_default.into()) }; - if result != vars::SQLITE_OK { - // cleanup memory - unsafe { - drop(Box::from_raw(p_vfs)); - drop(Box::from_raw(p_appdata)); - drop(CString::from_raw(p_name as *mut c_char)); - }; - Err(result) - } else { - Ok(logger) +fn shm_vfs_methods() -> ffi::sqlite3_vfs { + ffi::sqlite3_vfs { + szOsFile: const { + let size = size_of::>(); + assert!(size < i32::MAX as usize); + size as i32 + }, + xOpen: Some(x_shm_open::), + ..vfs_methods::() + } +} + +fn io_methods() -> ffi::sqlite3_io_methods { + ffi::sqlite3_io_methods { + iVersion: 3, + xClose: Some(x_close::), + xRead: Some(x_read::), + xWrite: Some(x_write::), + xTruncate: Some(x_truncate::), + xSync: Some(x_sync::), + xFileSize: Some(x_file_size::), + xLock: Some(x_lock::), + xUnlock: Some(x_unlock::), + xCheckReservedLock: Some(x_check_reserved_lock::), + xFileControl: Some(x_file_control::), + xSectorSize: Some(x_sector_size::), + xDeviceCharacteristics: Some(x_device_characteristics::), + xShmMap: None, + xShmLock: None, + xShmBarrier: None, + xShmUnmap: None, + xFetch: None, + xUnfetch: None, + } +} + +fn shm_io_methods() -> ffi::sqlite3_io_methods { + ffi::sqlite3_io_methods { + xClose: Some(x_shm_close::), + xShmMap: Some(x_shm_map::), + xShmLock: Some(x_shm_lock::), + xShmBarrier: Some(x_shm_barrier::), + xShmUnmap: Some(x_shm_unmap::), + ..io_methods::() } } @@ -371,7 +503,6 @@ unsafe extern "C" fn x_open( let vfs = unwrap_vfs!(p_vfs, T)?; let handle = vfs.open(name.as_ref().map(|s| s.as_ref()), opts)?; - let out_file = unwrap_file!(p_file, T)?; let appdata = unwrap_appdata!(p_vfs, T)?; if let Some(p_out_flags) = unsafe { p_out_flags.as_mut() } { @@ -385,9 +516,62 @@ unsafe extern "C" fn x_open( *p_out_flags = out_flags; } - out_file.file.pMethods = &appdata.io_methods; - out_file.vfs = p_vfs; - out_file.handle.write(handle); + let out_file = p_file.cast::>(); + unsafe { + core::ptr::write( + out_file, + FileWrapper { + file: ffi::sqlite3_file { pMethods: &appdata.io_methods }, + vfs: p_vfs, + handle: handle, + }, + ); + } + + Ok(vars::SQLITE_OK) + }) +} + +unsafe extern "C" fn x_shm_open( + p_vfs: *mut ffi::sqlite3_vfs, + z_name: ffi::sqlite3_filename, + p_file: *mut ffi::sqlite3_file, + flags: c_int, + p_out_flags: *mut c_int, +) -> c_int { + fallible(|| { + let opts = flags.into(); + let name = unsafe { lossy_cstr(z_name) }.ok(); + let vfs = unwrap_vfs!(p_vfs, T)?; + let handle = vfs.open(name.as_ref().map(|s| s.as_ref()), opts)?; + + let appdata = unwrap_appdata!(p_vfs, T)?; + + if let Some(p_out_flags) = unsafe { p_out_flags.as_mut() } { + let mut out_flags = flags; + if handle.readonly() { + out_flags |= vars::SQLITE_OPEN_READONLY; + } + if handle.in_memory() { + out_flags |= vars::SQLITE_OPEN_MEMORY; + } + *p_out_flags = out_flags; + } + + let out_file = p_file.cast::>(); + unsafe { + core::ptr::write( + out_file, + ShmFileWrapper { + base: FileWrapper { + file: ffi::sqlite3_file { pMethods: &appdata.io_methods }, + vfs: p_vfs, + handle: handle, + }, + last_region: None, + }, + ); + } Ok(vars::SQLITE_OK) }) @@ -447,11 +631,20 @@ unsafe extern "C" fn x_full_pathname( unsafe extern "C" fn x_close(p_file: *mut ffi::sqlite3_file) -> c_int { fallible(|| { - let file = unwrap_file!(p_file, T)?; + let file = unsafe { core::ptr::read(p_file.cast::>()) }; let vfs = unwrap_vfs!(file.vfs, T)?; - let handle = mem::replace(&mut file.handle, MaybeUninit::uninit()); - let handle = unsafe { handle.assume_init() }; - vfs.close(handle)?; + vfs.close(file.handle)?; + Ok(vars::SQLITE_OK) + }) +} + +unsafe extern "C" fn x_shm_close(p_file: *mut ffi::sqlite3_file) -> c_int { + fallible(|| { + let mut file = + unsafe { core::ptr::read(p_file.cast::>()) }; + let vfs = unwrap_vfs!(file.base.vfs, T)?; + drop(file.last_region.take()); + vfs.close(file.base.handle)?; Ok(vars::SQLITE_OK) }) } @@ -468,7 +661,7 @@ unsafe extern "C" fn x_read( let buf_len: usize = i_amt.try_into().map_err(|_| vars::SQLITE_IOERR_READ)?; let offset: usize = i_ofst.try_into().map_err(|_| vars::SQLITE_IOERR_READ)?; let buf = unsafe { slice::from_raw_parts_mut(buf.cast::(), buf_len) }; - vfs.read(unsafe { file.handle.assume_init_mut() }, offset, buf)?; + vfs.read(&mut file.handle, offset, buf)?; Ok(vars::SQLITE_OK) }) } @@ -485,7 +678,7 @@ unsafe extern "C" fn x_write( let buf_len: usize = i_amt.try_into().map_err(|_| vars::SQLITE_IOERR_WRITE)?; let offset: usize = i_ofst.try_into().map_err(|_| vars::SQLITE_IOERR_WRITE)?; let buf = unsafe { slice::from_raw_parts(buf.cast::(), buf_len) }; - let n = vfs.write(unsafe { file.handle.assume_init_mut() }, offset, buf)?; + let n = vfs.write(&mut file.handle, offset, buf)?; if n != buf_len { return Err(vars::SQLITE_IOERR_WRITE); } @@ -501,7 +694,7 @@ unsafe extern "C" fn x_truncate( let file = unwrap_file!(p_file, T)?; let vfs = unwrap_vfs!(file.vfs, T)?; let size: usize = size.try_into().map_err(|_| vars::SQLITE_IOERR_TRUNCATE)?; - vfs.truncate(unsafe { file.handle.assume_init_mut() }, size)?; + vfs.truncate(&mut file.handle, size)?; Ok(vars::SQLITE_OK) }) } @@ -510,7 +703,7 @@ unsafe extern "C" fn x_sync(p_file: *mut ffi::sqlite3_file, _flags: c_in fallible(|| { let file = unwrap_file!(p_file, T)?; let vfs = unwrap_vfs!(file.vfs, T)?; - vfs.sync(unsafe { file.handle.assume_init_mut() })?; + vfs.sync(&mut file.handle)?; Ok(vars::SQLITE_OK) }) } @@ -522,7 +715,7 @@ unsafe extern "C" fn x_file_size( fallible(|| { let file = unwrap_file!(p_file, T)?; let vfs = unwrap_vfs!(file.vfs, T)?; - let size = vfs.file_size(unsafe { file.handle.assume_init_mut() })?; + let size = vfs.file_size(&mut file.handle)?; let p_size = unsafe { p_size.as_mut() }.ok_or(vars::SQLITE_INTERNAL)?; *p_size = size.try_into().map_err(|_| vars::SQLITE_IOERR_FSTAT)?; Ok(vars::SQLITE_OK) @@ -534,7 +727,7 @@ unsafe extern "C" fn x_lock(p_file: *mut ffi::sqlite3_file, raw_lock: c_ let level: LockLevel = raw_lock.into(); let file = unwrap_file!(p_file, T)?; let vfs = unwrap_vfs!(file.vfs, T)?; - vfs.lock(unsafe { file.handle.assume_init_mut() }, level)?; + vfs.lock(&mut file.handle, level)?; Ok(vars::SQLITE_OK) }) } @@ -544,7 +737,7 @@ unsafe extern "C" fn x_unlock(p_file: *mut ffi::sqlite3_file, raw_lock: let level: LockLevel = raw_lock.into(); let file = unwrap_file!(p_file, T)?; let vfs = unwrap_vfs!(file.vfs, T)?; - vfs.unlock(unsafe { file.handle.assume_init_mut() }, level)?; + vfs.unlock(&mut file.handle, level)?; Ok(vars::SQLITE_OK) }) } @@ -557,7 +750,7 @@ unsafe extern "C" fn x_check_reserved_lock( let file = unwrap_file!(p_file, T)?; let vfs = unwrap_vfs!(file.vfs, T)?; unsafe { - *p_out = vfs.check_reserved_lock(file.handle.assume_init_mut())? as c_int; + *p_out = vfs.check_reserved_lock(&mut file.handle)? as c_int; } Ok(vars::SQLITE_OK) }) @@ -598,7 +791,7 @@ unsafe extern "C" fn x_file_control( }; let pragma = Pragma { name: &name, arg: arg.as_deref() }; - let (result, msg) = match vfs.pragma(unsafe { file.handle.assume_init_mut() }, pragma) { + let (result, msg) = match vfs.pragma(&mut file.handle, pragma) { Ok(msg) => (Ok(vars::SQLITE_OK), msg), Err(PragmaErr::NotFound) => (Err(vars::SQLITE_NOTFOUND), None), Err(PragmaErr::Fail(err, msg)) => (Err(err), msg), @@ -623,7 +816,7 @@ unsafe extern "C" fn x_sector_size(p_file: *mut ffi::sqlite3_file) -> c_ fallible(|| { let file = unwrap_file!(p_file, T)?; let vfs = unwrap_vfs!(file.vfs, T)?; - vfs.sector_size(unsafe { file.handle.assume_init_mut() }) + vfs.sector_size(&mut file.handle) }) } @@ -631,7 +824,79 @@ unsafe extern "C" fn x_device_characteristics(p_file: *mut ffi::sqlite3_ fallible(|| { let file = unwrap_file!(p_file, T)?; let vfs = unwrap_vfs!(file.vfs, T)?; - vfs.device_characteristics(unsafe { file.handle.assume_init_mut() }) + vfs.device_characteristics(&mut file.handle) + }) +} + +unsafe extern "C" fn x_shm_map( + p_file: *mut ffi::sqlite3_file, + pg: c_int, + pgsz: c_int, + extend: c_int, + p_page: *mut *mut c_void, +) -> c_int { + fallible(|| { + let file = unwrap_shm_file!(p_file, T)?; + let vfs = unwrap_vfs!(file.base.vfs, T)?; + // Drop any prior mapped region + drop(file.last_region.take()); + file.last_region = vfs.shm_map( + &mut file.base.handle, + pg.try_into().map_err(|_| vars::SQLITE_IOERR)?, + pgsz.try_into().map_err(|_| vars::SQLITE_IOERR)?, + extend != 0, + )?; + if let Some(last_page) = file.last_region.as_mut() { + let slice = last_page.as_mut(); + if slice.len() < pgsz as usize { + file.last_region = None; + return Err(vars::SQLITE_IOERR); + } + unsafe { *p_page = slice.as_mut_ptr() as *mut c_void } + } else { + unsafe { *p_page = null_mut() } + } + Ok(vars::SQLITE_OK) + }) +} + +unsafe extern "C" fn x_shm_lock( + p_file: *mut ffi::sqlite3_file, + offset: c_int, + n: c_int, + flags: c_int, +) -> c_int { + fallible(|| { + let file = unwrap_shm_file!(p_file, T)?; + let vfs = unwrap_vfs!(file.base.vfs, T)?; + vfs.shm_lock( + &mut file.base.handle, + offset.try_into().map_err(|_| vars::SQLITE_IOERR)?, + n.try_into().map_err(|_| vars::SQLITE_IOERR)?, + ShmLockMode::try_from(flags)?, + )?; + Ok(vars::SQLITE_OK) + }) +} + +unsafe extern "C" fn x_shm_barrier(p_file: *mut ffi::sqlite3_file) { + if let Ok(file) = unwrap_shm_file!(p_file, T) { + if let Ok(vfs) = unwrap_vfs!(file.base.vfs, T) { + vfs.shm_barrier(&mut file.base.handle) + } + } +} + +unsafe extern "C" fn x_shm_unmap( + p_file: *mut ffi::sqlite3_file, + delete_flag: c_int, +) -> c_int { + fallible(|| { + let file = unwrap_shm_file!(p_file, T)?; + let vfs = unwrap_vfs!(file.base.vfs, T)?; + drop(file.last_region.take()); + vfs.shm_unmap(&mut file.base.handle, delete_flag != 0)?; + Ok(vars::SQLITE_OK) }) }