diff --git a/Cargo.toml b/Cargo.toml index fd1021f8..d1d07c70 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,11 +36,14 @@ all-features = true default-target = "x86_64-unknown-linux-gnu" targets = [] +[workspace.dependencies] +nginx-sys = { path = "nginx-sys", version = "0.5.0-beta" } + [dependencies] allocator-api2 = { version = "0.2.21", default-features = false } async-task = { version = "4.7.1", optional = true } lock_api = "0.4.13" -nginx-sys = { path = "nginx-sys", version = "0.5.0-beta"} +nginx-sys = { workspace = true } pin-project-lite = { version = "0.2.16", optional = true } [features] @@ -69,4 +72,5 @@ vendored = ["nginx-sys/vendored"] maintenance = { status = "experimental" } [dev-dependencies] +nginx-sys = { workspace = true, features = ["unittest"] } tempfile = { version = "3.20.0", default-features = false } diff --git a/nginx-src/Cargo.toml b/nginx-src/Cargo.toml index 4ac31046..57c1efcd 100644 --- a/nginx-src/Cargo.toml +++ b/nginx-src/Cargo.toml @@ -12,6 +12,10 @@ homepage.workspace = true repository.workspace = true rust-version.workspace = true +[features] +# Builds nginx library from vendored sources for unit tests. +unittest = [] + [dependencies] duct = "1" flate2 = "1" diff --git a/nginx-src/libnginx/config b/nginx-src/libnginx/config new file mode 100644 index 00000000..ffde6d7f --- /dev/null +++ b/nginx-src/libnginx/config @@ -0,0 +1,5 @@ + +# Copyright (C) Nginx, Inc. + + +ngx_addon_name="libnginx" diff --git a/nginx-src/libnginx/config.make b/nginx-src/libnginx/config.make new file mode 100644 index 00000000..dfdfac96 --- /dev/null +++ b/nginx-src/libnginx/config.make @@ -0,0 +1,73 @@ + +# Copyright (C) Nginx, Inc. + + +ngx_addon_name=libnginx +ngx_module=$ngx_addon_name +ngx_module_c=$ngx_addon_dir/libnginx.c + +ngx_ar="\$(AR)" +ngx_libext=.a +ngx_libout="r " + +case "$NGX_CC_NAME" in + + msvc) + ngx_ar=lib + ngx_libext=.lib + ngx_libout="/OUT:" + ;; + +esac + +if test -n "$NGX_PCH"; then + ngx_cc="\$(CC) $ngx_compile_opt \$(CFLAGS) $ngx_use_pch \$(ALL_INCS)" +else + ngx_cc="\$(CC) $ngx_compile_opt \$(CFLAGS) \$(CORE_INCS)" +fi + +ngx_module_objs= +for ngx_src in $ngx_module_c +do + ngx_obj="addon/`basename \`dirname $ngx_src\``" + + test -d $NGX_OBJS/$ngx_obj || mkdir -p $NGX_OBJS/$ngx_obj + + ngx_obj=`echo $ngx_obj/\`basename $ngx_src\` \ + | sed -e "s/\//$ngx_regex_dirsep/g" \ + -e "s#^\(.*\.\)c\\$#$ngx_objs_dir\1$ngx_objext#g"` + + ngx_module_objs="$ngx_module_objs $ngx_obj" + + cat << END >> $NGX_MAKEFILE + +$ngx_obj: \$(CORE_DEPS)$ngx_cont$ngx_src + $ngx_cc$ngx_tab$ngx_objout$ngx_obj$ngx_tab$ngx_src$NGX_AUX + +END + +done + +ngx_objs=`echo $ngx_module_objs $ngx_modules_obj $ngx_all_objs \ + | sed -e "s/[^ ]*\\/nginx\\.$ngx_objext//g" \ + -e "s/ *\([^ ][^ ]*\)/$ngx_long_regex_cont\1/g" \ + -e "s/\//$ngx_regex_dirsep/g"` + +ngx_deps=`echo $ngx_module_objs $ngx_modules_obj $ngx_all_objs \ + | sed -e "s/[^ ]*\\/nginx\\.$ngx_objext//g" \ + -e "s/ *\([^ ][^ ]*\)/$ngx_regex_cont\1/g" \ + -e "s/\//$ngx_regex_dirsep/g"` + +ngx_obj=$NGX_OBJS$ngx_dirsep$ngx_module$ngx_libext + +cat << END >> $NGX_MAKEFILE + +modules: $ngx_obj + +$ngx_obj: $ngx_deps$ngx_spacer + $ngx_ar $ngx_long_start$ngx_libout$ngx_obj$ngx_long_cont$ngx_objs +$ngx_long_end + +LIBNGINX_LDFLAGS = $NGX_LD_OPT $CORE_LIBS + +END diff --git a/nginx-src/libnginx/libnginx.c b/nginx-src/libnginx/libnginx.c new file mode 100644 index 00000000..da122305 --- /dev/null +++ b/nginx-src/libnginx/libnginx.c @@ -0,0 +1,187 @@ + +/* + * Copyright (C) Nginx, Inc. + */ + + +#include +#include +#include + +#include "libnginx.h" + + +/* + * We need to build nginx.c to correctly initialize ngx_core_module, + * but exclude an existing definition of main. + */ +#define main main_unused +#include "nginx.c" +#undef main + + +static ngx_int_t libngx_write_temp_conf_file(ngx_cycle_t *cycle, + ngx_str_t *data, ngx_str_t *name); + + +ngx_cycle_t * +libngx_init(u_char *prefix) +{ + static ngx_cycle_t *cycle = NGX_CONF_UNSET_PTR, init_cycle; + + ngx_log_t *log; + char *const argv[] = { "nginx" }; + + if (cycle != NGX_CONF_UNSET_PTR) { + return cycle; + } + + cycle = NULL; + + ngx_conf_params = (u_char *) "daemon off; master_process off;"; + ngx_error_log = (u_char *) ""; + ngx_prefix = prefix; + + ngx_debug_init(); + + if (ngx_strerror_init() != NGX_OK) { + return cycle; + } + + ngx_max_sockets = -1; + + ngx_time_init(); + +#if (NGX_PCRE) + ngx_regex_init(); +#endif + + ngx_pid = ngx_getpid(); + ngx_parent = ngx_getppid(); + + log = ngx_log_init(ngx_prefix, ngx_error_log); + if (log == NULL) { + return NULL; + } + + log->log_level = NGX_LOG_INFO; + +#if (NGX_OPENSSL) + ngx_ssl_init(log); +#endif + + ngx_memzero(&init_cycle, sizeof(ngx_cycle_t)); + init_cycle.log = log; + init_cycle.log_use_stderr = 1; + ngx_cycle = &init_cycle; + + init_cycle.pool = ngx_create_pool(1024, log); + if (init_cycle.pool == NULL) { + return NULL; + } + + if (ngx_save_argv(&init_cycle, sizeof(argv)/sizeof(argv[0]), argv) != NGX_OK) { + return NULL; + } + + if (ngx_process_options(&init_cycle) != NGX_OK) { + return NULL; + } + + if (ngx_os_init(log) != NGX_OK) { + return NULL; + } + + if (ngx_crc32_table_init() != NGX_OK) { + return NULL; + } + + ngx_slab_sizes_init(); + + if (ngx_preinit_modules() != NGX_OK) { + return NULL; + } + + cycle = &init_cycle; + return cycle; +} + + +ngx_int_t +libngx_create_cycle(ngx_cycle_t *cycle, ngx_str_t *conf) +{ + ngx_str_t conf_file; + + ngx_cycle = cycle; + + if (libngx_write_temp_conf_file(cycle, conf, &conf_file) != NGX_OK) { + return NGX_ERROR; + } + + ngx_conf_file = conf_file.data; + + if (ngx_process_options(cycle) != NGX_OK) { + return NGX_ERROR; + } + + cycle = ngx_init_cycle(cycle); + if (cycle == NULL) { + return NGX_ERROR; + } + + ngx_cycle = cycle; + + return NGX_OK; +} + + +static ngx_int_t +libngx_write_temp_conf_file(ngx_cycle_t *cycle, ngx_str_t *data, + ngx_str_t *name) +{ + ngx_int_t rc; + ngx_path_t *path; + ngx_temp_file_t tf; + + path = ngx_pcalloc(cycle->pool, sizeof(ngx_path_t)); + if (path == NULL) { + return NGX_ERROR; + } + + ngx_memzero(&tf, sizeof(ngx_temp_file_t)); + + tf.file.fd = NGX_INVALID_FILE; + tf.file.log = cycle->log; + tf.access = NGX_FILE_OWNER_ACCESS; + tf.clean = 1; + tf.path = path; + tf.pool = cycle->pool; + tf.persistent = 1; + + ngx_str_set(&path->name, "conf"); + + rc = ngx_conf_full_name(cycle, &path->name, 0); + if (rc != NGX_OK) { + return rc; + } + + if (ngx_create_dir(path->name.data, ngx_dir_access(tf.access)) + == NGX_FILE_ERROR) + { + return ngx_errno; + } + + rc = ngx_create_temp_file(&tf.file, tf.path, tf.pool, tf.persistent, + tf.clean, tf.access); + if (rc != NGX_OK) { + return rc; + } + + if (ngx_write_file(&tf.file, data->data, data->len, 0) == NGX_ERROR) { + return NGX_ERROR; + } + + *name = tf.file.name; + + return NGX_OK; +} diff --git a/nginx-src/libnginx/libnginx.h b/nginx-src/libnginx/libnginx.h new file mode 100644 index 00000000..c2b52fb5 --- /dev/null +++ b/nginx-src/libnginx/libnginx.h @@ -0,0 +1,14 @@ + +/* + * Copyright (C) Nginx, Inc + */ + + +#ifndef _LIBNGINX_H_INCLUDED_ +#define _LIBNGINX_H_INCLUDED_ + +ngx_cycle_t *libngx_init(u_char *prefix); +ngx_int_t libngx_create_cycle(ngx_cycle_t *cycle, ngx_str_t *conf); + + +#endif /* _LIBNGINX_H_INCLUDED_ */ diff --git a/nginx-src/src/lib.rs b/nginx-src/src/lib.rs index a606196a..fa52f39c 100644 --- a/nginx-src/src/lib.rs +++ b/nginx-src/src/lib.rs @@ -113,6 +113,12 @@ fn nginx_configure_flags(vendored: &[String]) -> Vec { nginx_opts.push(format!("--with-ld-opt={ldflags}")); } + #[cfg(feature = "unittest")] + nginx_opts.push(format!( + "--add-dynamic-module={}/libnginx", + env!("CARGO_MANIFEST_DIR") + )); + nginx_opts } diff --git a/nginx-sys/Cargo.toml b/nginx-sys/Cargo.toml index 4c2ce3ed..94f1a558 100644 --- a/nginx-sys/Cargo.toml +++ b/nginx-sys/Cargo.toml @@ -34,3 +34,4 @@ shlex = "1.3" [features] vendored = ["dep:nginx-src"] +unittest = ["vendored", "nginx-src/unittest"] diff --git a/nginx-sys/build/main.rs b/nginx-sys/build/main.rs index 8f85bd0c..f806cceb 100644 --- a/nginx-sys/build/main.rs +++ b/nginx-sys/build/main.rs @@ -192,7 +192,11 @@ impl NginxSource { /// Generates Rust bindings for NGINX fn generate_binding(nginx: &NginxSource) { let autoconf_makefile_path = nginx.build_dir.join("Makefile"); - let (includes, defines) = parse_makefile(&autoconf_makefile_path); + let ParsedMakefile { + includes, + defines, + libs, + } = parse_makefile(&autoconf_makefile_path); let includes: Vec<_> = includes .into_iter() .map(|path| { @@ -216,7 +220,22 @@ fn generate_binding(nginx: &NginxSource) { } })); - print_cargo_metadata(nginx, &includes, &defines).expect("cargo dependency metadata"); + let libs: Vec<_> = libs + .into_iter() + .map(|lib_opt| { + if let LibOption::LibPath(path) = lib_opt { + if Path::new(&path).is_absolute() { + LibOption::LibPath(path) + } else { + LibOption::LibPath(nginx.source_dir.join(path).to_string_lossy().to_string()) + } + } else { + lib_opt + } + }) + .collect(); + + print_cargo_metadata(nginx, &includes, &defines, &libs).expect("cargo dependency metadata"); // bindgen targets the latest known stable by default let rust_target: bindgen::RustTarget = env::var("CARGO_PKG_RUST_VERSION") @@ -249,12 +268,28 @@ fn generate_binding(nginx: &NginxSource) { .expect("Couldn't write bindings!"); } +/// Represents a library flags for NGINX: +/// * `LibPath`: A library directory path +/// * `Lib`: A library name +pub enum LibOption { + LibPath(String), + Lib(String), +} + +/// Parsed representation of the NGINX Makefile: +/// * `includes`: List of include paths +/// * `defines`: List of preprocessor definitions +/// * `libs`: List of libraries and/or library directories +pub struct ParsedMakefile { + pub includes: Vec, + pub defines: Vec<(String, Option)>, + pub libs: Vec, +} + /// Reads through the makefile generated by autoconf and finds all of the includes /// and definitions used to compile nginx. This is used to generate the correct bindings /// for the nginx source code. -pub fn parse_makefile( - nginx_autoconf_makefile_path: &PathBuf, -) -> (Vec, Vec<(String, Option)>) { +pub fn parse_makefile(nginx_autoconf_makefile_path: &PathBuf) -> ParsedMakefile { fn parse_line( includes: &mut Vec, defines: &mut Vec<(String, Option)>, @@ -286,11 +321,35 @@ pub fn parse_makefile( } } + fn parse_lib_line(libs: &mut Vec, line: &str) { + let mut words = shlex::Shlex::new(line); + + while let Some(word) = words.next() { + if let Some(lib_dir) = word.strip_prefix("-L") { + let value = if lib_dir.is_empty() { + words.next().expect("-L argument") + } else { + lib_dir.to_string() + }; + libs.push(LibOption::LibPath(value)); + } else if let Some(lib) = word.strip_prefix("-l") { + let value = if lib.is_empty() { + words.next().expect("-l argument") + } else { + lib.to_string() + }; + libs.push(LibOption::Lib(value)); + } + } + } + let mut all_incs = vec![]; let mut cflags_includes = vec![]; let mut defines = vec![]; + let mut libs = vec![]; + let makefile_contents = match read_to_string(nginx_autoconf_makefile_path) { Ok(path) => path, Err(e) => { @@ -316,6 +375,8 @@ pub fn parse_makefile( parse_line(&mut all_incs, &mut defines, tail); } else if let Some(tail) = line.strip_prefix("CFLAGS") { parse_line(&mut cflags_includes, &mut defines, tail); + } else if let Some(tail) = line.strip_prefix("LIBNGINX_LDFLAGS") { + parse_lib_line(&mut libs, tail); } line.clear(); @@ -323,10 +384,11 @@ pub fn parse_makefile( cflags_includes.extend(all_incs); - ( - cflags_includes.into_iter().map(PathBuf::from).collect(), + ParsedMakefile { + includes: cflags_includes.into_iter().map(PathBuf::from).collect(), defines, - ) + libs, + } } /// Collect info about the nginx configuration and expose it to the dependents via @@ -335,6 +397,7 @@ pub fn print_cargo_metadata>( nginx: &NginxSource, includes: &[T], defines: &[(String, Option)], + _libs: &[LibOption], ) -> Result<(), Box> { // Unquote and merge C string constants let unquote_re = regex::Regex::new(r#""(.*?[^\\])"\s*"#).unwrap(); @@ -420,6 +483,23 @@ pub fn print_cargo_metadata>( println!("cargo::metadata=os={ngx_os}"); println!("cargo::rustc-cfg=ngx_os=\"{ngx_os}\""); + #[cfg(feature = "unittest")] + { + // Linker flags to use libnginx in tests + println!( + "cargo::rustc-link-search={}", + nginx.build_dir.to_str().expect("Unicode build path") + ); + + for lib in _libs { + if let LibOption::LibPath(ref path) = lib { + println!("cargo::rustc-link-search={path}"); + } else if let LibOption::Lib(ref name) = lib { + println!("cargo::rustc-link-lib={name}"); + } + } + } + Ok(()) } diff --git a/src/core/pool.rs b/src/core/pool.rs index 59a41bd0..f7a6bc50 100644 --- a/src/core/pool.rs +++ b/src/core/pool.rs @@ -326,3 +326,105 @@ impl Pool { unsafe extern "C" fn cleanup_type(data: *mut c_void) { ptr::drop_in_place(data as *mut T); } + +#[cfg(all(test, feature = "vendored"))] +mod tests { + + use nginx_sys::{ngx_create_pool, ngx_destroy_pool}; + + use super::*; + + #[link(name = "nginx", kind = "static")] + extern "C" { + pub fn libngx_init(prefix: *mut nginx_sys::u_char) -> *mut nginx_sys::ngx_cycle_t; + } + + #[test] + fn test_pool_resize() { + unsafe { + libngx_init("prefix".as_ptr() as *mut nginx_sys::u_char); + }; + + let mut log: nginx_sys::ngx_log_t = unsafe { core::mem::zeroed() }; + let p: *mut nginx_sys::ngx_pool_t = unsafe { ngx_create_pool(1024, &mut log) }; + let pool = unsafe { Pool::from_ngx_pool(p) }; + + let layout = Layout::from_size_align(16, 8).unwrap(); + let slice_ptr = Allocator::allocate(&pool, layout).unwrap(); + let ptr = slice_ptr.as_ptr().cast::(); + + let new_layout = Layout::from_size_align(32, 8).unwrap(); + let nonnull_ptr = unsafe { NonNull::new_unchecked(ptr) }; + let newptr = unsafe { pool.resize(nonnull_ptr, layout, new_layout) }; + + assert!(newptr.is_ok()); + assert!(core::ptr::addr_eq(newptr.unwrap().as_ptr(), ptr)); + + unsafe { ngx_destroy_pool(p) }; + } + + #[test] + fn test_vec() { + unsafe { + libngx_init("prefix".as_ptr() as *mut nginx_sys::u_char); + }; + + let mut log: nginx_sys::ngx_log_t = unsafe { core::mem::zeroed() }; + let p: *mut nginx_sys::ngx_pool_t = unsafe { ngx_create_pool(1024, &mut log) }; + let pool = unsafe { Pool::from_ngx_pool(p) }; + + let mut v1: allocator_api2::vec::Vec = allocator_api2::vec::Vec::new_in(pool); + + v1.reserve(4); + assert!(v1.capacity() >= 4); + let v1_ptr1 = v1.as_ptr(); + + v1.reserve(4); + assert!(v1.capacity() >= 8); + let v1_ptr2 = v1.as_ptr(); + + assert!(v1_ptr1 == v1_ptr2); + + v1.resize(4, 1); + + v1.shrink_to_fit(); + let v1_ptr3 = v1.as_ptr(); + + assert!(v1_ptr1 == v1_ptr3); + + unsafe { ngx_destroy_pool(p) }; + } + + #[test] + fn test_two_vecs() { + unsafe { + libngx_init("prefix".as_ptr() as *mut nginx_sys::u_char); + }; + + let mut log: nginx_sys::ngx_log_t = unsafe { core::mem::zeroed() }; + let p: *mut nginx_sys::ngx_pool_t = unsafe { ngx_create_pool(2048, &mut log) }; + let pool = unsafe { Pool::from_ngx_pool(p) }; + + let mut v1: allocator_api2::vec::Vec = + allocator_api2::vec::Vec::new_in(pool.clone()); + + v1.reserve(128); + assert!(v1.capacity() >= 128); + let v1_ptr1 = v1.as_ptr(); + + v1.resize(128, 1); + + let mut v2: allocator_api2::vec::Vec = allocator_api2::vec::Vec::new_in(pool); + + v2.reserve(128); + assert!(v2.capacity() >= 128); + + v1.reserve(128); + assert!(v1.capacity() >= 256, "actual capacity: {}", v1.capacity()); + let v1_ptr2 = v1.as_ptr(); + + assert!(v1_ptr1 != v1_ptr2); + + unsafe { ngx_destroy_pool(p) }; + } +}