Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions crates/macros/src/module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,16 @@ pub fn parser(input: ItemFn) -> Result<TokenStream> {
let b = __EXT_PHP_RS_MODULE_STARTUP
.lock()
.take()
.inspect(|_| ::ext_php_rs::internal::ext_php_rs_startup())
.expect("Module startup function has already been called.")
.startup(ty, mod_num)
.map(|_| 0)
.unwrap_or(1);
.map(|startup| {
::ext_php_rs::internal::ext_php_rs_startup();
startup.startup(ty, mod_num).map(|_| 0).unwrap_or(1)
})
.unwrap_or_else(|| {
// Module already started, call ext_php_rs_startup for idempotent
// initialization (e.g., Closure::build early-returns if already built)
::ext_php_rs::internal::ext_php_rs_startup();
0
});
a | b
}

Expand Down
9 changes: 7 additions & 2 deletions src/closure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,11 +115,16 @@ impl Closure {
/// function should only be called once inside your module startup
/// function.
///
/// If the class has already been built, this function returns early without
/// doing anything. This allows for safe repeated calls in test environments.
///
/// # Panics
///
/// Panics if the function is called more than once.
/// Panics if the `RustClosure` PHP class cannot be registered.
pub fn build() {
assert!(!CLOSURE_META.has_ce(), "Closure class already built.");
if CLOSURE_META.has_ce() {
return;
}

ClassBuilder::new("RustClosure")
.method(
Expand Down
105 changes: 104 additions & 1 deletion tests/sapi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,26 @@
extern crate ext_php_rs;

use ext_php_rs::builders::SapiBuilder;
use ext_php_rs::embed::{Embed, ext_php_rs_sapi_startup};
use ext_php_rs::embed::{
Embed, ext_php_rs_sapi_per_thread_init, ext_php_rs_sapi_shutdown, ext_php_rs_sapi_startup,
};
use ext_php_rs::ffi::{
ZEND_RESULT_CODE_SUCCESS, php_module_shutdown, php_module_startup, php_request_shutdown,
php_request_startup, sapi_shutdown, sapi_startup,
};
use ext_php_rs::prelude::*;
use ext_php_rs::zend::try_catch_first;
use std::ffi::c_char;
use std::sync::{Arc, Mutex};
use std::thread;

static mut LAST_OUTPUT: String = String::new();

// Global mutex to ensure SAPI tests don't run concurrently. PHP does not allow
// multiple SAPIs to exist at the same time. This prevents the tests from
// overwriting each other's state.
static SAPI_TEST_MUTEX: Mutex<()> = Mutex::new(());

extern "C" fn output_tester(str: *const c_char, str_length: usize) -> usize {
let char = unsafe { std::slice::from_raw_parts(str.cast::<u8>(), str_length) };
let string = String::from_utf8_lossy(char);
Expand All @@ -35,6 +44,8 @@ extern "C" fn output_tester(str: *const c_char, str_length: usize) -> usize {

#[test]
fn test_sapi() {
let _guard = SAPI_TEST_MUTEX.lock().unwrap();

let mut builder = SapiBuilder::new("test", "Test");
builder = builder.ub_write_function(output_tester);

Expand Down Expand Up @@ -86,6 +97,10 @@ fn test_sapi() {
unsafe {
sapi_shutdown();
}

unsafe {
ext_php_rs_sapi_shutdown();
}
}

/// Gives you a nice greeting!
Expand All @@ -102,3 +117,91 @@ pub fn hello_world(name: String) -> String {
pub fn module(module: ModuleBuilder) -> ModuleBuilder {
module.function(wrap_function!(hello_world))
}

#[test]
fn test_sapi_multithread() {
let _guard = SAPI_TEST_MUTEX.lock().unwrap();

let mut builder = SapiBuilder::new("test-mt", "Test Multi-threaded");
builder = builder.ub_write_function(output_tester);

let sapi = builder.build().unwrap().into_raw();
let module = get_module();

unsafe {
ext_php_rs_sapi_startup();
}

unsafe {
sapi_startup(sapi);
}

unsafe {
php_module_startup(sapi, module);
}

let results = Arc::new(Mutex::new(Vec::new()));
let mut handles = vec![];

for i in 0..4 {
let results = Arc::clone(&results);

let handle = thread::spawn(move || {
unsafe {
ext_php_rs_sapi_per_thread_init();
}

let result = unsafe { php_request_startup() };
assert_eq!(result, ZEND_RESULT_CODE_SUCCESS);

let _ = try_catch_first(|| {
let eval_result = Embed::eval(&format!("hello_world('thread-{i}');"));

match eval_result {
Ok(zval) => {
assert!(zval.is_string());
let string = zval.string().unwrap();
let output = string.to_string();
assert_eq!(output, format!("Hello, thread-{i}!"));

results.lock().unwrap().push((i, output));
}
Err(e) => panic!("Evaluation failed in thread {i}: {e:?}"),
}
});

unsafe {
php_request_shutdown(std::ptr::null_mut());
}
});

handles.push(handle);
}

for handle in handles {
handle.join().expect("Thread panicked");
}

let results = results.lock().unwrap();
assert_eq!(results.len(), 4);

for i in 0..4 {
assert!(
results
.iter()
.any(|(idx, output)| { *idx == i && output == &format!("Hello, thread-{i}!") })
);
}

unsafe {
php_module_shutdown();
}

unsafe {
sapi_shutdown();
}

unsafe {
ext_php_rs_sapi_shutdown();
}
}
Loading