diff --git a/examples/shared_dict.rs b/examples/shared_dict.rs index d88ebdbf..e80b39cf 100644 --- a/examples/shared_dict.rs +++ b/examples/shared_dict.rs @@ -9,10 +9,14 @@ use nginx_sys::{ ngx_command_t, ngx_conf_t, ngx_http_add_variable, ngx_http_compile_complex_value_t, ngx_http_complex_value, ngx_http_complex_value_t, ngx_http_module_t, ngx_http_request_t, ngx_http_variable_t, ngx_http_variable_value_t, ngx_int_t, ngx_module_t, ngx_parse_size, - ngx_shared_memory_add, ngx_shm_zone_t, ngx_str_t, ngx_uint_t, + ngx_str_t, ngx_uint_t, }; +use ngx::allocator::AllocError; use ngx::collections::RbTreeMap; -use ngx::core::{NGX_CONF_ERROR, NGX_CONF_OK, NgxStr, NgxString, Pool, SlabPool, Status}; +use ngx::core::{ + NGX_CONF_ERROR, NGX_CONF_OK, NgxStr, NgxString, Pool, SharedZone, SharedZoneData, SlabPool, + Status, +}; use ngx::http::{HttpModule, HttpModuleMainConf}; use ngx::{ngx_conf_log_error, ngx_log_debug, ngx_string}; @@ -97,17 +101,18 @@ pub static mut ngx_http_shared_dict_module: ngx_module_t = ngx_module_t { ..ngx_module_t::default() }; -type SharedData = ngx::sync::RwLock, NgxString, SlabPool>>; +/// Contents of the shared memory zone. +struct SharedDict(ngx::sync::RwLock, NgxString, SlabPool>>); -#[derive(Debug)] -struct SharedDictMainConfig { - shm_zone: *mut ngx_shm_zone_t, +impl SharedZoneData for SharedDict { + fn new_in(alloc: SlabPool) -> Result { + Ok(Self(ngx::sync::RwLock::new(RbTreeMap::try_new_in(alloc)?))) + } } -impl Default for SharedDictMainConfig { - fn default() -> Self { - Self { shm_zone: ptr::null_mut() } - } +#[derive(Debug, Default)] +struct SharedDictMainConfig { + shm_zone: Option>, } extern "C" fn ngx_http_shared_dict_add_zone( @@ -126,58 +131,19 @@ extern "C" fn ngx_http_shared_dict_add_zone( debug_assert!(!cf.args.is_null() && unsafe { (*cf.args).nelts >= 3 }); let args = unsafe { (*cf.args).as_slice_mut() }; - let mut name: ngx_str_t = args[1]; + // SAFETY: the directive arguments are valid nginx strings owned by the configuration pool. + let name = unsafe { NgxStr::from_ngx_str(args[1]) }; let size = unsafe { ngx_parse_size(&raw mut args[2]) }; if size == -1 { return NGX_CONF_ERROR; } - smcf.shm_zone = unsafe { - ngx_shared_memory_add( - cf, - &raw mut name, - size as usize, - (&raw mut ngx_http_shared_dict_module).cast(), - ) - }; - - let Some(shm_zone) = (unsafe { smcf.shm_zone.as_mut() }) else { - return NGX_CONF_ERROR; - }; - - shm_zone.init = Some(ngx_http_shared_dict_zone_init); - shm_zone.data = ptr::from_mut(smcf).cast(); - - NGX_CONF_OK -} - -fn ngx_http_shared_dict_get_shared(shm_zone: &mut ngx_shm_zone_t) -> Result<&SharedData, Status> { - let mut alloc = unsafe { SlabPool::from_shm_zone(shm_zone) }.ok_or(Status::NGX_ERROR)?; - - if alloc.as_mut().data.is_null() { - let shared: RbTreeMap, NgxString, SlabPool> = - RbTreeMap::try_new_in(alloc.clone()).map_err(|_| Status::NGX_ERROR)?; - - let shared = ngx::sync::RwLock::new(shared); - - alloc.as_mut().data = ngx::allocator::allocate(shared, &alloc) - .map_err(|_| Status::NGX_ERROR)? - .as_ptr() - .cast(); - } - - unsafe { alloc.as_ref().data.cast::().as_ref().ok_or(Status::NGX_ERROR) } -} - -extern "C" fn ngx_http_shared_dict_zone_init( - shm_zone: *mut ngx_shm_zone_t, - _data: *mut c_void, -) -> ngx_int_t { - let shm_zone = unsafe { &mut *shm_zone }; - - match ngx_http_shared_dict_get_shared(shm_zone) { - Err(e) => e.into(), - Ok(_) => Status::NGX_OK.into(), + match SharedZone::add(cf, name, size as usize, HttpSharedDictModule::module()) { + Ok(zone) => { + smcf.shm_zone = Some(zone); + NGX_CONF_OK + } + Err(_) => NGX_CONF_ERROR, } } @@ -256,12 +222,15 @@ extern "C" fn ngx_http_shared_dict_get_variable( let key = unsafe { NgxStr::from_ngx_str(key) }; - let Ok(shared) = ngx_http_shared_dict_get_shared(unsafe { &mut *smcf.shm_zone }) else { + let Some(shared) = smcf.shm_zone.as_ref().and_then(SharedZone::get) else { return Status::NGX_ERROR.into(); }; - let value = - shared.read().get(key).and_then(|x| unsafe { ngx_str_t::from_bytes(r.pool, x.as_bytes()) }); + let value = shared + .0 + .read() + .get(key) + .and_then(|x| unsafe { ngx_str_t::from_bytes(r.pool, x.as_bytes()) }); ngx_log_debug!( unsafe { (*r.connection).log }, @@ -301,7 +270,7 @@ extern "C" fn ngx_http_shared_dict_set_variable( return; } - let Ok(shared) = ngx_http_shared_dict_get_shared(unsafe { &mut *smcf.shm_zone }) else { + let Some(shared) = smcf.shm_zone.as_ref().and_then(SharedZone::get) else { return; }; @@ -316,9 +285,11 @@ extern "C" fn ngx_http_shared_dict_set_variable( unsafe { nginx_sys::ngx_pid }, ); - let _ = shared.write().remove(key); + let _ = shared.0.write().remove(key); } else { - let alloc = unsafe { SlabPool::from_shm_zone(&*smcf.shm_zone).expect("slab pool") }; + let Some(alloc) = smcf.shm_zone.as_ref().and_then(SharedZone::slab_pool) else { + return; + }; let Ok(key) = NgxString::try_from_bytes_in(key.as_bytes(), alloc.clone()) else { return; @@ -337,7 +308,7 @@ extern "C" fn ngx_http_shared_dict_set_variable( unsafe { nginx_sys::ngx_pid }, ); - let _ = shared.write().try_insert(key, value); + let _ = shared.0.write().try_insert(key, value); } } @@ -355,13 +326,13 @@ extern "C" fn ngx_http_shared_dict_get_entries( ngx_log_debug!(unsafe { (*r.connection).log }, "shared dict: get all entries"); - let Ok(shared) = ngx_http_shared_dict_get_shared(unsafe { &mut *smcf.shm_zone }) else { + let Some(shared) = smcf.shm_zone.as_ref().and_then(SharedZone::get) else { return Status::NGX_ERROR.into(); }; let mut str = NgxString::new_in(pool); { - let dict = shared.read(); + let dict = shared.0.read(); let mut len: usize = 0; let mut values: usize = 0; @@ -411,15 +382,15 @@ extern "C" fn ngx_http_shared_dict_set_entries( ngx_log_debug!(unsafe { (*r.connection).log }, "shared dict: clear"); - let Ok(shared) = ngx_http_shared_dict_get_shared(unsafe { &mut *smcf.shm_zone }) else { + let Some(shared) = smcf.shm_zone.as_ref().and_then(SharedZone::get) else { return; }; - let Ok(tree) = RbTreeMap::try_new_in(shared.read().allocator().clone()) else { + let Ok(tree) = RbTreeMap::try_new_in(shared.0.read().allocator().clone()) else { return; }; // This would check both .clear() and the drop implementation - *shared.write() = tree; - // shared.write().clear() + *shared.0.write() = tree; + // shared.0.write().clear() } diff --git a/src/core/mod.rs b/src/core/mod.rs index 9b9a7783..215488d4 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -1,6 +1,7 @@ mod buffer; mod conf; mod pool; +pub mod shm; pub mod slab; mod status; mod string; @@ -8,6 +9,7 @@ mod string; pub use buffer::*; pub use conf::*; pub use pool::*; +pub use shm::{SharedZone, SharedZoneData, ShmError}; pub use slab::SlabPool; pub use status::*; pub use string::*; diff --git a/src/core/shm.rs b/src/core/shm.rs new file mode 100644 index 00000000..e84d3a26 --- /dev/null +++ b/src/core/shm.rs @@ -0,0 +1,220 @@ +//! Safe wrappers for nginx shared memory zones. +//! +//! Shared memory zones are declared during configuration parsing and initialized once the +//! configuration is applied. The raw interface requires an `unsafe extern "C"` callback and a +//! hand-rolled cast of the untyped `ngx_slab_pool_t::data` pointer at every access; the types +//! here keep the zone's payload type in the signature instead. +//! +//! See . +use core::ffi::c_void; +use core::fmt; +use core::marker::PhantomData; +use core::ptr::{self, NonNull}; + +use nginx_sys::{ + ngx_conf_t, ngx_int_t, ngx_module_t, ngx_shared_memory_add, ngx_shm_zone_t, ngx_str_t, +}; + +use crate::allocator::{AllocError, allocate}; +use crate::core::{NgxStr, SlabPool, Status}; + +/// A value stored in the slab pool of a shared memory zone. +/// +/// The value is created once, on the first initialization of the zone, and then reused for as +/// long as the zone keeps the same mapping — notably across configuration reloads and across a +/// binary upgrade. It is never dropped: shared memory outlives the process that created it, so +/// implementers should not rely on [`Drop`] for cleanup. +pub trait SharedZoneData: Sized { + /// Creates the initial value in the zone's slab pool. + /// + /// Everything reachable from the returned value must itself be allocated from `alloc`, or it + /// will not be visible to the other worker processes. + fn new_in(alloc: SlabPool) -> Result; + + /// Accepts or rejects a value left in the zone by a previous configuration. + /// + /// Called instead of [`SharedZoneData::new_in`] when the zone already holds a value, which + /// happens when a configuration reload reuses the mapping or when a zone is inherited from + /// the master process. Returning an error aborts the reload, leaving the running + /// configuration in place. + /// + /// The default implementation accepts the existing value unchanged. + fn reuse(&self) -> Result<(), Status> { + Ok(()) + } +} + +/// Error returned when a shared memory zone cannot be declared. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum ShmError { + /// nginx refused the zone: the name is already declared for a different module, or with a + /// conflicting size. A message describing which of the two it was has already been written + /// to the configuration log. + Rejected, + /// Allocation from the configuration pool failed. + Alloc, +} + +impl fmt::Display for ShmError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Rejected => f.write_str("shared memory zone rejected"), + Self::Alloc => f.write_str("configuration pool allocation failed"), + } + } +} + +impl core::error::Error for ShmError {} + +impl From for Status { + fn from(_: ShmError) -> Self { + Status::NGX_ERROR + } +} + +/// A shared memory zone holding a value of type `T`. +/// +/// Obtained from [`SharedZone::add`] while parsing a configuration directive, and normally kept +/// in the module's configuration. The zone is not usable until nginx has applied the +/// configuration, so [`SharedZone::get`] returns [`None`] when called before that point. +pub struct SharedZone { + zone: NonNull, + _data: PhantomData T>, +} + +impl Clone for SharedZone { + fn clone(&self) -> Self { + *self + } +} + +impl Copy for SharedZone {} + +impl fmt::Debug for SharedZone { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("SharedZone") + .field("name", &self.name()) + .field("size", &self.size()) + .finish() + } +} + +impl SharedZone { + /// Declares a shared memory zone, or joins one already declared by this module. + /// + /// `name` is copied into the configuration pool, so the caller's buffer does not have to + /// outlive the call. `size` may be zero to join a zone whose size is declared elsewhere. + /// `module` is used as the zone's tag: two modules may declare zones of the same name + /// without colliding, while a second declaration from the same module must agree on the + /// size. + /// + /// The zone's initialization callback is installed here; it calls + /// [`SharedZoneData::new_in`] or [`SharedZoneData::reuse`] as appropriate. + pub fn add( + cf: &mut ngx_conf_t, + name: &NgxStr, + size: usize, + module: &'static ngx_module_t, + ) -> Result { + // SAFETY: `cf.pool` is a valid pool for the duration of configuration parsing. + let mut name = + unsafe { ngx_str_t::from_bytes(cf.pool, name.as_bytes()) }.ok_or(ShmError::Alloc)?; + + // SAFETY: `cf` and the freshly allocated `name` are both valid; the name's contents are + // owned by the configuration pool and outlive the cycle. + let zone = unsafe { + ngx_shared_memory_add( + cf, + &raw mut name, + size, + ptr::from_ref(module).cast_mut().cast::(), + ) + }; + + let mut zone = NonNull::new(zone).ok_or(ShmError::Rejected)?; + + // SAFETY: `ngx_shared_memory_add` returned a valid, uniquely borrowed zone. + unsafe { zone.as_mut() }.init = Some(init_zone::); + + Ok(Self { zone, _data: PhantomData }) + } + + /// Returns the value stored in the zone, or [`None`] if the zone is not initialized yet. + /// + /// # Panics + /// + /// Does not panic, but the returned reference must not be held across a configuration + /// reload: zone addresses of an old cycle may be unmapped once the reload completes. In + /// practice this means the reference is valid for the whole lifetime of a worker process, + /// and inside a cycle pool cleanup handler it is not. + pub fn get(&self) -> Option<&T> { + // SAFETY: the zone is allocated from the cycle pool and outlives this handle. + let alloc = unsafe { SlabPool::from_shm_zone(self.zone.as_ref()) }?; + // SAFETY: `data` is only ever set by `init_zone::`, which stores a `T`. + unsafe { alloc.as_ref().data.cast::().as_ref() } + } + + /// Returns the zone's slab pool, for allocating values that the payload will refer to. + /// + /// Returns [`None`] before the zone is mapped. + pub fn slab_pool(&self) -> Option { + // SAFETY: the zone is allocated from the cycle pool and outlives this handle. + unsafe { SlabPool::from_shm_zone(self.zone.as_ref()) } + } +} + +impl SharedZone { + /// Returns the zone's name. + pub fn name(&self) -> &NgxStr { + // SAFETY: the zone and its name are allocated from the cycle pool. + unsafe { NgxStr::from_ngx_str(self.zone.as_ref().shm.name) } + } + + /// Returns the configured size of the zone in bytes. + pub fn size(&self) -> usize { + // SAFETY: the zone is allocated from the cycle pool and outlives this handle. + unsafe { self.zone.as_ref() }.shm.size + } + + /// Returns a pointer to the underlying zone, for interoperation with the raw API. + pub fn as_ptr(&self) -> *mut ngx_shm_zone_t { + self.zone.as_ptr() + } +} + +/// Initialization callback installed by [`SharedZone::add`]. +/// +/// The `data` argument carries the previous cycle's `ngx_shm_zone_t::data`, which this wrapper +/// never sets; the existing value is recovered from the slab pool instead, so that a zone +/// inherited from the master process is handled the same way as one reused across a reload. +unsafe extern "C" fn init_zone( + zone: *mut ngx_shm_zone_t, + _data: *mut c_void, +) -> ngx_int_t { + // SAFETY: nginx passes a mapped, uniquely borrowed zone to the init callback. + let zone = unsafe { &*zone }; + + match init_data::(zone) { + Ok(()) => Status::NGX_OK.into(), + Err(err) => err.into(), + } +} + +fn init_data(zone: &ngx_shm_zone_t) -> Result<(), Status> { + // SAFETY: called from the init callback, where the zone is mapped and its slab pool has + // already been set up by `ngx_init_zone_pool`. + let mut alloc = unsafe { SlabPool::from_shm_zone(zone) }.ok_or(Status::NGX_ERROR)?; + + let existing = alloc.as_ref().data; + // SAFETY: `data` is only ever set below, to a `T` allocated from this pool. + if let Some(existing) = unsafe { existing.cast::().as_ref() } { + return existing.reuse(); + } + + let value = T::new_in(alloc.clone()).map_err(|_| Status::NGX_ERROR)?; + let value = allocate(value, &alloc).map_err(|_| Status::NGX_ERROR)?; + alloc.as_mut().data = value.as_ptr().cast(); + + Ok(()) +}