From b60be2cf048cb17f9b180ce3ba2951ed82dd83e4 Mon Sep 17 00:00:00 2001 From: Aleksei Bavshin Date: Thu, 5 Jun 2025 18:34:41 -0700 Subject: [PATCH 01/12] fix: correct type for ngx::core::NGX_CONF_ERROR --- examples/awssig.rs | 2 +- src/core/status.rs | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/examples/awssig.rs b/examples/awssig.rs index 384f98d2..5521a576 100644 --- a/examples/awssig.rs +++ b/examples/awssig.rs @@ -230,7 +230,7 @@ extern "C" fn ngx_http_awssigv4_commands_set_s3_bucket( conf.s3_bucket = (*args.add(1)).to_string(); if conf.s3_bucket.len() == 1 { println!("Validation failed"); - return ngx::core::NGX_CONF_ERROR as _; + return ngx::core::NGX_CONF_ERROR; } }; std::ptr::null_mut() diff --git a/src/core/status.rs b/src/core/status.rs index 46bb79f7..4cab8844 100644 --- a/src/core/status.rs +++ b/src/core/status.rs @@ -1,4 +1,6 @@ +use core::ffi::c_char; use core::fmt; +use core::ptr; use crate::ffi::*; @@ -62,6 +64,7 @@ ngx_codes! { (NGX_ABORT); } -/// NGX_CONF_ERROR - An error occurred while parsing and validating configuration. -pub const NGX_CONF_ERROR: *const () = -1isize as *const (); -// pub const CONF_OK: Status = Status(NGX_CONF_OK as ngx_int_t); +/// An error occurred while parsing and validating configuration. +pub const NGX_CONF_ERROR: *mut c_char = ptr::null_mut::().wrapping_offset(-1); +/// Configuration handler succeeded. +pub const NGX_CONF_OK: *mut c_char = ptr::null_mut(); From c732b0108533f8e305ff4e5a7c939c8763641c16 Mon Sep 17 00:00:00 2001 From: Aleksei Bavshin Date: Sat, 11 Jan 2025 22:59:51 -0800 Subject: [PATCH 02/12] feat: enforce minimal required alignment for pool allocations Refuse to compile if the NGX_ALIGNMENT value detected or specified while building nginx is insufficient. --- nginx-sys/build/main.rs | 2 ++ nginx-sys/build/wrapper.h | 4 ++++ nginx-sys/src/lib.rs | 8 ++++++++ 3 files changed, 14 insertions(+) diff --git a/nginx-sys/build/main.rs b/nginx-sys/build/main.rs index 22208dd7..a657276d 100644 --- a/nginx-sys/build/main.rs +++ b/nginx-sys/build/main.rs @@ -216,6 +216,8 @@ fn generate_binding(nginx: &NginxSource) { // Bindings will not compile on Linux without block listing this item // It is worth investigating why this is .blocklist_item("IPPORT_RESERVED") + // will be restored later in build.rs + .blocklist_item("NGX_ALIGNMENT") .generate_cstr(true) // The input header we would like to generate bindings for. .header("build/wrapper.h") diff --git a/nginx-sys/build/wrapper.h b/nginx-sys/build/wrapper.h index c333744f..552a793b 100644 --- a/nginx-sys/build/wrapper.h +++ b/nginx-sys/build/wrapper.h @@ -20,6 +20,10 @@ const char *NGX_RS_MODULE_SIGNATURE = NGX_MODULE_SIGNATURE; +// NGX_ALIGNMENT could be defined as a constant or an expression, with the +// latter being unsupported by bindgen. +const size_t NGX_RS_ALIGNMENT = NGX_ALIGNMENT; + // `--prefix=` results in not emitting the declaration #ifndef NGX_PREFIX #define NGX_PREFIX "" diff --git a/nginx-sys/src/lib.rs b/nginx-sys/src/lib.rs index 83a458c4..ffb7eddf 100644 --- a/nginx-sys/src/lib.rs +++ b/nginx-sys/src/lib.rs @@ -36,6 +36,14 @@ pub use queue::*; #[cfg(ngx_feature = "stream")] pub use stream::*; +/// Default alignment for pool allocations. +pub const NGX_ALIGNMENT: usize = NGX_RS_ALIGNMENT; + +// Check if the allocations made with ngx_palloc are properly aligned. +// If the check fails, objects allocated from `ngx_pool` can violate Rust pointer alignment +// requirements. +const _: () = assert!(core::mem::align_of::() <= NGX_ALIGNMENT); + impl ngx_command_t { /// Creates a new empty [`ngx_command_t`] instance. /// From 5d8d15a06e98e2897979f9c8f2b9955e039f98ea Mon Sep 17 00:00:00 2001 From: Aleksei Bavshin Date: Tue, 10 Jun 2025 14:40:47 -0700 Subject: [PATCH 03/12] chore: address compiler warnings - mismatched_lifetime_syntaxes - clippy::uninlined_format_args --- nginx-sys/build/main.rs | 13 +++++-------- src/core/string.rs | 2 +- src/http/request.rs | 6 +++--- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/nginx-sys/build/main.rs b/nginx-sys/build/main.rs index a657276d..f2dc1afe 100644 --- a/nginx-sys/build/main.rs +++ b/nginx-sys/build/main.rs @@ -341,22 +341,19 @@ pub fn print_cargo_metadata>(includes: &[T]) -> Result<(), Box Cow { + pub fn to_string_lossy(&self) -> Cow<'_, str> { String::from_utf8_lossy(self.as_bytes()) } diff --git a/src/http/request.rs b/src/http/request.rs index c3a21556..d17d294c 100644 --- a/src/http/request.rs +++ b/src/http/request.rs @@ -375,13 +375,13 @@ impl Request { /// Iterate over headers_in /// each header item is (&str, &str) (borrowed) - pub fn headers_in_iterator(&self) -> NgxListIterator { + pub fn headers_in_iterator(&self) -> NgxListIterator<'_> { unsafe { list_iterator(&self.0.headers_in.headers) } } /// Iterate over headers_out /// each header item is (&str, &str) (borrowed) - pub fn headers_out_iterator(&self) -> NgxListIterator { + pub fn headers_out_iterator(&self) -> NgxListIterator<'_> { unsafe { list_iterator(&self.0.headers_out.headers) } } } @@ -448,7 +448,7 @@ impl<'a> From<&'a ngx_list_part_t> for ListPart<'a> { /// # Safety /// /// The caller has provided a valid [`ngx_str_t`] which can be dereferenced validly. -pub unsafe fn list_iterator(list: &ngx_list_t) -> NgxListIterator { +pub unsafe fn list_iterator(list: &ngx_list_t) -> NgxListIterator<'_> { NgxListIterator { part: Some((&list.part).into()), i: 0, From b712492fc7151273c42a941e93814e9dc6701337 Mon Sep 17 00:00:00 2001 From: Aleksei Bavshin Date: Thu, 5 Jun 2025 16:37:09 -0700 Subject: [PATCH 04/12] ci: disable fail-fast for Rust/Test (Linux) jobs This allows to complete the linting and format checks on MSRV failure. --- .github/workflows/ci.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index cb15d608..5cb4130e 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -27,6 +27,7 @@ jobs: needs: rust-version runs-on: ubuntu-latest strategy: + fail-fast: false matrix: rust-version: - ${{ needs.rust-version.outputs.version }} From 5c5ec6ff710a7e2ed2b87789c04cf6b3d7068a69 Mon Sep 17 00:00:00 2001 From: Aleksei Bavshin Date: Fri, 13 Jun 2025 00:02:10 -0700 Subject: [PATCH 05/12] chore: update dependencies --- Cargo.lock | 101 +++++++++++++++++++++---------------------- nginx-sys/Cargo.toml | 4 +- 2 files changed, 52 insertions(+), 53 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f4c405a1..fcc15077 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,9 +13,9 @@ dependencies = [ [[package]] name = "adler2" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aho-corasick" @@ -95,9 +95,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bindgen" -version = "0.71.1" +version = "0.72.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" +checksum = "4f72209734318d0b619a5e0f5129918b848c416e122a3c4ce054e03cb87b726f" dependencies = [ "bitflags", "cexpr", @@ -130,9 +130,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.17.0" +version = "3.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee" [[package]] name = "bytes" @@ -142,9 +142,9 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.2.24" +version = "1.2.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16595d3be041c03b09d08d0858631facccee9221e579704070e6e9e4915d3bc7" +checksum = "956a5e21988b87f372569b66183b78babf23ebc2e744b733e4350a752c4dafac" dependencies = [ "shlex", ] @@ -160,9 +160,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" [[package]] name = "chrono" @@ -301,9 +301,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" dependencies = [ "crc32fast", "miniz_oxide", @@ -468,12 +468,12 @@ checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" [[package]] name = "libloading" -version = "0.8.7" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a793df0d7afeac54f95b471d3af7f0d4fb975699f972341a4b76988d49cdf0c" +checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", - "windows-targets 0.53.0", + "windows-targets 0.53.2", ] [[package]] @@ -495,9 +495,9 @@ checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" dependencies = [ "autocfg", "scopeguard", @@ -511,9 +511,9 @@ checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "minimal-lexical" @@ -523,22 +523,22 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", ] [[package]] name = "mio" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", "wasi", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -611,9 +611,9 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" dependencies = [ "lock_api", "parking_lot_core", @@ -621,9 +621,9 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ "cfg-if", "libc", @@ -646,9 +646,9 @@ checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "prettyplease" -version = "0.2.32" +version = "0.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "664ec5419c51e34154eec046ebcba56312d5a2fc3b09a06da188e1ad21afadf6" +checksum = "6837b9e10d61f45f987d50808f83d1ee3d206c66acf650c3e4ae2e1f6ddedf55" dependencies = [ "proc-macro2", "syn", @@ -674,9 +674,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.12" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" +checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" dependencies = [ "bitflags", ] @@ -726,9 +726,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" [[package]] name = "rustc-hash" @@ -862,15 +862,15 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.15.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.5.9" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" dependencies = [ "libc", "windows-sys 0.52.0", @@ -884,9 +884,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.101" +version = "2.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +checksum = "e4307e30089d6fd6aff212f2da3a1f9e32f3223b1f010fb09b7c95f90f3ca1e8" dependencies = [ "proc-macro2", "quote", @@ -927,9 +927,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.45.0" +version = "1.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165" +checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" dependencies = [ "backtrace", "bytes", @@ -1054,9 +1054,9 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasm-bindgen" @@ -1136,11 +1136,10 @@ dependencies = [ [[package]] name = "which" -version = "7.0.3" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d643ce3fd3e5b54854602a080f34fb10ab75e0b813ee32d00ca2b44fa74762" +checksum = "d3fabb953106c3c8eea8306e4393700d7657561cb43122571b172bbfb7c7ba1d" dependencies = [ - "either", "env_home", "rustix", "winsafe", @@ -1183,9 +1182,9 @@ dependencies = [ [[package]] name = "windows-link" -version = "0.1.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" [[package]] name = "windows-result" @@ -1241,9 +1240,9 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.0" +version = "0.53.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" +checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" dependencies = [ "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", diff --git a/nginx-sys/Cargo.toml b/nginx-sys/Cargo.toml index 7bfc6e99..02f2293d 100644 --- a/nginx-sys/Cargo.toml +++ b/nginx-sys/Cargo.toml @@ -20,7 +20,7 @@ rust-version.workspace = true errno = { version = "0.3", default-features = false } [build-dependencies] -bindgen = "0.71" +bindgen = "0.72" cc = "1.2.0" duct = { version = "1", optional = true } dunce = "1.0.5" @@ -28,7 +28,7 @@ flate2 = { version = "1.0.28", optional = true } regex = "1.11.1" tar = { version = "0.4.40", optional = true } ureq = { version = "3.0.10", optional = true } -which = { version = "7.0.0", optional = true } +which = { version = "8.0.0", optional = true } [features] vendored = ["dep:which", "dep:duct", "dep:ureq", "dep:flate2", "dep:tar"] From 4c3ffba8a8f7fdb7d88fcd0d900d1aff1ec7cbbf Mon Sep 17 00:00:00 2001 From: Aleksei Bavshin Date: Wed, 4 Jun 2025 11:44:51 -0700 Subject: [PATCH 06/12] feat: add and reexport allocator-api2 Add some helpers for working with allocators. --- Cargo.lock | 7 ++++++ Cargo.toml | 8 +++++-- src/allocator.rs | 62 ++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 5 ++++ 4 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 src/allocator.rs diff --git a/Cargo.lock b/Cargo.lock index fcc15077..b9c09224 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -561,6 +567,7 @@ dependencies = [ name = "ngx" version = "0.5.0" dependencies = [ + "allocator-api2", "nginx-sys", "target-triple", ] diff --git a/Cargo.toml b/Cargo.toml index d46cb3d9..8578e125 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,16 +25,20 @@ repository.workspace = true rust-version.workspace = true [dependencies] +allocator-api2 = { version = "0.2.21", default-features = false } nginx-sys = { path = "nginx-sys", default-features=false, version = "0.5.0"} [features] default = ["vendored","std"] # Enables the components using memory allocation. # If no `std` flag, `alloc` crate is internally used instead. This flag is mainly for `no_std` build. -alloc = [] +alloc = ["allocator-api2/alloc"] # Enables the components using `std` crate. # Currently the only difference to `alloc` flag is `std::error::Error` implementation. -std = ["alloc"] +std = [ + "alloc", + "allocator-api2/std" +] # Build our own copy of the NGINX by default. # This could be disabled with `--no-default-features` to minimize the dependency # tree when building against an existing copy of the NGINX with the diff --git a/src/allocator.rs b/src/allocator.rs new file mode 100644 index 00000000..3cb726c1 --- /dev/null +++ b/src/allocator.rs @@ -0,0 +1,62 @@ +use ::core::mem; +use ::core::ptr::NonNull; + +pub use allocator_api2::alloc::*; + +#[cfg(feature = "alloc")] +pub use allocator_api2::{boxed, collections, vec, SliceExt}; + +/// Explicitly duplicate an object using the specified Allocator. +pub trait TryCloneIn: Sized { + /// Target type, generic over an allocator. + type Target; + + /// Attempts to copy the value using `alloc` as an underlying Allocator. + fn try_clone_in(&self, alloc: A) -> Result, AllocError>; +} + +/// Moves `value` to the memory backed by `alloc` and returns a pointer. +/// +/// This should be similar to `Box::into_raw(Box::try_new_in(value, alloc)?)`, except without +/// `alloc` requirement and intermediate steps. +/// +/// # Note +/// +/// The resulting pointer has no owner. The caller is responsible for destroying `T` and releasing +/// the memory. +pub fn allocate(value: T, alloc: &A) -> Result, AllocError> +where + A: Allocator, +{ + let layout = Layout::for_value(&value); + let ptr: NonNull = alloc.allocate(layout)?.cast(); + + // SAFETY: the allocator succeeded and gave us a correctly aligned pointer to an uninitialized + // data + unsafe { ptr.cast::>().as_mut().write(value) }; + + Ok(ptr) +} + +#[cfg(feature = "alloc")] +mod impls { + use super::*; + + use super::boxed::Box; + + impl TryCloneIn for Box + where + T: TryCloneIn, + OA: Allocator, + { + type Target = Box<::Target, A>; + + fn try_clone_in( + &self, + alloc: A, + ) -> Result, AllocError> { + let x = self.as_ref().try_clone_in(alloc.clone())?; + Box::try_new_in(x, alloc) + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 32a16f41..ece02efd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -40,6 +40,11 @@ extern crate alloc; #[cfg(feature = "std")] extern crate std; +/// The allocator module. +/// +/// Currently reexports parts of the allocator-api2. +pub mod allocator; + /// The core module. /// /// This module provides fundamental utilities needed to interface with many NGINX primitives. From c795b09b72a8412968517eebc5d83afc8213cd4c Mon Sep 17 00:00:00 2001 From: Aleksei Bavshin Date: Wed, 4 Jun 2025 18:30:31 -0700 Subject: [PATCH 07/12] feat: implement Allocator for Pool --- nginx-sys/build/main.rs | 2 + src/allocator.rs | 13 ++++++ src/core/pool.rs | 99 ++++++++++++++++++++++++++++++++++++----- 3 files changed, 103 insertions(+), 11 deletions(-) diff --git a/nginx-sys/build/main.rs b/nginx-sys/build/main.rs index f2dc1afe..f6826e6a 100644 --- a/nginx-sys/build/main.rs +++ b/nginx-sys/build/main.rs @@ -25,6 +25,8 @@ const NGX_CONF_FEATURES: &[&str] = &[ "have_epollrdhup", "have_file_aio", "have_kqueue", + "have_memalign", + "have_posix_memalign", "have_variadic_macros", "http", "http_cache", diff --git a/src/allocator.rs b/src/allocator.rs index 3cb726c1..8b7ccc40 100644 --- a/src/allocator.rs +++ b/src/allocator.rs @@ -37,6 +37,19 @@ where Ok(ptr) } +/// +/// Creates a `NonNull` that is dangling, but well-aligned for this alignment. +/// +/// See also [::core::alloc::Layout::dangling()] +#[inline(always)] +pub(crate) const fn dangling_aligned(align: usize) -> NonNull { + unsafe { + // TODO: use ptr::without_provenance with msrv >= 1.84 + #[allow(clippy::useless_transmute)] + let ptr: *mut T = mem::transmute(align); + NonNull::new_unchecked(ptr) + } +} #[cfg(feature = "alloc")] mod impls { diff --git a/src/core/pool.rs b/src/core/pool.rs index c17ec3b3..97a29446 100644 --- a/src/core/pool.rs +++ b/src/core/pool.rs @@ -1,13 +1,89 @@ +use core::alloc::Layout; use core::ffi::c_void; -use core::{mem, ptr}; +use core::mem; +use core::ptr::{self, NonNull}; +use nginx_sys::{ + ngx_buf_t, ngx_create_temp_buf, ngx_palloc, ngx_pcalloc, ngx_pfree, ngx_pmemalign, ngx_pnalloc, + ngx_pool_cleanup_add, ngx_pool_t, NGX_ALIGNMENT, +}; + +use crate::allocator::{dangling_aligned, AllocError, Allocator}; use crate::core::buffer::{Buffer, MemoryBuffer, TemporaryBuffer}; -use crate::ffi::*; -/// Wrapper struct for an [`ngx_pool_t`] pointer, providing methods for working with memory pools. +/// Non-owning wrapper for an [`ngx_pool_t`] pointer, providing methods for working with memory pools. /// /// See -pub struct Pool(*mut ngx_pool_t); +#[derive(Clone, Debug)] +#[repr(transparent)] +pub struct Pool(NonNull); + +unsafe impl Allocator for Pool { + fn allocate(&self, layout: Layout) -> Result, AllocError> { + // SAFETY: + // * This wrapper should be constructed with a valid pointer to ngx_pool_t. + // * The Pool type is !Send, thus we expect exclusive access for this call. + // * Pointers are considered mutable unless obtained from an immutable reference. + let ptr = if layout.size() == 0 { + // We can guarantee alignment <= NGX_ALIGNMENT for allocations of size 0 made with + // ngx_palloc_small. Any other cases are implementation-defined, and we can't tell which + // one will be used internally. + return Ok(NonNull::slice_from_raw_parts( + dangling_aligned(layout.align()), + layout.size(), + )); + } else if layout.align() == 1 { + unsafe { ngx_pnalloc(self.0.as_ptr(), layout.size()) } + } else if layout.align() <= NGX_ALIGNMENT { + unsafe { ngx_palloc(self.0.as_ptr(), layout.size()) } + } else if cfg!(any( + ngx_feature = "have_posix_memalign", + ngx_feature = "have_memalign" + )) { + // ngx_pmemalign is always defined, but does not guarantee the requested alignment + // unless memalign/posix_memalign exists. + unsafe { ngx_pmemalign(self.0.as_ptr(), layout.size(), layout.align()) } + } else { + return Err(AllocError); + }; + + // Verify the alignment of the result + debug_assert_eq!(ptr.align_offset(layout.align()), 0); + + let ptr = NonNull::new(ptr.cast()).ok_or(AllocError)?; + Ok(NonNull::slice_from_raw_parts(ptr, layout.size())) + } + + unsafe fn deallocate(&self, ptr: NonNull, layout: Layout) { + // ngx_pfree is noop for small allocations unless NGX_DEBUG_PCALLOC is set. + // + // XXX: there should be no cleanup handlers for the allocations made using this API. + // Violating that could result in the following issues: + // - use-after-free on large allocation + // - multiple cleanup handlers attached to a dangling ptr (these are not unique) + if layout.size() > 0 // 0 is dangling ptr + && (layout.size() > self.as_ref().max || layout.align() > NGX_ALIGNMENT) + { + ngx_pfree(self.0.as_ptr(), ptr.as_ptr().cast()); + } + } +} + +impl AsRef for Pool { + #[inline] + fn as_ref(&self) -> &ngx_pool_t { + // SAFETY: this wrapper should be constructed with a valid pointer to ngx_pool_t + unsafe { self.0.as_ref() } + } +} + +impl AsMut for Pool { + #[inline] + fn as_mut(&mut self) -> &mut ngx_pool_t { + // SAFETY: this wrapper should be constructed with a valid pointer to ngx_pool_t + unsafe { self.0.as_mut() } + } +} impl Pool { /// Creates a new `Pool` from an `ngx_pool_t` pointer. @@ -16,8 +92,9 @@ impl Pool { /// The caller must ensure that a valid `ngx_pool_t` pointer is provided, pointing to valid /// memory and non-null. A null argument will cause an assertion failure and panic. pub unsafe fn from_ngx_pool(pool: *mut ngx_pool_t) -> Pool { - assert!(!pool.is_null()); - Pool(pool) + debug_assert!(!pool.is_null()); + debug_assert!(pool.is_aligned()); + Pool(NonNull::new_unchecked(pool)) } /// Creates a buffer of the specified size in the memory pool. @@ -25,7 +102,7 @@ impl Pool { /// Returns `Some(TemporaryBuffer)` if the buffer is successfully created, or `None` if /// allocation fails. pub fn create_buffer(&mut self, size: usize) -> Option { - let buf = unsafe { ngx_create_temp_buf(self.0, size) }; + let buf = unsafe { ngx_create_temp_buf(self.as_mut(), size) }; if buf.is_null() { return None; } @@ -80,7 +157,7 @@ impl Pool { /// # Safety /// This function is marked as unsafe because it involves raw pointer manipulation. unsafe fn add_cleanup_for_value(&mut self, value: *mut T) -> Result<(), ()> { - let cln = ngx_pool_cleanup_add(self.0, 0); + let cln = ngx_pool_cleanup_add(self.0.as_ptr(), 0); if cln.is_null() { return Err(()); } @@ -95,7 +172,7 @@ impl Pool { /// /// Returns a raw pointer to the allocated memory. pub fn alloc(&mut self, size: usize) -> *mut c_void { - unsafe { ngx_palloc(self.0, size) } + unsafe { ngx_palloc(self.0.as_ptr(), size) } } /// Allocates memory for a type from the pool. @@ -111,7 +188,7 @@ impl Pool { /// /// Returns a raw pointer to the allocated memory. pub fn calloc(&mut self, size: usize) -> *mut c_void { - unsafe { ngx_pcalloc(self.0, size) } + unsafe { ngx_pcalloc(self.0.as_ptr(), size) } } /// Allocates zeroed memory for a type from the pool. @@ -126,7 +203,7 @@ impl Pool { /// /// Returns a raw pointer to the allocated memory. pub fn alloc_unaligned(&mut self, size: usize) -> *mut c_void { - unsafe { ngx_pnalloc(self.0, size) } + unsafe { ngx_pnalloc(self.0.as_ptr(), size) } } /// Allocates unaligned memory for a type from the pool. From 2ae62af9005e6f2b06aa839f429a9e96fd538469 Mon Sep 17 00:00:00 2001 From: Aleksei Bavshin Date: Wed, 4 Jun 2025 23:01:12 -0700 Subject: [PATCH 08/12] feat: wrapper for ngx_slab_pool_t --- src/core/mod.rs | 2 + src/core/slab.rs | 140 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 142 insertions(+) create mode 100644 src/core/slab.rs diff --git a/src/core/mod.rs b/src/core/mod.rs index a5b87a98..c7b8bcae 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -1,10 +1,12 @@ mod buffer; mod pool; +mod slab; mod status; mod string; pub use buffer::*; pub use pool::*; +pub use slab::*; pub use status::*; pub use string::*; diff --git a/src/core/slab.rs b/src/core/slab.rs new file mode 100644 index 00000000..19e730dc --- /dev/null +++ b/src/core/slab.rs @@ -0,0 +1,140 @@ +use core::alloc::Layout; +use core::ptr::{self, NonNull}; + +use nginx_sys::{ + ngx_shm_zone_t, ngx_shmtx_lock, ngx_shmtx_unlock, ngx_slab_alloc_locked, ngx_slab_free_locked, + ngx_slab_pool_t, +}; + +use crate::allocator::{dangling_aligned, AllocError, Allocator}; + +/// Non-owning wrapper for an [`ngx_slab_pool_t`] pointer, providing methods for working with +/// shared memory slab pools. +/// +/// See . +#[derive(Clone, Debug)] +pub struct SlabPool(NonNull); + +unsafe impl Send for SlabPool {} +unsafe impl Sync for SlabPool {} + +unsafe impl Allocator for SlabPool { + #[inline] + fn allocate(&self, layout: Layout) -> Result, AllocError> { + self.lock().allocate(layout) + } + + #[inline] + unsafe fn deallocate(&self, ptr: NonNull, layout: Layout) { + self.lock().deallocate(ptr, layout) + } + + #[inline] + fn allocate_zeroed(&self, layout: Layout) -> Result, AllocError> { + self.lock().allocate_zeroed(layout) + } + + #[inline] + unsafe fn grow( + &self, + ptr: NonNull, + old_layout: Layout, + new_layout: Layout, + ) -> Result, AllocError> { + self.lock().grow(ptr, old_layout, new_layout) + } + + #[inline] + unsafe fn grow_zeroed( + &self, + ptr: NonNull, + old_layout: Layout, + new_layout: Layout, + ) -> Result, AllocError> { + self.lock().grow_zeroed(ptr, old_layout, new_layout) + } + + #[inline] + unsafe fn shrink( + &self, + ptr: NonNull, + old_layout: Layout, + new_layout: Layout, + ) -> Result, AllocError> { + self.lock().shrink(ptr, old_layout, new_layout) + } +} + +impl AsRef for SlabPool { + #[inline] + fn as_ref(&self) -> &ngx_slab_pool_t { + // SAFETY: this wrapper should be constructed with a valid pointer to ngx_slab_pool_t + unsafe { self.0.as_ref() } + } +} + +impl AsMut for SlabPool { + #[inline] + fn as_mut(&mut self) -> &mut ngx_slab_pool_t { + // SAFETY: this wrapper should be constructed with a valid pointer to ngx_slab_pool_t + unsafe { self.0.as_mut() } + } +} + +impl SlabPool { + /// Creates a new `SlabPool` from an initialized shared zone. + /// + /// # Safety + /// + /// Shared zone should be initialized and + pub unsafe fn from_shm_zone(shm_zone: &ngx_shm_zone_t) -> Option { + let ptr = NonNull::new(shm_zone.shm.addr)?.cast(); + Some(Self(ptr)) + } + + /// Locks the slab pool mutex. + #[inline] + pub fn lock(&self) -> LockedSlabPool { + let shpool = self.0.as_ptr(); + unsafe { ngx_shmtx_lock(ptr::addr_of_mut!((*shpool).mutex)) }; + LockedSlabPool(self.0) + } +} + +/// Wrapper for a locked [`ngx_slab_pool_t`] pointer. +pub struct LockedSlabPool(NonNull); + +unsafe impl Allocator for LockedSlabPool { + fn allocate(&self, layout: Layout) -> Result, AllocError> { + if layout.size() == 0 { + return Ok(NonNull::slice_from_raw_parts( + dangling_aligned(layout.align()), + layout.size(), + )); + } + + let raw = unsafe { ngx_slab_alloc_locked(self.0.as_ptr(), layout.size()) }; + let ptr = NonNull::new(raw.cast()).ok_or(AllocError)?; + + // TODO: NonNull::align_offset with msrv >= 1.80 + if raw.align_offset(layout.align()) != 0 { + unsafe { self.deallocate(ptr, layout) }; + return Err(AllocError); + } + + Ok(NonNull::slice_from_raw_parts(ptr, layout.size())) + } + + unsafe fn deallocate(&self, ptr: NonNull, layout: Layout) { + if layout.size() != 0 { + ngx_slab_free_locked(self.0.as_ptr(), ptr.as_ptr().cast()) + } + } +} + +impl Drop for LockedSlabPool { + fn drop(&mut self) { + let shpool = unsafe { self.0.as_mut() }; + unsafe { ngx_shmtx_unlock(&mut shpool.mutex) } + } +} From 43e83dfe5ef6e2cd7620383456db812511ac99f3 Mon Sep 17 00:00:00 2001 From: Aleksei Bavshin Date: Thu, 5 Jun 2025 15:18:42 -0700 Subject: [PATCH 09/12] feat: owned byte string type with Allocator support --- src/core/string.rs | 370 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 368 insertions(+), 2 deletions(-) diff --git a/src/core/string.rs b/src/core/string.rs index 1db73f8a..e48fe6d0 100644 --- a/src/core/string.rs +++ b/src/core/string.rs @@ -23,6 +23,9 @@ macro_rules! ngx_string { }}; } +#[cfg(feature = "alloc")] +pub use self::_alloc::NgxString; + /// Representation of a borrowed [Nginx string]. /// /// [Nginx string]: https://nginx.org/en/docs/dev/development_guide.html#string_overview @@ -196,8 +199,318 @@ impl_partial_ord_eq_from!(NgxStr, &'a [u8; N]; const N: usize); impl_partial_ord_eq_from!(NgxStr, &'a str); #[cfg(feature = "alloc")] -mod _alloc_impls { +mod _alloc { + use core::borrow::Borrow; + use core::hash; + use core::ops; + use core::ptr; + use super::*; + + use crate::allocator::collections::TryReserveError; + use crate::allocator::vec::Vec; + use crate::allocator::{self, Allocator}; + + /// Owned byte string type with Allocator support. + /// + /// Inspired by `bstr` and `feature(bstr)`, with two important differences: + /// - Allocator always have to be specified, + /// - any allocating methods are failible and require explicit handling of the result. + #[derive(Clone)] + #[repr(transparent)] + pub struct NgxString(Vec) + where + A: Allocator + Clone; + + impl NgxString + where + A: Allocator + Clone, + { + /// Constructs a new, empty `NgxString`. + /// + /// No allocations will be made until data is added to the string. + pub fn new_in(alloc: A) -> Self { + Self(Vec::new_in(alloc)) + } + + /// Tries to construct a new `NgxString` from a byte slice. + #[inline] + pub fn try_from_bytes_in( + bytes: impl AsRef<[u8]>, + alloc: A, + ) -> Result { + let mut this = Self::new_in(alloc); + this.try_reserve_exact(bytes.as_ref().len())?; + this.0.extend_from_slice(bytes.as_ref()); + Ok(this) + } + + /// Returns a reference to the underlying allocator + #[inline] + pub fn allocator(&self) -> &A { + self.0.allocator() + } + + /// Returns this `NgxString`'s capacity, in bytes. + #[inline] + pub fn capacity(&self) -> usize { + self.0.capacity() + } + + /// Returns `true` if this `NgxString` has a length of zero, and `false` otherwise. + #[inline] + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Return this `NgxString`'s length, in bytes. + #[inline] + pub fn len(&self) -> usize { + self.0.len() + } + + /// Appends bytes if there is sufficient spare capacity. + /// + /// Returns the number of remaining bytes on overflow. + #[inline] + pub fn append_within_capacity(&mut self, other: impl AsRef<[u8]>) -> Result<(), usize> { + let other = other.as_ref(); + if self.0.len() == self.0.capacity() { + return Err(other.len()); + } + + let n = cmp::min(self.0.capacity() - self.0.len(), other.len()); + unsafe { + // SAFETY: + // - self.0 has at least n writable bytes allocated past self.0.len(), + // - other has at least n bytes available for reading. + // - self.0 internal buffer will be initialized until len + n after this operation + // - other is not borrowed from `self` + let p = self.0.as_mut_ptr().add(self.0.len()); + ptr::copy_nonoverlapping(other.as_ptr(), p, n); + self.0.set_len(self.0.len() + n); + } + + match other.len() - n { + 0 => Ok(()), + x => Err(x), + } + } + + /// Tries to append the bytes to the `NgxString`. + #[inline] + pub fn try_append(&mut self, other: impl AsRef<[u8]>) -> Result<(), TryReserveError> { + let other = other.as_ref(); + self.0.try_reserve_exact(other.len())?; + self.0.extend_from_slice(other); + Ok(()) + } + + /// Tries to reserve capacity for at least `additional` more bytes. + #[inline] + pub fn try_reserve(&mut self, additional: usize) -> Result<(), TryReserveError> { + self.0.try_reserve(additional) + } + + /// Tries to reserve the minimum capacity for at least `additional` more bytes. + #[inline] + pub fn try_reserve_exact(&mut self, additional: usize) -> Result<(), TryReserveError> { + self.0.try_reserve_exact(additional) + } + + #[inline] + pub(crate) fn as_bytes(&self) -> &[u8] { + &self.0 + } + + #[inline] + pub(crate) fn as_bytes_mut(&mut self) -> &mut [u8] { + &mut self.0 + } + + #[inline] + pub(crate) fn as_ngx_str(&self) -> &NgxStr { + NgxStr::from_bytes(self.0.as_slice()) + } + + #[inline] + pub(crate) fn as_ngx_str_mut(&mut self) -> &mut NgxStr { + NgxStr::from_bytes_mut(self.0.as_mut_slice()) + } + } + + impl AsRef for NgxString + where + A: Allocator + Clone, + { + fn as_ref(&self) -> &NgxStr { + self.as_ngx_str() + } + } + + impl AsMut for NgxString + where + A: Allocator + Clone, + { + fn as_mut(&mut self) -> &mut NgxStr { + self.as_ngx_str_mut() + } + } + + impl AsRef<[u8]> for NgxString + where + A: Allocator + Clone, + { + #[inline] + fn as_ref(&self) -> &[u8] { + self.as_bytes() + } + } + + impl AsMut<[u8]> for NgxString + where + A: Allocator + Clone, + { + #[inline] + fn as_mut(&mut self) -> &mut [u8] { + self.as_bytes_mut() + } + } + + impl Borrow for NgxString + where + A: Allocator + Clone, + { + fn borrow(&self) -> &NgxStr { + self.as_ngx_str() + } + } + + impl Borrow<[u8]> for NgxString + where + A: Allocator + Clone, + { + fn borrow(&self) -> &[u8] { + self.0.as_slice() + } + } + + impl fmt::Debug for NgxString + where + A: Allocator + Clone, + { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // XXX: Use debug_tuple() and feature(debug_closure_helpers) once it's stabilized + f.write_str("NgxString(")?; + nginx_sys::detail::debug_bytes(f, &self.0)?; + f.write_str(")") + } + } + + impl ops::Deref for NgxString + where + A: Allocator + Clone, + { + type Target = NgxStr; + + fn deref(&self) -> &Self::Target { + self.as_ngx_str() + } + } + + impl ops::DerefMut for NgxString + where + A: Allocator + Clone, + { + fn deref_mut(&mut self) -> &mut Self::Target { + self.as_ngx_str_mut() + } + } + + impl fmt::Display for NgxString + where + A: Allocator + Clone, + { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(self.as_ngx_str(), f) + } + } + + impl hash::Hash for NgxString + where + A: Allocator + Clone, + { + fn hash(&self, state: &mut H) { + self.0.hash(state); + } + } + + // `NgxString`'s with different allocators should be comparable + + impl PartialEq> for NgxString + where + A1: Allocator + Clone, + A2: Allocator + Clone, + { + fn eq(&self, other: &NgxString) -> bool { + PartialEq::eq(self.as_bytes(), other.as_bytes()) + } + } + + impl Eq for NgxString where A: Allocator + Clone {} + + impl PartialOrd> for NgxString + where + A1: Allocator + Clone, + A2: Allocator + Clone, + { + fn partial_cmp(&self, other: &NgxString) -> Option { + Some(Ord::cmp(self.as_bytes(), other.as_bytes())) + } + } + + impl Ord for NgxString + where + A: Allocator + Clone, + { + fn cmp(&self, other: &Self) -> cmp::Ordering { + Ord::cmp(self.as_bytes(), other.as_bytes()) + } + } + + impl allocator::TryCloneIn for NgxString { + type Target = NgxString; + + fn try_clone_in( + &self, + alloc: A, + ) -> Result, allocator::AllocError> { + NgxString::try_from_bytes_in(self.as_bytes(), alloc).map_err(|_| allocator::AllocError) + } + } + + impl fmt::Write for NgxString + where + A: Allocator + Clone, + { + fn write_str(&mut self, s: &str) -> fmt::Result { + self.append_within_capacity(s).map_err(|_| fmt::Error) + } + } + + // Implement byte comparisons directly, leave the rest to Deref. + + impl_partial_eq!(NgxString, &'a [u8]; A: Allocator + Clone); + impl_partial_eq!(NgxString, &'a [u8; N]; A: Allocator + Clone, const N: usize); + impl_partial_eq!(NgxString, &'a NgxStr; A: Allocator + Clone); + impl_partial_eq!(NgxString, ngx_str_t; A: Allocator + Clone); + + impl_partial_ord!(NgxString, &'a [u8]; A: Allocator + Clone); + impl_partial_ord!(NgxString, &'a [u8; N]; A: Allocator + Clone, const N: usize); + impl_partial_ord!(NgxString, &'a NgxStr; A: Allocator + Clone); + impl_partial_ord!(NgxString, ngx_str_t; A: Allocator + Clone); + impl_partial_eq!(NgxStr, String); impl_partial_eq!(&'a NgxStr, String); impl_partial_ord!(NgxStr, String); @@ -214,7 +527,7 @@ mod tests { use super::*; #[test] - fn test_comparisons() { + fn test_str_comparisons() { let string = "test".to_string(); let ngx_string = ngx_str_t { data: string.as_ptr().cast_mut(), @@ -239,6 +552,59 @@ mod tests { assert_eq!(ns, "test"); } + #[test] + #[cfg(feature = "alloc")] + fn test_string_comparisons() { + use crate::allocator::Global; + + let string = "test".to_string(); + let ngx_string = ngx_str_t { + data: string.as_ptr().cast_mut(), + len: string.len(), + }; + let borrowed: &NgxStr = string.as_bytes().into(); + let owned = NgxString::try_from_bytes_in(&string, Global).unwrap(); + + assert_eq!(string.as_bytes(), owned); + assert_eq!(ngx_string, owned); + assert_eq!(borrowed, owned); + assert_eq!(b"test", owned); + assert_eq!(owned, string.as_bytes()); + assert_eq!(owned, borrowed); + assert_eq!(owned, b"test"); + + // String comparisons via Deref + assert_eq!(string, *owned); + assert_eq!(ngx_string, owned); + assert_eq!(string.as_str(), *owned); + assert_eq!("test", *owned); + assert_eq!(*owned, string); + assert_eq!(*owned, string.as_str()); + assert_eq!(*owned, "test"); + } + + #[test] + #[cfg(feature = "alloc")] + fn test_string_write() { + use core::fmt::Write; + + use crate::allocator::Global; + + let h = NgxStr::from_bytes(b"Hello"); + let w = NgxStr::from_bytes(b"world"); + + let mut s = NgxString::new_in(Global); + s.try_reserve(16).expect("reserve"); + + // Remember ptr and len of internal buffer + let saved = (s.as_bytes().as_ptr(), s.capacity()); + + write!(s, "{h} {w}!").expect("write"); + + assert_eq!(s, b"Hello world!"); + assert_eq!((s.as_bytes().as_ptr(), s.capacity()), saved); + } + #[test] fn test_lifetimes() { let a: &NgxStr = "Hello World!".into(); From 99862fae4c2e1545310aa4c0e6dcb802b59a0a34 Mon Sep 17 00:00:00 2001 From: Aleksei Bavshin Date: Mon, 9 Jun 2025 15:37:33 -0700 Subject: [PATCH 10/12] feat: ngx::sync::RwLock implementation --- Cargo.lock | 1 + Cargo.toml | 1 + nginx-sys/build/main.rs | 1 + nginx-sys/src/lib.rs | 17 ++++++ src/lib.rs | 2 + src/sync.rs | 123 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 145 insertions(+) create mode 100644 src/sync.rs diff --git a/Cargo.lock b/Cargo.lock index b9c09224..550fb04e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -568,6 +568,7 @@ name = "ngx" version = "0.5.0" dependencies = [ "allocator-api2", + "lock_api", "nginx-sys", "target-triple", ] diff --git a/Cargo.toml b/Cargo.toml index 8578e125..859a2d61 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ rust-version.workspace = true [dependencies] allocator-api2 = { version = "0.2.21", default-features = false } +lock_api = "0.4.13" nginx-sys = { path = "nginx-sys", default-features=false, version = "0.5.0"} [features] diff --git a/nginx-sys/build/main.rs b/nginx-sys/build/main.rs index f6826e6a..6303693d 100644 --- a/nginx-sys/build/main.rs +++ b/nginx-sys/build/main.rs @@ -27,6 +27,7 @@ const NGX_CONF_FEATURES: &[&str] = &[ "have_kqueue", "have_memalign", "have_posix_memalign", + "have_sched_yield", "have_variadic_macros", "http", "http_cache", diff --git a/nginx-sys/src/lib.rs b/nginx-sys/src/lib.rs index ffb7eddf..0b1ef6c4 100644 --- a/nginx-sys/src/lib.rs +++ b/nginx-sys/src/lib.rs @@ -163,6 +163,23 @@ pub fn ngx_random() -> core::ffi::c_long { } } +/// Causes the calling thread to relinquish the CPU. +#[inline] +pub fn ngx_sched_yield() { + #[cfg(windows)] + unsafe { + SwitchToThread() + }; + #[cfg(all(not(windows), ngx_feature = "have_sched_yield"))] + unsafe { + sched_yield() + }; + #[cfg(not(any(windows, ngx_feature = "have_sched_yield")))] + unsafe { + usleep(1) + } +} + /// Returns cached timestamp in seconds, updated at the start of the event loop iteration. /// /// Can be stale when accessing from threads, see [ngx_time_update]. diff --git a/src/lib.rs b/src/lib.rs index ece02efd..c31de461 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -68,6 +68,8 @@ pub mod http; /// This module provides an interface into the NGINX logger framework. pub mod log; +pub mod sync; + /// Define modules exported by this library. /// /// These are normally generated by the Nginx module system, but need to be diff --git a/src/sync.rs b/src/sync.rs new file mode 100644 index 00000000..6bdbd6f2 --- /dev/null +++ b/src/sync.rs @@ -0,0 +1,123 @@ +//! Synchronization primitives over shared memory. +//! +//! This module provides an alternative implementation for the `ngx_atomic_t` type, +//! `ngx_atomic_*`/`ngx_rwlock_*` family of functions and related usage patterns from nginx. +//! +//! `` contains a wide variety of implementation variants for different platforms and +//! build configurations. It's not feasible to properly expose all of these to the Rust code, and we +//! are not going to. The implementation here uses similar logic on the foundation of the +//! [core::sync::atomic] types and is intentionally _not interoperable_ with the nginx atomics. +//! Thus, it's only suitable for use for new shared memory structures instead of, for example, +//! interacting with the upstream zones. +//! +//! One potential pitfall here is that atomics in Rust are specified in terms of threads, and we use +//! the types in this module for interprocess synchronization. This should not be an issue though, +//! as Rust refers to the C/C++11 memory model for atomics, and there's a following note in +//! [atomics.lockfree]: +//! +//! > [Note: Operations that are lock-free should also be address-free. That is, atomic operations +//! > on the same memory location via two different addresses will communicate atomically. The +//! > implementation should not depend on any per-process state. This restriction enables +//! > communication via memory that is mapped into a process more than once and by memory that is +//! > shared between two processes. — end note] +//! +//! In practice, this recommendation is applied in all the implementations that matter to us. +use core::sync::atomic::{self, Ordering}; + +use nginx_sys::ngx_sched_yield; + +const NGX_RWLOCK_SPIN: usize = 2048; +const NGX_RWLOCK_WLOCK: usize = usize::MAX; + +type NgxAtomic = atomic::AtomicUsize; + +/// Raw lock type. +/// +pub struct RawSpinlock(NgxAtomic); + +/// Reader-writer lock over an atomic variable, based on the nginx rwlock implementation. +pub type RwLock = lock_api::RwLock; + +/// RAII structure used to release the shared read access of a lock when dropped. +pub type RwLockReadGuard<'a, T> = lock_api::RwLockReadGuard<'a, RawSpinlock, T>; + +/// RAII structure used to release the exclusive write access of a lock when dropped. +pub type RwLockWriteGuard<'a, T> = lock_api::RwLockWriteGuard<'a, RawSpinlock, T>; + +unsafe impl lock_api::RawRwLock for RawSpinlock { + // Only used for initialization, will not be mutated + #[allow(clippy::declare_interior_mutable_const)] + const INIT: RawSpinlock = RawSpinlock(NgxAtomic::new(0)); + + type GuardMarker = lock_api::GuardNoSend; + + fn lock_shared(&self) { + loop { + if self.try_lock_shared() { + return; + } + + if unsafe { nginx_sys::ngx_ncpu > 1 } { + for n in 0..NGX_RWLOCK_SPIN { + for _ in 0..n { + core::hint::spin_loop() + } + + if self.try_lock_shared() { + return; + } + } + } + + ngx_sched_yield() + } + } + + fn try_lock_shared(&self) -> bool { + let value = self.0.load(Ordering::Acquire); + + if value == NGX_RWLOCK_WLOCK { + return false; + } + + self.0 + .compare_exchange(value, value + 1, Ordering::Acquire, Ordering::Relaxed) + .is_ok() + } + + unsafe fn unlock_shared(&self) { + self.0.fetch_sub(1, Ordering::Release); + } + + fn lock_exclusive(&self) { + loop { + if self.try_lock_exclusive() { + return; + } + + if unsafe { nginx_sys::ngx_ncpu > 1 } { + for n in 0..NGX_RWLOCK_SPIN { + for _ in 0..n { + core::hint::spin_loop() + } + + if self.try_lock_exclusive() { + return; + } + } + } + + ngx_sched_yield() + } + } + + fn try_lock_exclusive(&self) -> bool { + self.0 + .compare_exchange(0, NGX_RWLOCK_WLOCK, Ordering::Acquire, Ordering::Relaxed) + .is_ok() + } + + unsafe fn unlock_exclusive(&self) { + self.0.store(0, Ordering::Release) + } +} From 4ec5ec6b2708a2f4600ef84cdcd0794a64bbd671 Mon Sep 17 00:00:00 2001 From: Aleksei Bavshin Date: Tue, 3 Jun 2025 13:59:41 -0700 Subject: [PATCH 11/12] feat: ngx_rbtree_t wrapper --- nginx-sys/src/lib.rs | 2 + nginx-sys/src/rbtree.rs | 84 +++++++++++++++++++++ src/core/mod.rs | 2 + src/core/rbtree.rs | 160 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 248 insertions(+) create mode 100644 nginx-sys/src/rbtree.rs create mode 100644 src/core/rbtree.rs diff --git a/nginx-sys/src/lib.rs b/nginx-sys/src/lib.rs index 0b1ef6c4..e7038431 100644 --- a/nginx-sys/src/lib.rs +++ b/nginx-sys/src/lib.rs @@ -7,6 +7,7 @@ mod event; #[cfg(ngx_feature = "http")] mod http; mod queue; +mod rbtree; #[cfg(ngx_feature = "stream")] mod stream; mod string; @@ -33,6 +34,7 @@ pub use event::*; #[cfg(ngx_feature = "http")] pub use http::*; pub use queue::*; +pub use rbtree::*; #[cfg(ngx_feature = "stream")] pub use stream::*; diff --git a/nginx-sys/src/rbtree.rs b/nginx-sys/src/rbtree.rs new file mode 100644 index 00000000..88b94274 --- /dev/null +++ b/nginx-sys/src/rbtree.rs @@ -0,0 +1,84 @@ +use core::ptr; + +use crate::bindings::{ngx_rbtree_insert_pt, ngx_rbtree_node_t, ngx_rbtree_t}; + +/// Get a reference to the beginning of a tree element data structure, +/// considering the link field offset in it. +/// +/// # Safety +/// +/// `$node` must be a valid pointer to the field `$link` in the struct `$type` +#[macro_export] +macro_rules! ngx_rbtree_data { + ($node:expr, $type:path, $link:ident) => { + $node + .byte_sub(::core::mem::offset_of!($type, $link)) + .cast::<$type>() + }; +} + +/// Initializes the RbTree with specified sentinel and insert function. +/// +/// # Safety +/// +/// All of the pointers passed must be valid. +/// `sentinel` is expected to be valid for the whole lifetime of the `tree`. +/// +pub unsafe fn ngx_rbtree_init( + tree: *mut ngx_rbtree_t, + sentinel: *mut ngx_rbtree_node_t, + insert: ngx_rbtree_insert_pt, +) { + ngx_rbtree_sentinel_init(sentinel); + (*tree).root = sentinel; + (*tree).sentinel = sentinel; + (*tree).insert = insert; +} + +/// Marks the tree node as red. +/// +/// # Safety +/// +/// `node` must be a valid pointer to a [ngx_rbtree_node_t]. +#[inline] +pub unsafe fn ngx_rbt_red(node: *mut ngx_rbtree_node_t) { + (*node).color = 1 +} + +/// Marks the tree node as black. +/// +/// # Safety +/// +/// `node` must be a valid pointer to a [ngx_rbtree_node_t]. +#[inline] +pub unsafe fn ngx_rbt_black(node: *mut ngx_rbtree_node_t) { + (*node).color = 0 +} + +/// Initializes the sentinel node. +/// +/// # Safety +/// +/// `node` must be a valid pointer to a [ngx_rbtree_node_t]. +#[inline] +pub unsafe fn ngx_rbtree_sentinel_init(node: *mut ngx_rbtree_node_t) { + ngx_rbt_black(node) +} + +/// Returns the least (leftmost) node of the tree. +/// +/// # Safety +/// +/// `node` must be a valid pointer to a [ngx_rbtree_node_t]. +/// `sentinel` must be a valid pointer to the sentinel node in the same Red-Black tree. +#[inline] +pub unsafe fn ngx_rbtree_min( + mut node: *mut ngx_rbtree_node_t, + sentinel: *mut ngx_rbtree_node_t, +) -> *mut ngx_rbtree_node_t { + while !ptr::addr_eq((*node).left, sentinel) { + node = (*node).left; + } + + node +} diff --git a/src/core/mod.rs b/src/core/mod.rs index c7b8bcae..8ed847ff 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -1,11 +1,13 @@ mod buffer; mod pool; +pub mod rbtree; mod slab; mod status; mod string; pub use buffer::*; pub use pool::*; +pub use rbtree::RbTree; pub use slab::*; pub use status::*; pub use string::*; diff --git a/src/core/rbtree.rs b/src/core/rbtree.rs new file mode 100644 index 00000000..2a04f813 --- /dev/null +++ b/src/core/rbtree.rs @@ -0,0 +1,160 @@ +//! Wrapper over the `ngx_rbtree_t`. + +use core::alloc::Layout; +use core::cmp::Ordering; +#[allow(deprecated)] +use core::hash::{Hash, Hasher, SipHasher}; +use core::marker::PhantomData; +use core::mem; +use core::ptr::{self, NonNull}; + +use nginx_sys::{ + ngx_rbtree_data, ngx_rbtree_init, ngx_rbtree_insert, ngx_rbtree_key_t, ngx_rbtree_node_t, + ngx_rbtree_t, +}; + +use crate::allocator::{self, AllocError, Allocator}; + +/// Wrapper over the `ngx_rbtree_t`. +#[derive(Debug)] +pub struct RbTree { + tree: ngx_rbtree_t, + sentinel: NonNull, + alloc: A, + // Magic line for dropck (Nomicon 3.9, 3.10) + _ph: PhantomData<(K, V)>, +} + +struct RbTreeNode { + node: ngx_rbtree_node_t, + key: K, + value: V, +} + +impl RbTreeNode +where + K: Hash, +{ + fn new(key: K, value: V) -> Self { + #[allow(deprecated)] + let mut h = SipHasher::new(); + key.hash(&mut h); + let hash = h.finish() as ngx_rbtree_key_t; + + let mut node: ngx_rbtree_node_t = unsafe { mem::zeroed() }; + node.key = hash; + + Self { node, key, value } + } +} + +impl RbTree +where + K: Hash + Ord, + A: Allocator + Clone, +{ + /// Returns a reference to the underlying allocator. + pub fn allocator(&self) -> &A { + &self.alloc + } + + /// Attempts to create and initialize a new RbTree with specified allocator. + pub fn try_new_in(alloc: A) -> Result { + let layout = Layout::new::(); + let sentinel: NonNull = alloc.allocate_zeroed(layout)?.cast(); + + let mut this = RbTree { + tree: unsafe { mem::zeroed() }, + sentinel, + alloc, + _ph: Default::default(), + }; + + unsafe { + ngx_rbtree_init( + &mut this.tree, + this.sentinel.as_ptr(), + Some(nginx_sys::ngx_rbtree_insert_value), + ) + }; + + Ok(this) + } + + /// Attempts to insert a new element into the tree. + pub fn try_insert(&mut self, key: K, value: V) -> Result<&mut V, AllocError> { + let mut node = if let Some(mut node) = self.lookup(&key) { + unsafe { node.as_mut().value = value }; + node + } else { + let node = RbTreeNode::new(key, value); + let mut node = allocator::allocate(node, self.allocator())?; + unsafe { ngx_rbtree_insert(&mut self.tree, &mut node.as_mut().node) }; + node + }; + + Ok(unsafe { &mut node.as_mut().value }) + } + + /// Returns a reference to the value corresponding to the key. + pub fn get(&self, key: &Q) -> Option<&V> + where + Q: Hash + Ord + ?Sized, + K: core::borrow::Borrow, + { + self.lookup(key).map(|x| unsafe { &x.as_ref().value }) + } + + /// Returns a mutable reference to the value corresponding to the key. + pub fn get_mut(&mut self, key: &Q) -> Option<&mut V> + where + Q: Hash + Ord + ?Sized, + K: core::borrow::Borrow, + { + self.lookup(key) + .map(|mut x| unsafe { &mut x.as_mut().value }) + } + + fn lookup(&self, key: &Q) -> Option>> + where + Q: Hash + Ord + ?Sized, + K: Hash + Ord + core::borrow::Borrow, + { + #[allow(deprecated)] + let mut h = SipHasher::new(); + key.hash(&mut h); + let hash = h.finish() as ngx_rbtree_key_t; + + let mut node = self.tree.root; + + while !ptr::addr_eq(node, self.tree.sentinel) { + let k = unsafe { (*node).key }; + + if hash != k { + node = if hash < k { + (unsafe { *node }).left + } else { + (unsafe { *node }).right + }; + + continue; + } + + let n = unsafe { ngx_rbtree_data!(node, RbTreeNode, node) }; + + match Ord::cmp(unsafe { (*n).key.borrow() }, key) { + Ordering::Less => { + node = unsafe { (*node).left }; + continue; + } + Ordering::Equal => return Some(unsafe { NonNull::new_unchecked(n) }), + Ordering::Greater => { + node = unsafe { (*node).left }; + continue; + } + } + } + + None + } +} From 770ada8eb97caa1a5e97c1394a1cdc9023aba658 Mon Sep 17 00:00:00 2001 From: Aleksei Bavshin Date: Thu, 5 Jun 2025 20:08:55 -0700 Subject: [PATCH 12/12] feat(examples): shared memory usage example --- .github/workflows/nginx.yaml | 1 + examples/Cargo.toml | 5 + examples/config | 8 + examples/shared_dict.rs | 310 +++++++++++++++++++++++++++++++++++ examples/t/shared_dict.t | 93 +++++++++++ 5 files changed, 417 insertions(+) create mode 100644 examples/shared_dict.rs create mode 100644 examples/t/shared_dict.t diff --git a/.github/workflows/nginx.yaml b/.github/workflows/nginx.yaml index a5994b90..d0efe15a 100644 --- a/.github/workflows/nginx.yaml +++ b/.github/workflows/nginx.yaml @@ -52,6 +52,7 @@ env: load_module ${{ github.workspace }}/nginx/objs/ngx_http_async_module.so; load_module ${{ github.workspace }}/nginx/objs/ngx_http_awssigv4_module.so; load_module ${{ github.workspace }}/nginx/objs/ngx_http_curl_module.so; + load_module ${{ github.workspace }}/nginx/objs/ngx_http_shared_dict_module.so; load_module ${{ github.workspace }}/nginx/objs/ngx_http_upstream_custom_module.so; OPENSSL_VERSION: '3.0.16' diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 3e199861..9006aba7 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -51,6 +51,11 @@ name = "async" path = "async.rs" crate-type = ["cdylib"] +[[example]] +name = "shared_dict" +path = "shared_dict.rs" +crate-type = ["cdylib"] + [features] default = ["export-modules", "ngx/vendored"] # Generate `ngx_modules` table with module exports diff --git a/examples/config b/examples/config index 5f1ce60b..6b763652 100644 --- a/examples/config +++ b/examples/config @@ -39,6 +39,14 @@ if [ $HTTP = YES ]; then ngx_rust_module fi + if :; then + ngx_module_name=ngx_http_shared_dict_module + ngx_module_libs= + ngx_rust_target_name=shared_dict + + ngx_rust_module + fi + if :; then ngx_module_name=ngx_http_upstream_custom_module ngx_module_libs= diff --git a/examples/shared_dict.rs b/examples/shared_dict.rs new file mode 100644 index 00000000..d0807cef --- /dev/null +++ b/examples/shared_dict.rs @@ -0,0 +1,310 @@ +#![no_std] +use ::core::ffi::{c_char, c_void}; +use ::core::{mem, ptr, slice}; + +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_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_CONF_TAKE2, NGX_ERROR, NGX_HTTP_MAIN_CONF, + NGX_HTTP_MAIN_CONF_OFFSET, NGX_HTTP_MODULE, NGX_HTTP_VAR_CHANGEABLE, NGX_HTTP_VAR_NOCACHEABLE, + NGX_LOG_EMERG, NGX_OK, +}; +use ngx::core::{NgxStr, NgxString, Pool, RbTree, SlabPool, Status, NGX_CONF_ERROR, NGX_CONF_OK}; +use ngx::http::{HttpModule, HttpModuleMainConf}; +use ngx::{ngx_conf_log_error, ngx_log_debug, ngx_string}; + +struct HttpSharedDictModule; + +impl HttpModule for HttpSharedDictModule { + fn module() -> &'static ngx_module_t { + unsafe { &*ptr::addr_of!(ngx_http_shared_dict_module) } + } +} + +unsafe impl HttpModuleMainConf for HttpSharedDictModule { + type MainConf = SharedDictMainConfig; +} + +static mut NGX_HTTP_SHARED_DICT_COMMANDS: [ngx_command_t; 3] = [ + ngx_command_t { + name: ngx_string!("shared_dict_zone"), + type_: (NGX_HTTP_MAIN_CONF | NGX_CONF_TAKE2) as ngx_uint_t, + set: Some(ngx_http_shared_dict_add_zone), + conf: NGX_HTTP_MAIN_CONF_OFFSET, + offset: 0, + post: ptr::null_mut(), + }, + ngx_command_t { + name: ngx_string!("shared_dict"), + type_: (NGX_HTTP_MAIN_CONF | NGX_CONF_TAKE2) as ngx_uint_t, + set: Some(ngx_http_shared_dict_add_variable), + conf: NGX_HTTP_MAIN_CONF_OFFSET, + offset: 0, + post: ptr::null_mut(), + }, + ngx_command_t::empty(), +]; + +static NGX_HTTP_SHARED_DICT_MODULE_CTX: ngx_http_module_t = ngx_http_module_t { + preconfiguration: None, + postconfiguration: Some(HttpSharedDictModule::postconfiguration), + create_main_conf: Some(HttpSharedDictModule::create_main_conf), + init_main_conf: None, + create_srv_conf: None, + merge_srv_conf: None, + create_loc_conf: None, + merge_loc_conf: None, +}; + +// Generate the `ngx_modules` table with exported modules. +// This feature is required to build a 'cdylib' dynamic module outside of the NGINX buildsystem. +#[cfg(feature = "export-modules")] +ngx::ngx_modules!(ngx_http_shared_dict_module); + +#[used] +#[allow(non_upper_case_globals)] +#[cfg_attr(not(feature = "export-modules"), no_mangle)] +pub static mut ngx_http_shared_dict_module: ngx_module_t = ngx_module_t { + ctx: ptr::addr_of!(NGX_HTTP_SHARED_DICT_MODULE_CTX) as _, + commands: unsafe { ptr::addr_of_mut!(NGX_HTTP_SHARED_DICT_COMMANDS[0]) }, + type_: NGX_HTTP_MODULE as _, + ..ngx_module_t::default() +}; + +type SharedData = ngx::sync::RwLock, NgxString, SlabPool>>; + +#[derive(Debug)] +struct SharedDictMainConfig { + shm_zone: *mut ngx_shm_zone_t, +} + +impl Default for SharedDictMainConfig { + fn default() -> Self { + Self { + shm_zone: ptr::null_mut(), + } + } +} + +extern "C" fn ngx_http_shared_dict_add_zone( + cf: *mut ngx_conf_t, + _cmd: *mut ngx_command_t, + conf: *mut c_void, +) -> *mut c_char { + let cf = unsafe { cf.as_mut().unwrap() }; + let smcf = unsafe { + conf.cast::() + .as_mut() + .expect("shared dict main config") + }; + + let args = + unsafe { slice::from_raw_parts_mut((*cf.args).elts as *mut ngx_str_t, (*cf.args).nelts) }; + + let name: ngx_str_t = args[1]; + let size = unsafe { ngx_parse_size(&mut args[2]) }; + if size == -1 { + return NGX_CONF_ERROR; + } + + smcf.shm_zone = unsafe { + ngx_shared_memory_add( + cf, + ptr::addr_of!(name).cast_mut(), + size as usize, + ptr::addr_of_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: RbTree, NgxString, SlabPool> = + RbTree::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(_) => NGX_OK as _, + } +} + +extern "C" fn ngx_http_shared_dict_add_variable( + cf: *mut ngx_conf_t, + _cmd: *mut ngx_command_t, + _conf: *mut c_void, +) -> *mut c_char { + let cf = unsafe { cf.as_mut().unwrap() }; + let mut pool = unsafe { Pool::from_ngx_pool(cf.pool) }; + + let key = pool.calloc_type::(); + if key.is_null() { + return NGX_CONF_ERROR; + } + + let args = + unsafe { slice::from_raw_parts_mut((*cf.args).elts as *mut ngx_str_t, (*cf.args).nelts) }; + + let mut ccv: ngx_http_compile_complex_value_t = unsafe { mem::zeroed() }; + ccv.cf = cf; + ccv.value = &mut args[1]; + ccv.complex_value = key; + + if unsafe { nginx_sys::ngx_http_compile_complex_value(&mut ccv) } != NGX_OK as _ { + return NGX_CONF_ERROR; + } + + let mut name = args[2]; + + if name.as_bytes()[0] != b'$' { + ngx_conf_log_error!(NGX_LOG_EMERG, cf, "invalid variable name \"{name}\""); + return NGX_CONF_ERROR; + } + + name.data = unsafe { name.data.add(1) }; + name.len -= 1; + + let var = unsafe { + ngx_http_add_variable( + cf, + &mut name, + (NGX_HTTP_VAR_CHANGEABLE | NGX_HTTP_VAR_NOCACHEABLE) as ngx_uint_t, + ) + }; + if var.is_null() { + return NGX_CONF_ERROR; + } + + unsafe { + (*var).get_handler = Some(ngx_http_shared_dict_get_variable); + (*var).set_handler = Some(ngx_http_shared_dict_set_variable); + (*var).data = key as usize; + } + + NGX_CONF_OK +} + +extern "C" fn ngx_http_shared_dict_get_variable( + r: *mut ngx_http_request_t, + v: *mut ngx_http_variable_value_t, + data: usize, +) -> ngx_int_t { + let r = unsafe { &mut *r }; + let v = unsafe { &mut *v }; + let smcf = HttpSharedDictModule::main_conf_mut(r).expect("shared dict main config"); + + let mut key = ngx_str_t::empty(); + if unsafe { ngx_http_complex_value(r, data as _, &mut key) } != NGX_OK as _ { + return NGX_ERROR as _; + } + + let key = unsafe { NgxStr::from_ngx_str(key) }; + + let Ok(shared) = ngx_http_shared_dict_get_shared(unsafe { &mut *smcf.shm_zone }) else { + return NGX_ERROR as _; + }; + + let value = shared + .read() + .get(key) + .and_then(|x| unsafe { ngx_str_t::from_bytes(r.pool, x.as_bytes()) }); + + ngx_log_debug!( + unsafe { (*r.connection).log }, + "shared dict: get \"{}\" -> {:?} w:{} p:{}", + key, + value.as_ref().map(|x| unsafe { NgxStr::from_ngx_str(*x) }), + unsafe { nginx_sys::ngx_worker }, + unsafe { nginx_sys::ngx_pid }, + ); + + let Some(value) = value else { + v.set_not_found(1); + return NGX_ERROR as _; + }; + + v.data = value.data; + v.set_len(value.len as _); + + v.set_valid(1); + v.set_no_cacheable(0); + v.set_not_found(0); + + NGX_OK as _ +} + +extern "C" fn ngx_http_shared_dict_set_variable( + r: *mut ngx_http_request_t, + v: *mut ngx_http_variable_value_t, + data: usize, +) { + let r = unsafe { &mut *r }; + let v = unsafe { &mut *v }; + let smcf = HttpSharedDictModule::main_conf_mut(r).expect("shared dict main config"); + let mut key = ngx_str_t::empty(); + + if unsafe { ngx_http_complex_value(r, data as _, &mut key) } != NGX_OK as _ { + return; + } + + let alloc = unsafe { SlabPool::from_shm_zone(&*smcf.shm_zone).expect("slab pool") }; + + let Ok(key) = NgxString::try_from_bytes_in(key.as_bytes(), alloc.clone()) else { + return; + }; + + let value = unsafe { slice::from_raw_parts(v.data, v.len() as usize) }; + let Ok(value) = NgxString::try_from_bytes_in(value, alloc.clone()) else { + return; + }; + + let Ok(shared) = ngx_http_shared_dict_get_shared(unsafe { &mut *smcf.shm_zone }) else { + return; + }; + + ngx_log_debug!( + unsafe { (*r.connection).log }, + "shared dict: set \"{}\" -> \"{}\" w:{} p:{}", + key, + value, + unsafe { nginx_sys::ngx_worker }, + unsafe { nginx_sys::ngx_pid }, + ); + + let _ = shared.write().try_insert(key, value); +} diff --git a/examples/t/shared_dict.t b/examples/t/shared_dict.t new file mode 100644 index 00000000..d16b0981 --- /dev/null +++ b/examples/t/shared_dict.t @@ -0,0 +1,93 @@ +#!/usr/bin/perl + +# (C) Nginx, Inc + +# Tests for ngx-rust example modules. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/http/)->plan(6) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +worker_processes 2; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + shared_dict_zone z 64k; + shared_dict $arg_key $foo; + + server { + listen 127.0.0.1:8080; + server_name localhost; + + add_header X-Value $foo; + add_header X-Process $pid; + + location /set/ { + set $foo $arg_value; + return 200; + } + } +} + +EOF + +$t->write_file('index.html', ''); +$t->run(); + +############################################################################### + +like(http_get('/set/?key=fst&value=hello'), qr/200 OK/, 'set value 1'); +like(http_get('/set/?key=snd&value=world'), qr/200 OK/, 'set value 2'); + +ok(check('/?key=fst', qr/X-Value: hello/i), 'check value 1'); +ok(check('/?key=snd', qr/X-Value: world/i), 'check value 2'); + +like(http_get('/set/?key=fst&value=new_value'), qr/200 OK/, 'update value 1'); +ok(check('/?key=fst', qr/X-Value: new_value/i), 'check updated value'); + +############################################################################### + +sub check { + my ($uri, $like) = @_; + + my $r = http_get($uri); + + return unless ($r =~ $like && $r =~ /X-Process: (\d+)/); + + return 1 if $^O eq 'MSWin32'; # only one active worker process + + my $pid = $1; + + for (1 .. 25) { + $r = http_get($uri); + + return unless ($r =~ $like && $r =~ /X-Process: (\d+)/); + return 1 if $pid != $1; + } +} + +###############################################################################