Skip to content

Commit 2d0e587

Browse files
authored
feat(embed): add embed features, add test example which run php inside it (#270)
* feat(embed): add embed features, add test example which run php inside it * feat(embed): use a guard to prevent running in parallel * chore(ci): update actions to not build and test with embed, add a specific build for embed testing * feat(embed): correcly start / shutdown embed api * chore(ci): use stable for rust in embed test * feat(embed): add documentation, manage potential errors
1 parent 15bed3b commit 2d0e587

File tree

15 files changed

+354
-4
lines changed

15 files changed

+354
-4
lines changed

.github/actions/embed/Dockerfile

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
FROM php:8.2-bullseye
2+
3+
WORKDIR /tmp
4+
5+
RUN apt update -y && apt upgrade -y
6+
RUN apt install lsb-release wget gnupg software-properties-common -y
7+
RUN bash -c "$(wget -O - https://apt.llvm.org/llvm.sh)"
8+
9+
ENV RUSTUP_HOME=/rust
10+
ENV CARGO_HOME=/cargo
11+
ENV PATH=/cargo/bin:/rust/bin:$PATH
12+
13+
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path
14+
15+
ENTRYPOINT [ "/cargo/bin/cargo", "test", "--lib", "--release", "--all-features" ]

.github/actions/embed/action.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
name: 'PHP Embed and Rust'
2+
description: 'Builds the crate after installing the latest PHP with php embed and stable Rust.'
3+
runs:
4+
using: 'docker'
5+
image: 'Dockerfile'

.github/workflows/build.yml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,10 @@ jobs:
8383
- name: Build
8484
env:
8585
EXT_PHP_RS_TEST: ""
86-
run: cargo build --release --all-features --all
86+
run: cargo build --release --features closure,anyhow --all
8787
# Test & lint
8888
- name: Test inline examples
89-
run: cargo test --release --all --all-features
89+
run: cargo test --release --all --features closure,anyhow
9090
- name: Run rustfmt
9191
if: matrix.rust == 'stable' && matrix.os == 'ubuntu-latest' && matrix.php == '8.2'
9292
run: cargo fmt --all -- --check
@@ -110,3 +110,11 @@ jobs:
110110
uses: actions/checkout@v3
111111
- name: Build
112112
uses: ./.github/actions/zts
113+
test-embed:
114+
name: Test with embed
115+
runs-on: ubuntu-latest
116+
steps:
117+
- name: Checkout code
118+
uses: actions/checkout@v3
119+
- name: Test
120+
uses: ./.github/actions/embed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ zip = "0.6"
3535

3636
[features]
3737
closure = []
38+
embed = []
3839

3940
[workspace]
4041
members = [

allowed_bindings.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,5 +256,9 @@ bind! {
256256
tsrm_get_ls_cache,
257257
executor_globals_offset,
258258
zend_atomic_bool_store,
259-
zend_interrupt_function
259+
zend_interrupt_function,
260+
zend_eval_string,
261+
zend_file_handle,
262+
zend_stream_init_filename,
263+
php_execute_script
260264
}

build.rs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,9 +151,31 @@ fn build_wrapper(defines: &[(&str, &str)], includes: &[PathBuf]) -> Result<()> {
151151
Ok(())
152152
}
153153

154+
#[cfg(feature = "embed")]
155+
/// Builds the embed library.
156+
fn build_embed(defines: &[(&str, &str)], includes: &[PathBuf]) -> Result<()> {
157+
let mut build = cc::Build::new();
158+
for (var, val) in defines {
159+
build.define(var, *val);
160+
}
161+
build
162+
.file("src/embed/embed.c")
163+
.includes(includes)
164+
.try_compile("embed")
165+
.context("Failed to compile ext-php-rs C embed interface")?;
166+
Ok(())
167+
}
168+
154169
/// Generates bindings to the Zend API.
155170
fn generate_bindings(defines: &[(&str, &str)], includes: &[PathBuf]) -> Result<String> {
156-
let mut bindgen = bindgen::Builder::default()
171+
let mut bindgen = bindgen::Builder::default();
172+
173+
#[cfg(feature = "embed")]
174+
{
175+
bindgen = bindgen.header("src/embed/embed.h");
176+
}
177+
178+
bindgen = bindgen
157179
.header("src/wrapper.h")
158180
.clang_args(
159181
includes
@@ -257,6 +279,10 @@ fn main() -> Result<()> {
257279

258280
check_php_version(&info)?;
259281
build_wrapper(&defines, &includes)?;
282+
283+
#[cfg(feature = "embed")]
284+
build_embed(&defines, &includes)?;
285+
260286
let bindings = generate_bindings(&defines, &includes)?;
261287

262288
let out_file =

src/embed/embed.c

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#include "embed.h"
2+
3+
// We actually use the PHP embed API to run PHP code in test
4+
// At some point we might want to use our own SAPI to do that
5+
void ext_php_rs_embed_callback(int argc, char** argv, void (*callback)(void *), void *ctx) {
6+
PHP_EMBED_START_BLOCK(argc, argv)
7+
8+
callback(ctx);
9+
10+
PHP_EMBED_END_BLOCK()
11+
}

src/embed/embed.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#include "zend.h"
2+
#include "sapi/embed/php_embed.h"
3+
4+
void ext_php_rs_embed_callback(int argc, char** argv, void (*callback)(void *), void *ctx);

src/embed/ffi.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
//! Raw FFI bindings to the Zend API.
2+
3+
#![allow(clippy::all)]
4+
#![allow(warnings)]
5+
6+
use std::ffi::{c_char, c_int, c_void};
7+
8+
#[link(name = "wrapper")]
9+
extern "C" {
10+
pub fn ext_php_rs_embed_callback(
11+
argc: c_int,
12+
argv: *mut *mut c_char,
13+
func: unsafe extern "C" fn(*const c_void),
14+
ctx: *const c_void,
15+
);
16+
}

src/embed/mod.rs

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
//! Provides implementations for running php code from rust.
2+
//! It only works on linux for now and you should have `php-embed` installed
3+
//!
4+
//! This crate was only test with PHP 8.2 please report any issue with other version
5+
//! You should only use this crate for test purpose, it's not production ready
6+
7+
mod ffi;
8+
9+
use crate::boxed::ZBox;
10+
use crate::embed::ffi::ext_php_rs_embed_callback;
11+
use crate::ffi::{
12+
_zend_file_handle__bindgen_ty_1, php_execute_script, zend_eval_string, zend_file_handle,
13+
zend_stream_init_filename, ZEND_RESULT_CODE_SUCCESS,
14+
};
15+
use crate::types::{ZendObject, Zval};
16+
use crate::zend::ExecutorGlobals;
17+
use parking_lot::{const_rwlock, RwLock};
18+
use std::ffi::{c_char, c_void, CString, NulError};
19+
use std::path::Path;
20+
use std::ptr::null_mut;
21+
22+
pub struct Embed;
23+
24+
#[derive(Debug)]
25+
pub enum EmbedError {
26+
InitError,
27+
ExecuteError(Option<ZBox<ZendObject>>),
28+
ExecuteScriptError,
29+
InvalidEvalString(NulError),
30+
InvalidPath,
31+
}
32+
33+
static RUN_FN_LOCK: RwLock<()> = const_rwlock(());
34+
35+
impl Embed {
36+
/// Run a php script from a file
37+
///
38+
/// This function will only work correctly when used inside the `Embed::run` function
39+
/// otherwise behavior is unexpected
40+
///
41+
/// # Returns
42+
///
43+
/// * `Ok(())` - The script was executed successfully
44+
/// * `Err(EmbedError)` - An error occured during the execution of the script
45+
///
46+
/// # Example
47+
///
48+
/// ```
49+
/// use ext_php_rs::embed::Embed;
50+
///
51+
/// Embed::run(|| {
52+
/// let result = Embed::run_script("src/embed/test-script.php");
53+
///
54+
/// assert!(result.is_ok());
55+
/// });
56+
/// ```
57+
pub fn run_script<P: AsRef<Path>>(path: P) -> Result<(), EmbedError> {
58+
let path = match path.as_ref().to_str() {
59+
Some(path) => match CString::new(path) {
60+
Ok(path) => path,
61+
Err(err) => return Err(EmbedError::InvalidEvalString(err)),
62+
},
63+
None => return Err(EmbedError::InvalidPath),
64+
};
65+
66+
let mut file_handle = zend_file_handle {
67+
handle: _zend_file_handle__bindgen_ty_1 { fp: null_mut() },
68+
filename: null_mut(),
69+
opened_path: null_mut(),
70+
type_: 0,
71+
primary_script: false,
72+
in_list: false,
73+
buf: null_mut(),
74+
len: 0,
75+
};
76+
77+
unsafe {
78+
zend_stream_init_filename(&mut file_handle, path.as_ptr());
79+
}
80+
81+
if unsafe { php_execute_script(&mut file_handle) } {
82+
Ok(())
83+
} else {
84+
Err(EmbedError::ExecuteScriptError)
85+
}
86+
}
87+
88+
/// Start and run embed sapi engine
89+
///
90+
/// This function will allow to run php code from rust, the same PHP context is keep between calls
91+
/// inside the function passed to this method.
92+
/// Which means subsequent calls to `Embed::eval` or `Embed::run_script` will be able to access
93+
/// variables defined in previous calls
94+
///
95+
/// # Example
96+
///
97+
/// ```
98+
/// use ext_php_rs::embed::Embed;
99+
///
100+
/// Embed::run(|| {
101+
/// let _ = Embed::eval("$foo = 'foo';");
102+
/// let foo = Embed::eval("$foo;");
103+
/// assert!(foo.is_ok());
104+
/// assert_eq!(foo.unwrap().string().unwrap(), "foo");
105+
/// });
106+
/// ```
107+
pub fn run<F: Fn()>(func: F) {
108+
// @TODO handle php thread safe
109+
//
110+
// This is to prevent multiple threads from running php at the same time
111+
// At some point we should detect if php is compiled with thread safety and avoid doing that in this case
112+
let _guard = RUN_FN_LOCK.write();
113+
114+
unsafe extern "C" fn wrapper<F: Fn()>(ctx: *const c_void) {
115+
(*(ctx as *const F))();
116+
}
117+
118+
unsafe {
119+
ext_php_rs_embed_callback(
120+
0,
121+
null_mut(),
122+
wrapper::<F>,
123+
&func as *const F as *const c_void,
124+
);
125+
}
126+
}
127+
128+
/// Evaluate a php code
129+
///
130+
/// This function will only work correctly when used inside the `Embed::run` function
131+
///
132+
/// # Returns
133+
///
134+
/// * `Ok(Zval)` - The result of the evaluation
135+
/// * `Err(EmbedError)` - An error occured during the evaluation
136+
///
137+
/// # Example
138+
///
139+
/// ```
140+
/// use ext_php_rs::embed::Embed;
141+
///
142+
/// Embed::run(|| {
143+
/// let foo = Embed::eval("$foo = 'foo';");
144+
/// assert!(foo.is_ok());
145+
/// });
146+
/// ```
147+
pub fn eval(code: &str) -> Result<Zval, EmbedError> {
148+
let cstr = match CString::new(code) {
149+
Ok(cstr) => cstr,
150+
Err(err) => return Err(EmbedError::InvalidEvalString(err)),
151+
};
152+
153+
let mut result = Zval::new();
154+
155+
// this eval is very limited as it only allow simple code, it's the same eval used by php -r
156+
let exec_result = unsafe {
157+
zend_eval_string(
158+
cstr.as_ptr() as *const c_char,
159+
&mut result,
160+
b"run\0".as_ptr() as *const _,
161+
)
162+
};
163+
164+
let exception = ExecutorGlobals::take_exception();
165+
166+
if exec_result != ZEND_RESULT_CODE_SUCCESS {
167+
Err(EmbedError::ExecuteError(exception))
168+
} else {
169+
Ok(result)
170+
}
171+
}
172+
}
173+
174+
#[cfg(test)]
175+
mod tests {
176+
use super::Embed;
177+
178+
#[test]
179+
fn test_run() {
180+
Embed::run(|| {
181+
let result = Embed::eval("$foo = 'foo';");
182+
183+
assert!(result.is_ok());
184+
});
185+
}
186+
187+
#[test]
188+
fn test_run_error() {
189+
Embed::run(|| {
190+
let result = Embed::eval("stupid code;");
191+
192+
assert!(!result.is_ok());
193+
});
194+
}
195+
196+
#[test]
197+
fn test_run_script() {
198+
Embed::run(|| {
199+
let result = Embed::run_script("src/embed/test-script.php");
200+
201+
assert!(result.is_ok());
202+
203+
let zval = Embed::eval("$foo;").unwrap();
204+
205+
assert!(zval.is_object());
206+
207+
let obj = zval.object().unwrap();
208+
209+
assert_eq!(obj.get_class_name().unwrap(), "Test");
210+
});
211+
}
212+
213+
#[test]
214+
fn test_run_script_error() {
215+
Embed::run(|| {
216+
let result = Embed::run_script("src/embed/test-script-exception.php");
217+
218+
assert!(!result.is_ok());
219+
});
220+
}
221+
}

0 commit comments

Comments
 (0)