use std::alloc::{alloc_zeroed, dealloc, Layout};
use std::collections::{HashMap, HashSet};
use std::os::raw::{c_int, c_uint, c_void};
use std::sync::Mutex;
use rusqlite::ffi;
const PAGE_ALIGN: usize = 16;
struct Cache {
sz_page: c_int,
sz_extra: c_int,
purgeable: bool,
state: Mutex<State>,
}
struct State {
pages: HashMap<c_uint, Box<Page>>,
unpinned: HashSet<c_uint>,
cache_size_hint: c_int,
}
struct Page {
handle: ffi::sqlite3_pcache_page,
buf_ptr: *mut u8,
extra_ptr: *mut u8,
sz_page: usize,
sz_extra: usize,
key: c_uint,
pinned: bool,
}
unsafe impl Send for Page {}
impl Page {
fn new(sz_page: usize, sz_extra: usize, key: c_uint) -> Box<Page> {
debug_assert!(
sz_page.is_multiple_of(PAGE_ALIGN),
"sz_page {sz_page} not aligned to {PAGE_ALIGN}"
);
unsafe {
let buf_layout = Layout::from_size_align(sz_page, PAGE_ALIGN).expect("buf layout");
let buf_ptr = alloc_zeroed(buf_layout);
assert!(!buf_ptr.is_null(), "pcache2: buf allocation failed");
let extra_sz = sz_extra.max(1);
let extra_layout = Layout::from_size_align(extra_sz, PAGE_ALIGN).expect("extra layout");
let extra_ptr = alloc_zeroed(extra_layout);
assert!(!extra_ptr.is_null(), "pcache2: extra allocation failed");
Box::new(Page {
handle: ffi::sqlite3_pcache_page {
pBuf: buf_ptr.cast::<c_void>(),
pExtra: extra_ptr.cast::<c_void>(),
},
buf_ptr,
extra_ptr,
sz_page,
sz_extra,
key,
pinned: true,
})
}
}
fn handle_ptr(&mut self) -> *mut ffi::sqlite3_pcache_page {
&mut self.handle as *mut _
}
unsafe fn from_handle_ptr<'a>(handle: *mut ffi::sqlite3_pcache_page) -> &'a Page {
debug_assert!(!handle.is_null());
let off = std::mem::offset_of!(Page, handle);
&*(handle as *mut u8).sub(off).cast::<Page>()
}
}
impl Drop for Page {
fn drop(&mut self) {
unsafe {
if !self.buf_ptr.is_null() {
let layout = Layout::from_size_align_unchecked(self.sz_page, PAGE_ALIGN);
dealloc(self.buf_ptr, layout);
}
if !self.extra_ptr.is_null() {
let layout = Layout::from_size_align_unchecked(self.sz_extra.max(1), PAGE_ALIGN);
dealloc(self.extra_ptr, layout);
}
}
}
}
unsafe extern "C" fn pcache_init(_arg: *mut c_void) -> c_int {
ffi::SQLITE_OK
}
unsafe extern "C" fn pcache_shutdown(_arg: *mut c_void) {}
unsafe extern "C" fn pcache_create(
sz_page: c_int,
sz_extra: c_int,
purgeable: c_int,
) -> *mut ffi::sqlite3_pcache {
let cache = Box::new(Cache {
sz_page,
sz_extra,
purgeable: purgeable != 0,
state: Mutex::new(State {
pages: HashMap::new(),
unpinned: HashSet::new(),
cache_size_hint: 0,
}),
});
Box::into_raw(cache).cast::<ffi::sqlite3_pcache>()
}
unsafe extern "C" fn pcache_cachesize(cache: *mut ffi::sqlite3_pcache, n_cachesize: c_int) {
let cache = &*cache.cast::<Cache>();
let mut s = cache.state.lock().unwrap();
s.cache_size_hint = n_cachesize;
}
unsafe extern "C" fn pcache_pagecount(cache: *mut ffi::sqlite3_pcache) -> c_int {
let cache = &*cache.cast::<Cache>();
let s = cache.state.lock().unwrap();
c_int::try_from(s.pages.len()).unwrap_or(c_int::MAX)
}
unsafe extern "C" fn pcache_fetch(
cache: *mut ffi::sqlite3_pcache,
key: c_uint,
create_flag: c_int,
) -> *mut ffi::sqlite3_pcache_page {
let cache_ref: &Cache = &*cache.cast::<Cache>();
debug_assert!(
(cache_ref.sz_page as usize).is_multiple_of(PAGE_ALIGN),
"SQLite passed sz_page={} not aligned to {PAGE_ALIGN}",
cache_ref.sz_page,
);
let mut s = cache_ref.state.lock().unwrap();
if let Some(page) = s.pages.get_mut(&key) {
page.pinned = true;
let ptr = page.handle_ptr();
s.unpinned.remove(&key);
return ptr;
}
if create_flag == 0 {
return std::ptr::null_mut();
}
if cache_ref.purgeable && s.cache_size_hint > 0 {
let limit = s.cache_size_hint as usize;
while s.pages.len() >= limit {
let evict = match s.unpinned.iter().next().copied() {
Some(k) => k,
None => break, };
s.unpinned.remove(&evict);
s.pages.remove(&evict);
}
}
let mut page = Page::new(cache_ref.sz_page as usize, cache_ref.sz_extra as usize, key);
let ptr = page.handle_ptr();
s.pages.insert(key, page);
ptr
}
unsafe extern "C" fn pcache_unpin(
cache: *mut ffi::sqlite3_pcache,
page_ptr: *mut ffi::sqlite3_pcache_page,
discard: c_int,
) {
if page_ptr.is_null() {
return;
}
let cache_ref: &Cache = &*cache.cast::<Cache>();
let key = Page::from_handle_ptr(page_ptr).key;
let mut s = cache_ref.state.lock().unwrap();
if discard != 0 {
s.pages.remove(&key);
s.unpinned.remove(&key);
} else if let Some(p) = s.pages.get_mut(&key) {
p.pinned = false;
s.unpinned.insert(key);
}
}
unsafe extern "C" fn pcache_rekey(
cache: *mut ffi::sqlite3_pcache,
_page_ptr: *mut ffi::sqlite3_pcache_page,
old_key: c_uint,
new_key: c_uint,
) {
let cache_ref: &Cache = &*cache.cast::<Cache>();
let mut s = cache_ref.state.lock().unwrap();
if let Some(mut page) = s.pages.remove(&old_key) {
page.key = new_key;
s.pages.insert(new_key, page);
}
if s.unpinned.remove(&old_key) {
s.unpinned.insert(new_key);
}
}
unsafe extern "C" fn pcache_truncate(cache: *mut ffi::sqlite3_pcache, i_limit: c_uint) {
let cache_ref: &Cache = &*cache.cast::<Cache>();
let mut s = cache_ref.state.lock().unwrap();
let drop_keys: Vec<c_uint> = s.pages.keys().copied().filter(|k| *k >= i_limit).collect();
for k in drop_keys {
s.pages.remove(&k);
s.unpinned.remove(&k);
}
}
unsafe extern "C" fn pcache_destroy(cache: *mut ffi::sqlite3_pcache) {
if cache.is_null() {
return;
}
drop(Box::from_raw(cache.cast::<Cache>()));
}
unsafe extern "C" fn pcache_shrink(cache: *mut ffi::sqlite3_pcache) {
let cache_ref: &Cache = &*cache.cast::<Cache>();
let mut s = cache_ref.state.lock().unwrap();
let to_evict: Vec<c_uint> = s.unpinned.drain().collect();
for k in to_evict {
s.pages.remove(&k);
}
}
#[repr(transparent)]
pub(crate) struct SyncMethods(pub ffi::sqlite3_pcache_methods2);
unsafe impl Sync for SyncMethods {}
pub(crate) static PCACHE2_METHODS: SyncMethods = SyncMethods(ffi::sqlite3_pcache_methods2 {
iVersion: 1,
pArg: std::ptr::null_mut(),
xInit: Some(pcache_init),
xShutdown: Some(pcache_shutdown),
xCreate: Some(pcache_create),
xCachesize: Some(pcache_cachesize),
xPagecount: Some(pcache_pagecount),
xFetch: Some(pcache_fetch),
xUnpin: Some(pcache_unpin),
xRekey: Some(pcache_rekey),
xTruncate: Some(pcache_truncate),
xDestroy: Some(pcache_destroy),
xShrink: Some(pcache_shrink),
});