Skip to content

Commit e2e9ce9

Browse files
Qardptondereau
andauthored
Add sapi zts test (#496)
Co-authored-by: Pierre Tondereau <[email protected]>
1 parent 34035ed commit e2e9ce9

File tree

8 files changed

+200
-41
lines changed

8 files changed

+200
-41
lines changed

.github/workflows/build.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,8 +169,11 @@ jobs:
169169
if: "!(contains(matrix.os, 'macos') && matrix.rust == 'nightly')"
170170
run: cargo test --release --workspace --features closure,anyhow,runtime --no-fail-fast
171171
test-embed:
172-
name: Test with embed
172+
name: Test with embed (${{ matrix.phpts }})
173173
runs-on: ubuntu-latest
174+
strategy:
175+
matrix:
176+
phpts: [ts, nts]
174177
env:
175178
clang: "17"
176179
php_version: "8.5"
@@ -183,6 +186,7 @@ jobs:
183186
with:
184187
php-version: ${{ env.php_version }}
185188
env:
189+
phpts: ${{ matrix.phpts }}
186190
debug: true
187191

188192
- name: Setup Rust

crates/macros/src/module.rs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,16 @@ pub fn parser(input: ItemFn) -> Result<TokenStream> {
3535
let b = __EXT_PHP_RS_MODULE_STARTUP
3636
.lock()
3737
.take()
38-
.inspect(|_| ::ext_php_rs::internal::ext_php_rs_startup())
39-
.expect("Module startup function has already been called.")
40-
.startup(ty, mod_num)
41-
.map(|_| 0)
42-
.unwrap_or(1);
38+
.map(|startup| {
39+
::ext_php_rs::internal::ext_php_rs_startup();
40+
startup.startup(ty, mod_num).map(|_| 0).unwrap_or(1)
41+
})
42+
.unwrap_or_else(|| {
43+
// Module already started, call ext_php_rs_startup for idempotent
44+
// initialization (e.g., Closure::build early-returns if already built)
45+
::ext_php_rs::internal::ext_php_rs_startup();
46+
0
47+
});
4348
a | b
4449
}
4550

src/builders/module.rs

Lines changed: 57 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -375,38 +375,63 @@ impl TryFrom<ModuleBuilder<'_>> for (ModuleEntry, ModuleStartup) {
375375
enums: builder.enums,
376376
};
377377

378-
Ok((
379-
ModuleEntry {
380-
size: mem::size_of::<ModuleEntry>().try_into()?,
381-
zend_api: ZEND_MODULE_API_NO,
382-
zend_debug: u8::from(PHP_DEBUG),
383-
zts: u8::from(PHP_ZTS),
384-
ini_entry: ptr::null(),
385-
deps: ptr::null(),
386-
name,
387-
functions,
388-
module_startup_func: builder.startup_func,
389-
module_shutdown_func: builder.shutdown_func,
390-
request_startup_func: builder.request_startup_func,
391-
request_shutdown_func: builder.request_shutdown_func,
392-
info_func: builder.info_func,
393-
version,
394-
globals_size: 0,
395-
#[cfg(not(php_zts))]
396-
globals_ptr: ptr::null_mut(),
397-
#[cfg(php_zts)]
398-
globals_id_ptr: ptr::null_mut(),
399-
globals_ctor: None,
400-
globals_dtor: None,
401-
post_deactivate_func: builder.post_deactivate_func,
402-
module_started: 0,
403-
type_: 0,
404-
handle: ptr::null_mut(),
405-
module_number: 0,
406-
build_id: unsafe { ext_php_rs_php_build_id() },
407-
},
408-
startup,
409-
))
378+
#[cfg(not(php_zts))]
379+
let module_entry = ModuleEntry {
380+
size: mem::size_of::<ModuleEntry>().try_into()?,
381+
zend_api: ZEND_MODULE_API_NO,
382+
zend_debug: u8::from(PHP_DEBUG),
383+
zts: u8::from(PHP_ZTS),
384+
ini_entry: ptr::null(),
385+
deps: ptr::null(),
386+
name,
387+
functions,
388+
module_startup_func: builder.startup_func,
389+
module_shutdown_func: builder.shutdown_func,
390+
request_startup_func: builder.request_startup_func,
391+
request_shutdown_func: builder.request_shutdown_func,
392+
info_func: builder.info_func,
393+
version,
394+
globals_size: 0,
395+
globals_ptr: ptr::null_mut(),
396+
globals_ctor: None,
397+
globals_dtor: None,
398+
post_deactivate_func: builder.post_deactivate_func,
399+
module_started: 0,
400+
type_: 0,
401+
handle: ptr::null_mut(),
402+
module_number: 0,
403+
build_id: unsafe { ext_php_rs_php_build_id() },
404+
};
405+
406+
#[cfg(php_zts)]
407+
let module_entry = ModuleEntry {
408+
size: mem::size_of::<ModuleEntry>().try_into()?,
409+
zend_api: ZEND_MODULE_API_NO,
410+
zend_debug: u8::from(PHP_DEBUG),
411+
zts: u8::from(PHP_ZTS),
412+
ini_entry: ptr::null(),
413+
deps: ptr::null(),
414+
name,
415+
functions,
416+
module_startup_func: builder.startup_func,
417+
module_shutdown_func: builder.shutdown_func,
418+
request_startup_func: builder.request_startup_func,
419+
request_shutdown_func: builder.request_shutdown_func,
420+
info_func: builder.info_func,
421+
version,
422+
globals_size: 0,
423+
globals_id_ptr: ptr::null_mut(),
424+
globals_ctor: None,
425+
globals_dtor: None,
426+
post_deactivate_func: builder.post_deactivate_func,
427+
module_started: 0,
428+
type_: 0,
429+
handle: ptr::null_mut(),
430+
module_number: 0,
431+
build_id: unsafe { ext_php_rs_php_build_id() },
432+
};
433+
434+
Ok((module_entry, startup))
410435
}
411436
}
412437

