99extern crate ext_php_rs;
1010
1111use 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} ;
1313use 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} ;
1717use ext_php_rs:: prelude:: * ;
1818use ext_php_rs:: zend:: try_catch_first;
1919use 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
2129static 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+
2336extern "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]
3750fn 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 {
102121pub 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