mirror of
https://github.com/danog/ext-php-rs.git
synced 2024-11-26 20:15:22 +01:00
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
This commit is contained in:
parent
15bed3b0b5
commit
2d0e587c7e
15
.github/actions/embed/Dockerfile
vendored
Normal file
15
.github/actions/embed/Dockerfile
vendored
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
FROM php:8.2-bullseye
|
||||||
|
|
||||||
|
WORKDIR /tmp
|
||||||
|
|
||||||
|
RUN apt update -y && apt upgrade -y
|
||||||
|
RUN apt install lsb-release wget gnupg software-properties-common -y
|
||||||
|
RUN bash -c "$(wget -O - https://apt.llvm.org/llvm.sh)"
|
||||||
|
|
||||||
|
ENV RUSTUP_HOME=/rust
|
||||||
|
ENV CARGO_HOME=/cargo
|
||||||
|
ENV PATH=/cargo/bin:/rust/bin:$PATH
|
||||||
|
|
||||||
|
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path
|
||||||
|
|
||||||
|
ENTRYPOINT [ "/cargo/bin/cargo", "test", "--lib", "--release", "--all-features" ]
|
5
.github/actions/embed/action.yml
vendored
Normal file
5
.github/actions/embed/action.yml
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
name: 'PHP Embed and Rust'
|
||||||
|
description: 'Builds the crate after installing the latest PHP with php embed and stable Rust.'
|
||||||
|
runs:
|
||||||
|
using: 'docker'
|
||||||
|
image: 'Dockerfile'
|
12
.github/workflows/build.yml
vendored
12
.github/workflows/build.yml
vendored
@ -83,10 +83,10 @@ jobs:
|
|||||||
- name: Build
|
- name: Build
|
||||||
env:
|
env:
|
||||||
EXT_PHP_RS_TEST: ""
|
EXT_PHP_RS_TEST: ""
|
||||||
run: cargo build --release --all-features --all
|
run: cargo build --release --features closure,anyhow --all
|
||||||
# Test & lint
|
# Test & lint
|
||||||
- name: Test inline examples
|
- name: Test inline examples
|
||||||
run: cargo test --release --all --all-features
|
run: cargo test --release --all --features closure,anyhow
|
||||||
- name: Run rustfmt
|
- name: Run rustfmt
|
||||||
if: matrix.rust == 'stable' && matrix.os == 'ubuntu-latest' && matrix.php == '8.2'
|
if: matrix.rust == 'stable' && matrix.os == 'ubuntu-latest' && matrix.php == '8.2'
|
||||||
run: cargo fmt --all -- --check
|
run: cargo fmt --all -- --check
|
||||||
@ -110,3 +110,11 @@ jobs:
|
|||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
- name: Build
|
- name: Build
|
||||||
uses: ./.github/actions/zts
|
uses: ./.github/actions/zts
|
||||||
|
test-embed:
|
||||||
|
name: Test with embed
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Test
|
||||||
|
uses: ./.github/actions/embed
|
||||||
|
@ -35,6 +35,7 @@ zip = "0.6"
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
closure = []
|
closure = []
|
||||||
|
embed = []
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
|
@ -256,5 +256,9 @@ bind! {
|
|||||||
tsrm_get_ls_cache,
|
tsrm_get_ls_cache,
|
||||||
executor_globals_offset,
|
executor_globals_offset,
|
||||||
zend_atomic_bool_store,
|
zend_atomic_bool_store,
|
||||||
zend_interrupt_function
|
zend_interrupt_function,
|
||||||
|
zend_eval_string,
|
||||||
|
zend_file_handle,
|
||||||
|
zend_stream_init_filename,
|
||||||
|
php_execute_script
|
||||||
}
|
}
|
||||||
|
28
build.rs
28
build.rs
@ -151,9 +151,31 @@ fn build_wrapper(defines: &[(&str, &str)], includes: &[PathBuf]) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "embed")]
|
||||||
|
/// Builds the embed library.
|
||||||
|
fn build_embed(defines: &[(&str, &str)], includes: &[PathBuf]) -> Result<()> {
|
||||||
|
let mut build = cc::Build::new();
|
||||||
|
for (var, val) in defines {
|
||||||
|
build.define(var, *val);
|
||||||
|
}
|
||||||
|
build
|
||||||
|
.file("src/embed/embed.c")
|
||||||
|
.includes(includes)
|
||||||
|
.try_compile("embed")
|
||||||
|
.context("Failed to compile ext-php-rs C embed interface")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Generates bindings to the Zend API.
|
/// Generates bindings to the Zend API.
|
||||||
fn generate_bindings(defines: &[(&str, &str)], includes: &[PathBuf]) -> Result<String> {
|
fn generate_bindings(defines: &[(&str, &str)], includes: &[PathBuf]) -> Result<String> {
|
||||||
let mut bindgen = bindgen::Builder::default()
|
let mut bindgen = bindgen::Builder::default();
|
||||||
|
|
||||||
|
#[cfg(feature = "embed")]
|
||||||
|
{
|
||||||
|
bindgen = bindgen.header("src/embed/embed.h");
|
||||||
|
}
|
||||||
|
|
||||||
|
bindgen = bindgen
|
||||||
.header("src/wrapper.h")
|
.header("src/wrapper.h")
|
||||||
.clang_args(
|
.clang_args(
|
||||||
includes
|
includes
|
||||||
@ -257,6 +279,10 @@ fn main() -> Result<()> {
|
|||||||
|
|
||||||
check_php_version(&info)?;
|
check_php_version(&info)?;
|
||||||
build_wrapper(&defines, &includes)?;
|
build_wrapper(&defines, &includes)?;
|
||||||
|
|
||||||
|
#[cfg(feature = "embed")]
|
||||||
|
build_embed(&defines, &includes)?;
|
||||||
|
|
||||||
let bindings = generate_bindings(&defines, &includes)?;
|
let bindings = generate_bindings(&defines, &includes)?;
|
||||||
|
|
||||||
let out_file =
|
let out_file =
|
||||||
|
11
src/embed/embed.c
Normal file
11
src/embed/embed.c
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
#include "embed.h"
|
||||||
|
|
||||||
|
// We actually use the PHP embed API to run PHP code in test
|
||||||
|
// At some point we might want to use our own SAPI to do that
|
||||||
|
void ext_php_rs_embed_callback(int argc, char** argv, void (*callback)(void *), void *ctx) {
|
||||||
|
PHP_EMBED_START_BLOCK(argc, argv)
|
||||||
|
|
||||||
|
callback(ctx);
|
||||||
|
|
||||||
|
PHP_EMBED_END_BLOCK()
|
||||||
|
}
|
4
src/embed/embed.h
Normal file
4
src/embed/embed.h
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
#include "zend.h"
|
||||||
|
#include "sapi/embed/php_embed.h"
|
||||||
|
|
||||||
|
void ext_php_rs_embed_callback(int argc, char** argv, void (*callback)(void *), void *ctx);
|
16
src/embed/ffi.rs
Normal file
16
src/embed/ffi.rs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
//! Raw FFI bindings to the Zend API.
|
||||||
|
|
||||||
|
#![allow(clippy::all)]
|
||||||
|
#![allow(warnings)]
|
||||||
|
|
||||||
|
use std::ffi::{c_char, c_int, c_void};
|
||||||
|
|
||||||
|
#[link(name = "wrapper")]
|
||||||
|
extern "C" {
|
||||||
|
pub fn ext_php_rs_embed_callback(
|
||||||
|
argc: c_int,
|
||||||
|
argv: *mut *mut c_char,
|
||||||
|
func: unsafe extern "C" fn(*const c_void),
|
||||||
|
ctx: *const c_void,
|
||||||
|
);
|
||||||
|
}
|
221
src/embed/mod.rs
Normal file
221
src/embed/mod.rs
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
//! Provides implementations for running php code from rust.
|
||||||
|
//! It only works on linux for now and you should have `php-embed` installed
|
||||||
|
//!
|
||||||
|
//! This crate was only test with PHP 8.2 please report any issue with other version
|
||||||
|
//! You should only use this crate for test purpose, it's not production ready
|
||||||
|
|
||||||
|
mod ffi;
|
||||||
|
|
||||||
|
use crate::boxed::ZBox;
|
||||||
|
use crate::embed::ffi::ext_php_rs_embed_callback;
|
||||||
|
use crate::ffi::{
|
||||||
|
_zend_file_handle__bindgen_ty_1, php_execute_script, zend_eval_string, zend_file_handle,
|
||||||
|
zend_stream_init_filename, ZEND_RESULT_CODE_SUCCESS,
|
||||||
|
};
|
||||||
|
use crate::types::{ZendObject, Zval};
|
||||||
|
use crate::zend::ExecutorGlobals;
|
||||||
|
use parking_lot::{const_rwlock, RwLock};
|
||||||
|
use std::ffi::{c_char, c_void, CString, NulError};
|
||||||
|
use std::path::Path;
|
||||||
|
use std::ptr::null_mut;
|
||||||
|
|
||||||
|
pub struct Embed;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum EmbedError {
|
||||||
|
InitError,
|
||||||
|
ExecuteError(Option<ZBox<ZendObject>>),
|
||||||
|
ExecuteScriptError,
|
||||||
|
InvalidEvalString(NulError),
|
||||||
|
InvalidPath,
|
||||||
|
}
|
||||||
|
|
||||||
|
static RUN_FN_LOCK: RwLock<()> = const_rwlock(());
|
||||||
|
|
||||||
|
impl Embed {
|
||||||
|
/// Run a php script from a file
|
||||||
|
///
|
||||||
|
/// This function will only work correctly when used inside the `Embed::run` function
|
||||||
|
/// otherwise behavior is unexpected
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// * `Ok(())` - The script was executed successfully
|
||||||
|
/// * `Err(EmbedError)` - An error occured during the execution of the script
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use ext_php_rs::embed::Embed;
|
||||||
|
///
|
||||||
|
/// Embed::run(|| {
|
||||||
|
/// let result = Embed::run_script("src/embed/test-script.php");
|
||||||
|
///
|
||||||
|
/// assert!(result.is_ok());
|
||||||
|
/// });
|
||||||
|
/// ```
|
||||||
|
pub fn run_script<P: AsRef<Path>>(path: P) -> Result<(), EmbedError> {
|
||||||
|
let path = match path.as_ref().to_str() {
|
||||||
|
Some(path) => match CString::new(path) {
|
||||||
|
Ok(path) => path,
|
||||||
|
Err(err) => return Err(EmbedError::InvalidEvalString(err)),
|
||||||
|
},
|
||||||
|
None => return Err(EmbedError::InvalidPath),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut file_handle = zend_file_handle {
|
||||||
|
handle: _zend_file_handle__bindgen_ty_1 { fp: null_mut() },
|
||||||
|
filename: null_mut(),
|
||||||
|
opened_path: null_mut(),
|
||||||
|
type_: 0,
|
||||||
|
primary_script: false,
|
||||||
|
in_list: false,
|
||||||
|
buf: null_mut(),
|
||||||
|
len: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
zend_stream_init_filename(&mut file_handle, path.as_ptr());
|
||||||
|
}
|
||||||
|
|
||||||
|
if unsafe { php_execute_script(&mut file_handle) } {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(EmbedError::ExecuteScriptError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start and run embed sapi engine
|
||||||
|
///
|
||||||
|
/// This function will allow to run php code from rust, the same PHP context is keep between calls
|
||||||
|
/// inside the function passed to this method.
|
||||||
|
/// Which means subsequent calls to `Embed::eval` or `Embed::run_script` will be able to access
|
||||||
|
/// variables defined in previous calls
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use ext_php_rs::embed::Embed;
|
||||||
|
///
|
||||||
|
/// Embed::run(|| {
|
||||||
|
/// let _ = Embed::eval("$foo = 'foo';");
|
||||||
|
/// let foo = Embed::eval("$foo;");
|
||||||
|
/// assert!(foo.is_ok());
|
||||||
|
/// assert_eq!(foo.unwrap().string().unwrap(), "foo");
|
||||||
|
/// });
|
||||||
|
/// ```
|
||||||
|
pub fn run<F: Fn()>(func: F) {
|
||||||
|
// @TODO handle php thread safe
|
||||||
|
//
|
||||||
|
// This is to prevent multiple threads from running php at the same time
|
||||||
|
// At some point we should detect if php is compiled with thread safety and avoid doing that in this case
|
||||||
|
let _guard = RUN_FN_LOCK.write();
|
||||||
|
|
||||||
|
unsafe extern "C" fn wrapper<F: Fn()>(ctx: *const c_void) {
|
||||||
|
(*(ctx as *const F))();
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
ext_php_rs_embed_callback(
|
||||||
|
0,
|
||||||
|
null_mut(),
|
||||||
|
wrapper::<F>,
|
||||||
|
&func as *const F as *const c_void,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluate a php code
|
||||||
|
///
|
||||||
|
/// This function will only work correctly when used inside the `Embed::run` function
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// * `Ok(Zval)` - The result of the evaluation
|
||||||
|
/// * `Err(EmbedError)` - An error occured during the evaluation
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use ext_php_rs::embed::Embed;
|
||||||
|
///
|
||||||
|
/// Embed::run(|| {
|
||||||
|
/// let foo = Embed::eval("$foo = 'foo';");
|
||||||
|
/// assert!(foo.is_ok());
|
||||||
|
/// });
|
||||||
|
/// ```
|
||||||
|
pub fn eval(code: &str) -> Result<Zval, EmbedError> {
|
||||||
|
let cstr = match CString::new(code) {
|
||||||
|
Ok(cstr) => cstr,
|
||||||
|
Err(err) => return Err(EmbedError::InvalidEvalString(err)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut result = Zval::new();
|
||||||
|
|
||||||
|
// this eval is very limited as it only allow simple code, it's the same eval used by php -r
|
||||||
|
let exec_result = unsafe {
|
||||||
|
zend_eval_string(
|
||||||
|
cstr.as_ptr() as *const c_char,
|
||||||
|
&mut result,
|
||||||
|
b"run\0".as_ptr() as *const _,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let exception = ExecutorGlobals::take_exception();
|
||||||
|
|
||||||
|
if exec_result != ZEND_RESULT_CODE_SUCCESS {
|
||||||
|
Err(EmbedError::ExecuteError(exception))
|
||||||
|
} else {
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::Embed;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_run() {
|
||||||
|
Embed::run(|| {
|
||||||
|
let result = Embed::eval("$foo = 'foo';");
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_run_error() {
|
||||||
|
Embed::run(|| {
|
||||||
|
let result = Embed::eval("stupid code;");
|
||||||
|
|
||||||
|
assert!(!result.is_ok());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_run_script() {
|
||||||
|
Embed::run(|| {
|
||||||
|
let result = Embed::run_script("src/embed/test-script.php");
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
let zval = Embed::eval("$foo;").unwrap();
|
||||||
|
|
||||||
|
assert!(zval.is_object());
|
||||||
|
|
||||||
|
let obj = zval.object().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(obj.get_class_name().unwrap(), "Test");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_run_script_error() {
|
||||||
|
Embed::run(|| {
|
||||||
|
let result = Embed::run_script("src/embed/test-script-exception.php");
|
||||||
|
|
||||||
|
assert!(!result.is_ok());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
3
src/embed/test-script-exception.php
Normal file
3
src/embed/test-script-exception.php
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
throw new \RuntimeException('This is a test exception');
|
7
src/embed/test-script.php
Normal file
7
src/embed/test-script.php
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class Test {
|
||||||
|
public function __construct() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
$foo = new Test();
|
@ -25,6 +25,8 @@ pub mod class;
|
|||||||
pub mod closure;
|
pub mod closure;
|
||||||
pub mod constant;
|
pub mod constant;
|
||||||
pub mod describe;
|
pub mod describe;
|
||||||
|
#[cfg(feature = "embed")]
|
||||||
|
pub mod embed;
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub mod internal;
|
pub mod internal;
|
||||||
pub mod props;
|
pub mod props;
|
||||||
|
@ -453,3 +453,23 @@ impl<'a> FromZval<'a> for &'a str {
|
|||||||
zval.str()
|
zval.str()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[cfg(feature = "embed")]
|
||||||
|
mod tests {
|
||||||
|
use crate::embed::Embed;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_string() {
|
||||||
|
Embed::run(|| {
|
||||||
|
let result = Embed::eval("'foo';");
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
let zval = result.as_ref().unwrap();
|
||||||
|
|
||||||
|
assert!(zval.is_string());
|
||||||
|
assert_eq!(zval.string().unwrap(), "foo");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -55,4 +55,11 @@ impl<'a> PHPProvider<'a> for Provider {
|
|||||||
fn get_defines(&self) -> Result<Vec<(&'static str, &'static str)>> {
|
fn get_defines(&self) -> Result<Vec<(&'static str, &'static str)>> {
|
||||||
Ok(vec![])
|
Ok(vec![])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn print_extra_link_args(&self) -> Result<()> {
|
||||||
|
#[cfg(feature = "embed")]
|
||||||
|
println!("cargo:rustc-link-lib=php");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user