src/closure.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,11 +115,16 @@ impl Closure {
115115
/// function should only be called once inside your module startup
116116
/// function.
117117
///
118+
/// If the class has already been built, this function returns early without
119+
/// doing anything. This allows for safe repeated calls in test environments.
120+
///
118121
/// # Panics
119122
///
120-
/// Panics if the function is called more than once.
123+
/// Panics if the `RustClosure` PHP class cannot be registered.
121124
pub fn build() {
122-
assert!(!CLOSURE_META.has_ce(), "Closure class already built.");
125+
if CLOSURE_META.has_ce() {
126+
return;
127+
}
123128

124129
ClassBuilder::new("RustClosure")
125130
.method(

src/embed/embed.c

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ SAPI_API void ext_php_rs_sapi_per_thread_init() {
4444
#endif
4545
}
4646

47+
SAPI_API void ext_php_rs_sapi_per_thread_shutdown() {
48+
#ifdef ZTS
49+
ts_free_thread();
50+
#endif
51+
}
52+
4753
void ext_php_rs_php_error(int type, const char *format, ...) {
4854
va_list args;
4955
va_start(args, format);

src/embed/embed.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@ void* ext_php_rs_embed_callback(int argc, char** argv, void* (*callback)(void *)
99
void ext_php_rs_sapi_startup();
1010
void ext_php_rs_sapi_shutdown();
1111
void ext_php_rs_sapi_per_thread_init();
12+
void ext_php_rs_sapi_per_thread_shutdown();
1213

1314
void ext_php_rs_php_error(int type, const char *format, ...);

src/embed/ffi.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ unsafe extern "C" {
2020
pub fn ext_php_rs_sapi_startup();
2121
pub fn ext_php_rs_sapi_shutdown();
2222
pub fn ext_php_rs_sapi_per_thread_init();
23+
pub fn ext_php_rs_sapi_per_thread_shutdown();
2324

2425
pub fn ext_php_rs_php_error(
2526
type_: ::std::os::raw::c_int,

tests/sapi.rs

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,30 @@
99
extern crate ext_php_rs;
1010

1111
use ext_php_rs::builders::SapiBuilder;
12-
use ext_php_rs::embed::{Embed, ext_php_rs_sapi_startup};
12+
use ext_php_rs::embed::{Embed, ext_php_rs_sapi_shutdown, ext_php_rs_sapi_startup};
1313
use ext_php_rs::ffi::{
1414
ZEND_RESULT_CODE_SUCCESS, php_module_shutdown, php_module_startup, php_request_shutdown,
1515
php_request_startup, sapi_shutdown, sapi_startup,
1616
};
1717
use ext_php_rs::prelude::*;
1818
use ext_php_rs::zend::try_catch_first;
1919
use std::ffi::c_char;
20+
use std::sync::Mutex;
21+
22+
#[cfg(php_zts)]
23+
use ext_php_rs::embed::{ext_php_rs_sapi_per_thread_init, ext_php_rs_sapi_per_thread_shutdown};
24+
#[cfg(php_zts)]
25+
use std::sync::Arc;
26+
#[cfg(php_zts)]
27+
use std::thread;
2028

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

31+
// Global mutex to ensure SAPI tests don't run concurrently. PHP does not allow
32+
// multiple SAPIs to exist at the same time. This prevents the tests from
33+
// overwriting each other's state.
34+
static SAPI_TEST_MUTEX: Mutex<()> = Mutex::new(());
35+
2336
extern "C" fn output_tester(str: *const c_char, str_length: usize) -> usize {
2437
let char = unsafe { std::slice::from_raw_parts(str.cast::<u8>(), str_length) };
2538
let string = String::from_utf8_lossy(char);
@@ -35,6 +48,8 @@ extern "C" fn output_tester(str: *const c_char, str_length: usize) -> usize {
3548

3649
#[test]
3750
fn test_sapi() {
51+
let _guard = SAPI_TEST_MUTEX.lock().unwrap();
52+
3853
let mut builder = SapiBuilder::new("test", "Test");
3954
builder = builder.ub_write_function(output_tester);
4055

@@ -86,6 +101,10 @@ fn test_sapi() {
86101
unsafe {
87102
sapi_shutdown();
88103
}
104+
105+
unsafe {
106+
ext_php_rs_sapi_shutdown();
107+
}
89108
}
90109

91110
/// Gives you a nice greeting!
@@ -102,3 +121,96 @@ pub fn hello_world(name: String) -> String {
102121
pub fn module(module: ModuleBuilder) -> ModuleBuilder {
103122
module.function(wrap_function!(hello_world))
104123
}
124+
125+
#[test]
126+
#[cfg(php_zts)]
127+
fn test_sapi_multithread() {
128+
let _guard = SAPI_TEST_MUTEX.lock().unwrap();
129+
130+
let mut builder = SapiBuilder::new("test-mt", "Test Multi-threaded");
131+
builder = builder.ub_write_function(output_tester);
132+
133+
let sapi = builder.build().unwrap().into_raw();
134+
let module = get_module();
135+
136+
unsafe {
137+
ext_php_rs_sapi_startup();
138+
}
139+
140+
unsafe {
141+
sapi_startup(sapi);
142+
}
143+
144+
unsafe {
145+
php_module_startup(sapi, module);
146+
}
147+
148+
let results = Arc::new(Mutex::new(Vec::new()));
149+
let mut handles = vec![];
150+
151+
for i in 0..4 {
152+
let results = Arc::clone(&results);
153+
154+
let handle = thread::spawn(move || {
155+
unsafe {
156+
ext_php_rs_sapi_per_thread_init();
157+
}
158+
159+
let result = unsafe { php_request_startup() };
160+
assert_eq!(result, ZEND_RESULT_CODE_SUCCESS);
161+
162+
let _ = try_catch_first(|| {
163+
let eval_result = Embed::eval(&format!("hello_world('thread-{i}');"));
164+
165+
match eval_result {
166+
Ok(zval) => {
167+
assert!(zval.is_string());
168+
let string = zval.string().unwrap();
169+
let output = string.to_string();
170+
assert_eq!(output, format!("Hello, thread-{i}!"));
171+
172+
results.lock().unwrap().push((i, output));
173+
}
174+
Err(e) => panic!("Evaluation failed in thread {i}: {e:?}"),
175+
}
176+
});
177+
178+
unsafe {
179+
php_request_shutdown(std::ptr::null_mut());
180+
}
181+
182+
unsafe {
183+
ext_php_rs_sapi_per_thread_shutdown();
184+
}
185+
});
186+
187+
handles.push(handle);
188+
}
189+
190+
for handle in handles {
191+
handle.join().expect("Thread panicked");
192+
}
193+
194+
let results = results.lock().unwrap();
195+
assert_eq!(results.len(), 4);
196+
197+
for i in 0..4 {
198+
assert!(
199+
results
200+
.iter()
201+
.any(|(idx, output)| { *idx == i && output == &format!("Hello, thread-{i}!") })
202+
);
203+
}
204+
205+
unsafe {
206+
php_module_shutdown();
207+
}
208+
209+
unsafe {
210+
sapi_shutdown();
211+
}
212+
213+
unsafe {
214+
ext_php_rs_sapi_shutdown();
215+
}
216+
}

0 commit comments

Comments
 (0)