Merge remote-tracking branch 'origin/master' into process-globals

This commit is contained in:
Daniil Gentili 2023-11-24 14:07:30 +01:00
commit 21134fb618
52 changed files with 2023 additions and 357 deletions

15
.github/actions/embed/Dockerfile vendored Normal file
View 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
View 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'

View File

@ -15,9 +15,9 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
php: ["8.0", "8.1", "8.2"]
php: ["8.0", "8.1", "8.2", "8.3"]
rust: [stable, nightly]
clang: ["14"]
clang: ["15", "17"]
phpts: [ts, nts]
exclude:
# ext-php-rs requires nightly Rust when on Windows.
@ -28,6 +28,12 @@ jobs:
phpts: ts
- os: ubuntu-latest
phpts: ts
- os: macos-latest
clang: "17"
- os: ubuntu-latest
clang: "15"
- os: windows-latest
clang: "15"
env:
CARGO_TERM_COLOR: always
steps:
@ -83,10 +89,10 @@ jobs:
- name: Build
env:
EXT_PHP_RS_TEST: ""
run: cargo build --release --all-features --all
run: cargo build --release --features closure,anyhow --all
# Test & lint
- name: Test inline examples
run: cargo test --release --all --all-features
run: cargo test --release --all --features closure,anyhow
- name: Run rustfmt
if: matrix.rust == 'stable' && matrix.os == 'ubuntu-latest' && matrix.php == '8.2'
run: cargo fmt --all -- --check
@ -110,3 +116,11 @@ jobs:
uses: actions/checkout@v3
- name: Build
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

View File

@ -16,7 +16,7 @@ jobs:
matrix:
os: ["ubuntu-latest"]
php: ["8.2"]
clang: ["14"]
clang: ["17"]
mdbook: ["latest"]
steps:
- name: Checkout code

View File

@ -5,7 +5,7 @@ repository = "https://github.com/davidcole1340/ext-php-rs"
homepage = "https://github.com/davidcole1340/ext-php-rs"
license = "MIT OR Apache-2.0"
keywords = ["php", "ffi", "zend"]
version = "0.10.1"
version = "0.10.5"
authors = ["David Cole <david.cole1340@gmail.com>"]
edition = "2018"
categories = ["api-bindings"]
@ -17,30 +17,31 @@ parking_lot = "0.12"
cfg-if = "1.0"
once_cell = "1.17"
anyhow = { version = "1", optional = true }
ext-php-rs-derive = { version = "=0.10.0", path = "./crates/macros" }
ext-php-rs-derive = { version = "=0.10.1", path = "./crates/macros" }
[dev-dependencies]
skeptic = "0.13"
[build-dependencies]
anyhow = "1"
bindgen = "0.65.1"
bindgen = "0.68.1"
cc = "1.0"
skeptic = "0.13"
[target.'cfg(windows)'.build-dependencies]
ureq = { version = "2.4", features = ["native-tls", "gzip"], default-features = false }
ureq = { version = "2.4", features = [
"native-tls",
"gzip",
], default-features = false }
native-tls = "0.2"
zip = "0.6"
[features]
closure = []
embed = []
[workspace]
members = [
"crates/macros",
"crates/cli"
]
members = ["crates/macros", "crates/cli"]
[package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docs"]

View File

@ -26,6 +26,8 @@ bind! {
_efree,
_emalloc,
_zend_executor_globals,
_sapi_globals_struct,
_sapi_module_struct,
_zend_expected_type,
_zend_expected_type_Z_EXPECTED_ARRAY,
_zend_expected_type_Z_EXPECTED_BOOL,
@ -55,6 +57,8 @@ bind! {
zend_array_destroy,
zend_array_dup,
zend_call_known_function,
zend_fetch_function_str,
zend_hash_str_find_ptr_lc,
zend_ce_argument_count_error,
zend_ce_arithmetic_error,
zend_ce_compile_error,
@ -99,6 +103,8 @@ bind! {
zend_objects_clone_members,
zend_register_bool_constant,
zend_register_double_constant,
zend_register_ini_entries,
zend_ini_entry_def,
zend_register_internal_class_ex,
zend_register_long_constant,
zend_register_string_constant,
@ -106,6 +112,7 @@ bind! {
zend_string,
zend_string_init_interned,
zend_throw_exception_ex,
zend_throw_exception_object,
zend_type,
zend_value,
zend_wrong_parameters_count_error,
@ -137,6 +144,7 @@ bind! {
IS_CONSTANT_AST_EX,
IS_DOUBLE,
IS_FALSE,
IS_INDIRECT,
IS_INTERNED_STRING_EX,
IS_LONG,
IS_MIXED,
@ -157,6 +165,10 @@ bind! {
IS_PTR,
MAY_BE_ANY,
MAY_BE_BOOL,
PHP_INI_USER,
PHP_INI_PERDIR,
PHP_INI_SYSTEM,
PHP_INI_ALL,
USING_ZTS,
ZEND_ACC_ABSTRACT,
ZEND_ACC_ANON_CLASS,
@ -233,6 +245,8 @@ bind! {
zend_class_serialize_deny,
zend_class_unserialize_deny,
zend_executor_globals,
sapi_globals_struct,
sapi_module_struct,
zend_objects_store_del,
zend_hash_move_forward_ex,
zend_hash_get_current_key_type_ex,
@ -247,6 +261,8 @@ bind! {
core_globals,
sapi_globals_struct,
sapi_globals,
sapi_globals,
sapi_module,
php_printf,
__zend_malloc,
tsrm_get_ls_cache,
@ -268,6 +284,13 @@ bind! {
zend_is_auto_global,
zend_llist_get_next_ex,
zend_llist_get_prev_ex,
sapi_globals_offset,
zend_atomic_bool_store,
zend_interrupt_function
zend_interrupt_function,
zend_eval_string,
zend_file_handle,
zend_stream_init_filename,
php_execute_script,
zend_register_module_ex,
_zend_bailout
}

View File

@ -16,7 +16,7 @@ use bindgen::RustTarget;
use impl_::Provider;
const MIN_PHP_API_VER: u32 = 20200930;
const MAX_PHP_API_VER: u32 = 20220829;
const MAX_PHP_API_VER: u32 = 20230831;
pub trait PHPProvider<'a>: Sized {
/// Create a new PHP provider.
@ -151,9 +151,31 @@ fn build_wrapper(defines: &[(&str, &str)], includes: &[PathBuf]) -> Result<()> {
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.
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")
.clang_args(
includes
@ -206,6 +228,8 @@ fn check_php_version(info: &PHPInfo) -> Result<()> {
const PHP_82_API_VER: u32 = 20220829;
const PHP_83_API_VER: u32 = 20230831;
println!("cargo:rustc-cfg=php80");
if (PHP_81_API_VER..PHP_82_API_VER).contains(&version) {
@ -216,6 +240,10 @@ fn check_php_version(info: &PHPInfo) -> Result<()> {
println!("cargo:rustc-cfg=php82");
}
if version >= PHP_83_API_VER {
println!("cargo:rustc-cfg=php83");
}
Ok(())
}
@ -226,6 +254,8 @@ fn main() -> Result<()> {
for path in [
manifest.join("src").join("wrapper.h"),
manifest.join("src").join("wrapper.c"),
manifest.join("src").join("embed").join("embed.h"),
manifest.join("src").join("embed").join("embed.c"),
manifest.join("allowed_bindings.rs"),
manifest.join("windows_build.rs"),
manifest.join("unix_build.rs"),
@ -257,6 +287,10 @@ fn main() -> Result<()> {
check_php_version(&info)?;
build_wrapper(&defines, &includes)?;
#[cfg(feature = "embed")]
build_embed(&defines, &includes)?;
let bindings = generate_bindings(&defines, &includes)?;
let out_file =

View File

@ -5,7 +5,7 @@ repository = "https://github.com/davidcole1340/ext-php-rs"
homepage = "https://github.com/davidcole1340/ext-php-rs"
license = "MIT OR Apache-2.0"
keywords = ["php", "ffi", "zend"]
version = "0.1.8"
version = "0.1.9"
authors = ["David Cole <david.cole1340@gmail.com>"]
edition = "2018"
categories = ["api-bindings", "command-line-interface"]

View File

@ -4,6 +4,7 @@ use anyhow::{Context, Result};
use ext_php_rs::describe::Description;
use libloading::os::unix::{Library, Symbol};
#[allow(improper_ctypes_definitions)]
pub struct Ext {
// These need to be here to keep the libraries alive. The extension library needs to be alive
// to access the describe function. Missing here is the lifetime on `Symbol<'a, fn() ->

View File

@ -4,7 +4,7 @@ description = "Derive macros for ext-php-rs."
repository = "https://github.com/davidcole1340/ext-php-rs"
homepage = "https://github.com/davidcole1340/ext-php-rs"
license = "MIT OR Apache-2.0"
version = "0.10.0"
version = "0.10.1"
authors = ["David Cole <david.cole1340@gmail.com>"]
edition = "2018"

View File

@ -26,6 +26,7 @@ pub struct Arg {
pub ty: String,
pub nullable: bool,
pub default: Option<String>,
pub as_ref: bool,
}
#[derive(Debug, Clone)]
@ -249,12 +250,19 @@ pub fn get_return_type(output_type: &ReturnType) -> Result<Option<(String, bool)
}
impl Arg {
pub fn new(name: String, ty: String, nullable: bool, default: Option<String>) -> Self {
pub fn new(
name: String,
ty: String,
nullable: bool,
default: Option<String>,
as_ref: bool,
) -> Self {
Self {
name,
ty,
nullable,
default,
as_ref,
}
}
@ -268,6 +276,7 @@ impl Arg {
match ty {
Type::Path(TypePath { path, .. }) => {
let mut path = path.clone();
let mut pass_by_ref = false;
path.drop_lifetimes();
let seg = path.segments.last()?;
@ -283,9 +292,45 @@ impl Arg {
None
}
});
// For for types that are `Option<&mut T>` to turn them into `Option<&T>`,
// marking the Arg as as "passed by reference".
let option = Some(seg)
.filter(|seg| seg.ident == "Option")
.and_then(|seg| {
if let PathArguments::AngleBracketed(args) = &seg.arguments {
args.args
.iter()
.find(|arg| matches!(arg, GenericArgument::Type(_)))
.map(|ga| {
let new_ga = match ga {
GenericArgument::Type(ty) => {
let _rtype = match ty {
Type::Reference(r) => {
let mut new_ref = r.clone();
new_ref.mutability = None;
pass_by_ref = true;
Type::Reference(new_ref)
}
_ => ty.clone(),
};
GenericArgument::Type(_rtype)
}
_ => ga.clone(),
};
new_ga.to_token_stream().to_string()
})
} else {
None
}
});
let stringified = match result {
Some(result) if is_return => result,
_ => path.to_token_stream().to_string(),
_ => match option {
Some(result) => result,
None => path.to_token_stream().to_string(),
},
};
Some(Arg::new(
@ -293,6 +338,7 @@ impl Arg {
stringified,
seg.ident == "Option" || default.is_some(),
default,
pass_by_ref,
))
}
Type::Reference(ref_) => {
@ -302,6 +348,7 @@ impl Arg {
ref_.to_token_stream().to_string(),
false,
default,
ref_.mutability.is_some(),
))
}
_ => None,
@ -361,6 +408,7 @@ impl Arg {
let ty = self.get_type_ident();
let null = self.nullable.then(|| quote! { .allow_null() });
let passed_by_ref = self.as_ref.then(|| quote! { .as_ref() });
let default = self.default.as_ref().map(|val| {
quote! {
.default(#val)
@ -368,7 +416,7 @@ impl Arg {
});
quote! {
::ext_php_rs::args::Arg::new(#name, #ty) #null #default
::ext_php_rs::args::Arg::new(#name, #ty) #null #passed_by_ref #default
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -23,12 +23,16 @@
- [Object](./types/object.md)
- [Class Object](./types/class_object.md)
- [Closure](./types/closure.md)
- [Functions & methods](./types/functions.md)
- [Async futures](./macros/async_impl.md)
- [Macros](./macros/index.md)
- [Module](./macros/module.md)
- [Module Startup Function](./macros/module_startup.md)
- [Function](./macros/function.md)
- [Classes](./macros/classes.md)
- [`impl`s](./macros/impl.md)
- [async `impl`s](./macros/async_impl.md)
- [Constants](./macros/constant.md)
- [`ZvalConvert`](./macros/zval_convert.md)
- [Exceptions](./exceptions.md)
- [INI Settings](./ini-settings.md)

48
guide/src/ini-settings.md Normal file
View File

@ -0,0 +1,48 @@
# INI Settings
Your PHP Extension may want to provide it's own PHP INI settings to configure behaviour. This can be done in the `#[php_startup]` annotated startup function.
## Registering INI Settings
All PHP INI definitions must be registered with PHP to get / set their values via the `php.ini` file or `ini_get() / ini_set()`.
```rust,no_run
# #![cfg_attr(windows, feature(abi_vectorcall))]
# extern crate ext_php_rs;
# use ext_php_rs::prelude::*;
# use ext_php_rs::zend::IniEntryDef;
# use ext_php_rs::flags::IniEntryPermission;
#[php_startup]
pub fn startup_function(ty: i32, module_number: i32) {
let ini_entries: Vec<IniEntryDef> = vec![
IniEntryDef::new(
"my_extension.display_emoji".to_owned(),
"yes".to_owned(),
IniEntryPermission::All,
),
];
IniEntryDef::register(ini_entries, module_number);
}
# fn main() {}
```
## Getting INI Settings
The INI values are stored as part of the `GlobalExecutor`, and can be accessed via the `ini_values()` function. To retrieve the value for a registered INI setting
```rust,no_run
# #![cfg_attr(windows, feature(abi_vectorcall))]
# extern crate ext_php_rs;
# use ext_php_rs::prelude::*;
# use ext_php_rs::zend::ExecutorGlobals;
#[php_startup]
pub fn startup_function(ty: i32, module_number: i32) {
// Get all INI values
let ini_values = ExecutorGlobals::get().ini_values(); // HashMap<String, Option<String>>
let my_ini_value = ini_values.get("my_extension.display_emoji"); // Option<Option<String>>
}
# fn main() {}
```

View File

@ -0,0 +1,130 @@
# `#[php_async_impl]`
Using `#[php_async_impl]` instead of `#[php_impl]` allows us to expose any async Rust library to PHP, using [PHP fibers](https://www.php.net/manual/en/language.fibers.php), [php-tokio](https://github.com/danog/php-tokio) and the [PHP Revolt event loop](https://revolt.run) under the hood to handle async interoperability.
This allows full compatibility with [amphp](https://amphp.org), [PSL](https://github.com/azjezz/psl), [reactphp](https://reactphp.org) and any other async PHP library based on [Revolt](https://revolt.run).
Traits annotated with `#[php_async_impl]` can freely expose any async function, using `await` and any async Rust library.
Make sure to also expose the `php_tokio::EventLoop::init` and `php_tokio::EventLoop::wakeup` functions to PHP in order to initialize the event loop, as specified in the full example [here &raquo;](#async-example).
Also, make sure to invoke `EventLoop::shutdown` in the request shutdown handler to clean up the tokio event loop before finishing the request.
## Async example
In this example, we're exposing an async Rust HTTP client library called [reqwest](https://docs.rs/reqwest/latest/reqwest/) to PHP, using [PHP fibers](https://www.php.net/manual/en/language.fibers.php), [php-tokio](https://github.com/danog/php-tokio) and the [PHP Revolt event loop](https://revolt.run) under the hood to handle async interoperability.
This allows full compatibility with [amphp](https://amphp.org), [PSL](https://github.com/azjezz/psl), [reactphp](https://reactphp.org) and any other async PHP library based on [Revolt](https://revolt.run).
Make sure to require [php-tokio](https://github.com/danog/php-tokio) as a dependency before proceeding.
```rust,ignore
use ext_php_rs::prelude::*;
use php_tokio::{php_async_impl, EventLoop};
#[php_class]
struct Client {}
#[php_async_impl]
impl Client {
pub fn init() -> PhpResult<u64> {
EventLoop::init()
}
pub fn wakeup() -> PhpResult<()> {
EventLoop::wakeup()
}
pub async fn get(url: &str) -> anyhow::Result<String> {
Ok(reqwest::get(url).await?.text().await?)
}
}
pub extern "C" fn request_shutdown(_type: i32, _module_number: i32) -> i32 {
EventLoop::shutdown();
0
}
#[php_module]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
module.request_shutdown_function(request_shutdown)
}
```
Here's the async PHP code we use to interact with the Rust class we just exposed.
The `Client::init` method needs to be called only once in order to initialize the Revolt event loop and link it to the Tokio event loop, as shown by the following code.
See [here &raquo;](https://amphp.org) for more info on async PHP using [amphp](https://amphp.org) + [revolt](https://revolt.run).
```php
<?php declare(strict_types=1);
namespace Reqwest;
use Revolt\EventLoop;
use function Amp\async;
use function Amp\Future\await;
final class Client
{
private static ?string $id = null;
public static function init(): void
{
if (self::$id !== null) {
return;
}
$f = \fopen("php://fd/".\Client::init(), 'r+');
\stream_set_blocking($f, false);
self::$id = EventLoop::onReadable($f, fn () => \Client::wakeup());
}
public static function reference(): void
{
EventLoop::reference(self::$id);
}
public static function unreference(): void
{
EventLoop::unreference(self::$id);
}
public static function __callStatic(string $name, array $args): mixed
{
return \Client::$name(...$args);
}
}
Client::init();
function test(int $delay): void
{
$url = "https://httpbin.org/delay/$delay";
$t = time();
echo "Making async reqwest to $url that will return after $delay seconds...".PHP_EOL;
Client::get($url);
$t = time() - $t;
echo "Got response from $url after ~".$t." seconds!".PHP_EOL;
};
$futures = [];
$futures []= async(test(...), 5);
$futures []= async(test(...), 5);
$futures []= async(test(...), 5);
await($futures);
```
Result:
```
Making async reqwest to https://httpbin.org/delay/5 that will return after 5 seconds...
Making async reqwest to https://httpbin.org/delay/5 that will return after 5 seconds...
Making async reqwest to https://httpbin.org/delay/5 that will return after 5 seconds...
Got response from https://httpbin.org/delay/5 after ~5 seconds!
Got response from https://httpbin.org/delay/5 after ~5 seconds!
Got response from https://httpbin.org/delay/5 after ~5 seconds!
```
[`php_function`]: ./function.md

View File

@ -8,6 +8,8 @@ implementations cannot be exported to PHP.
If you do not want a function exported to PHP, you should place it in a separate
`impl` block.
If you want to use async Rust, use `#[php_async_impl]`, instead: see [here &raquo;](./async_impl.md) for more info.
## Methods
Methods basically follow the same rules as functions, so read about the
@ -162,4 +164,4 @@ var_dump(Human::get_max_age()); // int(100)
var_dump(Human::MAX_AGE); // int(100)
```
[`php_function`]: ./function.md
[`php_async_impl`]: ./async_impl.md

View File

@ -45,3 +45,17 @@ pub fn test_bool(input: bool) -> String {
var_dump(test_bool(true)); // string(4) "Yes!"
var_dump(test_bool(false)); // string(3) "No!"
```
## Rust example, taking by reference
```rust,no_run
# #![cfg_attr(windows, feature(abi_vectorcall))]
# extern crate ext_php_rs;
# use ext_php_rs::prelude::*;
# use ext_php_rs::types;
#[php_function]
pub fn test_bool(input: &mut types::Zval) {
input.reference_mut().unwrap().set_bool(false);
}
# fn main() {}
```

View File

@ -0,0 +1,28 @@
# Functions & methods
PHP functions and methods are represented by the `Function` struct.
You can use the `try_from_function` and `try_from_method` methods to obtain a Function struct corresponding to the passed function or static method name.
It's heavily recommended you reuse returned `Function` objects, to avoid the overhead of looking up the function/method name.
```rust,no_run
# #![cfg_attr(windows, feature(abi_vectorcall))]
# extern crate ext_php_rs;
use ext_php_rs::prelude::*;
use ext_php_rs::zend::Function;
#[php_function]
pub fn test_function() -> () {
let var_dump = Function::try_from_function("var_dump").unwrap();
let _ = var_dump.try_call(vec![&"abc"]);
}
#[php_function]
pub fn test_method() -> () {
let f = Function::try_from_method("ClassName", "staticMethod").unwrap();
let _ = f.try_call(vec![&"abc"]);
}
# fn main() {}
```

View File

@ -15,6 +15,25 @@ object.
## Examples
### Calling a method
```rust,no_run
# #![cfg_attr(windows, feature(abi_vectorcall))]
# extern crate ext_php_rs;
use ext_php_rs::{prelude::*, types::ZendObject};
// Take an object reference and also return it.
#[php_function]
pub fn take_obj(obj: &mut ZendObject) -> () {
let _ = obj.try_call_method("hello", vec![&"arg1", &"arg2"]);
}
# #[php_module]
# pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
# module
# }
# fn main() {}
```
### Taking an object reference
```rust,no_run

View File

@ -123,6 +123,7 @@ impl<'a> Arg<'a> {
/// # Parameters
///
/// * `params` - A list of parameters to call the function with.
#[inline(always)]
pub fn try_call(&self, params: Vec<&dyn IntoZvalDyn>) -> Result<Zval> {
self.zval.as_ref().ok_or(Error::Callable)?.try_call(params)
}

View File

@ -161,10 +161,10 @@ impl ClassBuilder {
/// Panics if the class name associated with `T` is not the same as the
/// class name specified when creating the builder.
pub fn object_override<T: RegisteredClass>(mut self) -> Self {
extern "C" fn create_object<T: RegisteredClass>(_: *mut ClassEntry) -> *mut ZendObject {
extern "C" fn create_object<T: RegisteredClass>(ce: *mut ClassEntry) -> *mut ZendObject {
// SAFETY: After calling this function, PHP will always call the constructor
// defined below, which assumes that the object is uninitialized.
let obj = unsafe { ZendClassObject::<T>::new_uninit() };
let obj = unsafe { ZendClassObject::<T>::new_uninit(ce.as_ref()) };
obj.into_raw().get_mut_zend_obj()
}

View File

@ -32,7 +32,9 @@ impl<T> Deref for Vec<T> {
impl<T> Drop for Vec<T> {
fn drop(&mut self) {
unsafe { Box::from_raw(std::ptr::slice_from_raw_parts_mut(self.ptr, self.len)) };
unsafe {
let _ = Box::from_raw(std::ptr::slice_from_raw_parts_mut(self.ptr, self.len));
};
}
}

View File

@ -52,7 +52,7 @@ impl ToStub for Module {
// Inserts a value into the entries hashmap. Takes a key and an entry, creating
// the internal vector if it doesn't already exist.
let mut insert = |ns, entry| {
let bucket = entries.entry(ns).or_insert_with(StdVec::new);
let bucket = entries.entry(ns).or_default();
bucket.push(entry);
};

15
src/embed/embed.c Normal file
View File

@ -0,0 +1,15 @@
#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) {
void *result = NULL;
PHP_EMBED_START_BLOCK(argc, argv)
result = callback(ctx);
PHP_EMBED_END_BLOCK()
return result;
}

4
src/embed/embed.h Normal file
View 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
View 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) -> *const c_void,
ctx: *const c_void,
) -> *mut c_void;
}

277
src/embed/mod.rs Normal file
View File

@ -0,0 +1,277 @@
//! 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::{panic_wrapper, try_catch, ExecutorGlobals};
use parking_lot::{const_rwlock, RwLock};
use std::ffi::{c_char, c_void, CString, NulError};
use std::panic::{resume_unwind, RefUnwindSafe};
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,
CatchError,
}
impl EmbedError {
pub fn is_bailout(&self) -> bool {
matches!(self, EmbedError::CatchError)
}
}
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());
}
let exec_result = try_catch(|| unsafe { php_execute_script(&mut file_handle) });
match exec_result {
Err(_) => Err(EmbedError::CatchError),
Ok(true) => Ok(()),
Ok(false) => 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
///
/// # Returns
///
/// * R - The result of the function passed to this method
///
/// R must implement [`Default`] so it can be returned in case of a bailout
///
/// # 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<R, F: FnMut() -> R + RefUnwindSafe>(func: F) -> R
where
R: Default,
{
// @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();
let panic = unsafe {
ext_php_rs_embed_callback(
0,
null_mut(),
panic_wrapper::<R, F>,
&func as *const F as *const c_void,
)
};
// This can happen if there is a bailout
if panic.is_null() {
return R::default();
}
match unsafe { *Box::from_raw(panic as *mut std::thread::Result<R>) } {
Ok(r) => r,
Err(err) => {
// we resume the panic here so it can be catched correctly by the test framework
resume_unwind(err);
}
}
}
/// 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();
let exec_result = try_catch(|| unsafe {
zend_eval_string(
cstr.as_ptr() as *const c_char,
&mut result,
b"run\0".as_ptr() as *const _,
)
});
match exec_result {
Err(_) => Err(EmbedError::CatchError),
Ok(ZEND_RESULT_CODE_SUCCESS) => Ok(result),
Ok(_) => Err(EmbedError::ExecuteError(ExecutorGlobals::take_exception())),
}
}
}
#[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());
});
}
#[test]
#[should_panic]
fn test_panic() {
Embed::run(|| {
panic!("test panic");
});
}
#[test]
fn test_return() {
let foo = Embed::run(|| {
return "foo";
});
assert_eq!(foo, "foo");
}
#[test]
fn test_eval_bailout() {
Embed::run(|| {
let result = Embed::eval("str_repeat('a', 100_000_000_000_000);");
assert!(result.is_err());
assert!(result.unwrap_err().is_bailout());
});
}
}

View File

@ -0,0 +1,3 @@
<?php
throw new \RuntimeException('This is a test exception');

View File

@ -0,0 +1,7 @@
<?php
class Test {
public function __construct() {}
}
$foo = new Test();

View File

@ -57,6 +57,8 @@ pub enum Error {
InvalidUtf8,
/// Could not call the given function.
Callable,
/// An object was expected.
Object,
/// An invalid exception type was thrown.
InvalidException(ClassFlags),
/// Converting integer arguments resulted in an overflow.
@ -89,6 +91,7 @@ impl Display for Error {
),
Error::InvalidUtf8 => write!(f, "Invalid Utf8 byte sequence."),
Error::Callable => write!(f, "Could not call given function."),
Error::Object => write!(f, "An object was expected."),
Error::InvalidException(flags) => {
write!(f, "Invalid exception type was thrown: {flags:?}")
}

View File

@ -1,12 +1,14 @@
//! Types and functions used for throwing exceptions from Rust to PHP.
use std::ffi::CString;
use std::{ffi::CString, fmt::Debug};
use crate::{
class::RegisteredClass,
error::{Error, Result},
ffi::zend_throw_exception_ex,
ffi::zend_throw_exception_object,
flags::ClassFlags,
types::Zval,
zend::{ce, ClassEntry},
};
@ -25,6 +27,7 @@ pub struct PhpException {
message: String,
code: i32,
ex: &'static ClassEntry,
object: Option<Zval>,
}
impl PhpException {
@ -36,7 +39,12 @@ impl PhpException {
/// * `code` - Integer code to go inside the exception.
/// * `ex` - Exception type to throw.
pub fn new(message: String, code: i32, ex: &'static ClassEntry) -> Self {
Self { message, code, ex }
Self {
message,
code,
ex,
object: None,
}
}
/// Creates a new default exception instance, using the default PHP
@ -59,10 +67,25 @@ impl PhpException {
Self::new(message, 0, T::get_metadata().ce())
}
/// Set the Zval object for the exception.
///
/// Exceptions can be based of instantiated Zval objects when you are throwing a custom exception with
/// stateful properties.
///
/// # Parameters
///
/// * `object` - The Zval object.
pub fn set_object(&mut self, object: Option<Zval>) {
self.object = object;
}
/// Throws the exception, returning nothing inside a result if successful
/// and an error otherwise.
pub fn throw(self) -> Result<()> {
throw_with_code(self.ex, self.code, &self.message)
match self.object {
Some(object) => throw_object(object),
None => throw_with_code(self.ex, self.code, &self.message),
}
}
}
@ -146,3 +169,44 @@ pub fn throw_with_code(ex: &ClassEntry, code: i32, message: &str) -> Result<()>
};
Ok(())
}
/// Throws an exception object.
///
/// Returns a result containing nothing if the exception was successfully
/// thrown.
///
/// # Parameters
///
/// * `object` - The zval of type object
///
/// # Examples
///
/// ```no_run
/// use ext_php_rs::prelude::*;
/// use ext_php_rs::exception::throw_object;
/// use crate::ext_php_rs::convert::IntoZval;
///
/// #[php_class]
/// #[extends(ext_php_rs::zend::ce::exception())]
/// pub struct JsException {
/// #[prop(flags = ext_php_rs::flags::PropertyFlags::Public)]
/// message: String,
/// #[prop(flags = ext_php_rs::flags::PropertyFlags::Public)]
/// code: i32,
/// #[prop(flags = ext_php_rs::flags::PropertyFlags::Public)]
/// file: String,
/// }
///
/// #[php_module]
/// pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
/// module
/// }
///
/// let error = JsException { message: "A JS error occurred.".to_string(), code: 100, file: "index.js".to_string() };
/// throw_object( error.into_zval(true).unwrap() );
/// ```
pub fn throw_object(zval: Zval) -> Result<()> {
let mut zv = core::mem::ManuallyDrop::new(zval);
unsafe { zend_throw_exception_object(core::ptr::addr_of_mut!(zv).cast()) };
Ok(())
}

View File

@ -29,6 +29,13 @@ extern "C" {
pub fn ext_php_rs_process_globals() -> *mut php_core_globals;
pub fn ext_php_rs_sapi_globals() -> *mut sapi_globals_struct;
pub fn ext_php_rs_file_globals() -> *mut php_file_globals;
pub fn ext_php_rs_sapi_module() -> *mut sapi_module_struct;
pub fn ext_php_rs_zend_try_catch(
func: unsafe extern "C" fn(*const c_void) -> *const c_void,
ctx: *const c_void,
result: *mut *mut c_void,
) -> bool;
pub fn ext_php_rs_zend_bailout() -> !;
}
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));

View File

@ -8,9 +8,10 @@ use crate::ffi::{
CONST_CS, CONST_DEPRECATED, CONST_NO_FILE_CACHE, CONST_PERSISTENT, E_COMPILE_ERROR,
E_COMPILE_WARNING, E_CORE_ERROR, E_CORE_WARNING, E_DEPRECATED, E_ERROR, E_NOTICE, E_PARSE,
E_RECOVERABLE_ERROR, E_STRICT, E_USER_DEPRECATED, E_USER_ERROR, E_USER_NOTICE, E_USER_WARNING,
E_WARNING, IS_ARRAY, IS_CALLABLE, IS_CONSTANT_AST, IS_DOUBLE, IS_FALSE, IS_LONG, IS_MIXED,
IS_NULL, IS_OBJECT, IS_PTR, IS_REFERENCE, IS_RESOURCE, IS_STRING, IS_TRUE, IS_TYPE_COLLECTABLE,
IS_TYPE_REFCOUNTED, IS_UNDEF, IS_VOID, ZEND_ACC_ABSTRACT, ZEND_ACC_ANON_CLASS,
E_WARNING, IS_ARRAY, IS_CALLABLE, IS_CONSTANT_AST, IS_DOUBLE, IS_FALSE, IS_INDIRECT, IS_LONG,
IS_MIXED, IS_NULL, IS_OBJECT, IS_PTR, IS_REFERENCE, IS_RESOURCE, IS_STRING, IS_TRUE,
IS_TYPE_COLLECTABLE, IS_TYPE_REFCOUNTED, IS_UNDEF, IS_VOID, PHP_INI_ALL, PHP_INI_PERDIR,
PHP_INI_SYSTEM, PHP_INI_USER, ZEND_ACC_ABSTRACT, ZEND_ACC_ANON_CLASS,
ZEND_ACC_CALL_VIA_TRAMPOLINE, ZEND_ACC_CHANGED, ZEND_ACC_CLOSURE, ZEND_ACC_CONSTANTS_UPDATED,
ZEND_ACC_CTOR, ZEND_ACC_DEPRECATED, ZEND_ACC_DONE_PASS_TWO, ZEND_ACC_EARLY_BINDING,
ZEND_ACC_FAKE_CLOSURE, ZEND_ACC_FINAL, ZEND_ACC_GENERATOR, ZEND_ACC_HAS_FINALLY_BLOCK,
@ -174,6 +175,16 @@ bitflags! {
}
}
bitflags! {
/// Represents permissions for where a configuration setting may be set.
pub struct IniEntryPermission: u32 {
const User = PHP_INI_USER;
const PerDir = PHP_INI_PERDIR;
const System = PHP_INI_SYSTEM;
const All = PHP_INI_ALL;
}
}
bitflags! {
/// Represents error types when used via php_error_docref for example.
pub struct ErrorType: u32 {
@ -194,6 +205,7 @@ bitflags! {
const UserDeprecated = E_USER_DEPRECATED;
}
}
#[derive(PartialEq, Eq, Hash, Debug, Clone, Copy)]
pub enum FunctionType {
Internal,
@ -234,6 +246,7 @@ pub enum DataType {
Mixed,
Bool,
Ptr,
Indirect,
}
impl Default for DataType {
@ -257,6 +270,7 @@ impl DataType {
DataType::Object(_) => IS_OBJECT,
DataType::Resource => IS_RESOURCE,
DataType::Reference => IS_RESOURCE,
DataType::Indirect => IS_INDIRECT,
DataType::Callable => IS_CALLABLE,
DataType::ConstantExpression => IS_CONSTANT_AST,
DataType::Void => IS_VOID,
@ -325,6 +339,7 @@ impl From<u32> for DataType {
contains!(IS_VOID, Void);
contains!(IS_PTR, Ptr);
contains!(IS_INDIRECT, Indirect);
contains!(IS_CALLABLE, Callable);
contains!(IS_CONSTANT_AST, ConstantExpression);
contains!(IS_REFERENCE, Reference);
@ -367,6 +382,7 @@ impl Display for DataType {
DataType::Bool => write!(f, "Bool"),
DataType::Mixed => write!(f, "Mixed"),
DataType::Ptr => write!(f, "Pointer"),
DataType::Indirect => write!(f, "Indirect"),
}
}
}
@ -376,9 +392,9 @@ mod tests {
use super::DataType;
use crate::ffi::{
IS_ARRAY, IS_ARRAY_EX, IS_CALLABLE, IS_CONSTANT_AST, IS_CONSTANT_AST_EX, IS_DOUBLE,
IS_FALSE, IS_INTERNED_STRING_EX, IS_LONG, IS_NULL, IS_OBJECT, IS_OBJECT_EX, IS_PTR,
IS_REFERENCE, IS_REFERENCE_EX, IS_RESOURCE, IS_RESOURCE_EX, IS_STRING, IS_STRING_EX,
IS_TRUE, IS_UNDEF, IS_VOID,
IS_FALSE, IS_INDIRECT, IS_INTERNED_STRING_EX, IS_LONG, IS_NULL, IS_OBJECT, IS_OBJECT_EX,
IS_PTR, IS_REFERENCE, IS_REFERENCE_EX, IS_RESOURCE, IS_RESOURCE_EX, IS_STRING,
IS_STRING_EX, IS_TRUE, IS_UNDEF, IS_VOID,
};
use std::convert::TryFrom;
@ -402,7 +418,7 @@ mod tests {
test!(IS_RESOURCE, Resource);
test!(IS_REFERENCE, Reference);
test!(IS_CONSTANT_AST, ConstantExpression);
test!(IS_CALLABLE, Callable);
test!(IS_INDIRECT, Indirect);
test!(IS_VOID, Void);
test!(IS_PTR, Ptr);

View File

@ -25,6 +25,8 @@ pub mod class;
pub mod closure;
pub mod constant;
pub mod describe;
#[cfg(feature = "embed")]
pub mod embed;
#[doc(hidden)]
pub mod internal;
pub mod props;
@ -35,6 +37,7 @@ pub mod zend;
/// A module typically glob-imported containing the typically required macros
/// and imports.
pub mod prelude {
pub use crate::builders::ModuleBuilder;
#[cfg(any(docs, feature = "closure"))]
#[cfg_attr(docs, doc(cfg(feature = "closure")))]

View File

@ -187,6 +187,33 @@ impl ZendHashTable {
unsafe { zend_hash_str_find(self, str.as_ptr(), key.len() as _).as_ref() }
}
/// Attempts to retrieve a value from the hash table with a string key.
///
/// # Parameters
///
/// * `key` - The key to search for in the hash table.
///
/// # Returns
///
/// * `Some(&Zval)` - A reference to the zval at the position in the hash
/// table.
/// * `None` - No value at the given position was found.
///
/// # Example
///
/// ```no_run
/// use ext_php_rs::types::ZendHashTable;
///
/// let mut ht = ZendHashTable::new();
///
/// ht.insert("test", "hello world");
/// assert_eq!(ht.get("test").and_then(|zv| zv.str()), Some("hello world"));
/// ```
pub fn get_mut(&self, key: &'_ str) -> Option<&mut Zval> {
let str = CString::new(key).ok()?;
unsafe { zend_hash_str_find(self, str.as_ptr(), key.len() as _).as_mut() }
}
/// Attempts to retrieve a value from the hash table with an index.
///
/// # Parameters
@ -213,6 +240,32 @@ impl ZendHashTable {
unsafe { zend_hash_index_find(self, key).as_ref() }
}
/// Attempts to retrieve a value from the hash table with an index.
///
/// # Parameters
///
/// * `key` - The key to search for in the hash table.
///
/// # Returns
///
/// * `Some(&Zval)` - A reference to the zval at the position in the hash
/// table.
/// * `None` - No value at the given position was found.
///
/// # Example
///
/// ```no_run
/// use ext_php_rs::types::ZendHashTable;
///
/// let mut ht = ZendHashTable::new();
///
/// ht.push(100);
/// assert_eq!(ht.get_index(0).and_then(|zv| zv.long()), Some(100));
/// ```
pub fn get_index_mut(&self, key: u64) -> Option<&mut Zval> {
unsafe { zend_hash_index_find(self, key).as_mut() }
}
/// Attempts to remove a value from the hash table with a string key.
///
/// # Parameters
@ -715,7 +768,7 @@ impl<'a> FromZval<'a> for &'a ZendHashTable {
}
///////////////////////////////////////////
//// HashMap
/// HashMap
///////////////////////////////////////////
impl<'a, V> TryFrom<&'a ZendHashTable> for HashMap<String, V>
@ -784,7 +837,7 @@ where
}
///////////////////////////////////////////
//// Vec
/// Vec
///////////////////////////////////////////
impl<'a, T> TryFrom<&'a ZendHashTable> for Vec<T>

View File

@ -99,6 +99,7 @@ impl<'a> ZendCallable<'a> {
/// let result = strpos.try_call(vec![&"hello", &"e"]).unwrap();
/// assert_eq!(result.long(), Some(1));
/// ```
#[inline(always)]
pub fn try_call(&self, params: Vec<&dyn IntoZvalDyn>) -> Result<Zval> {
if !self.0.is_callable() {
return Err(Error::Callable);

View File

@ -5,6 +5,7 @@ use std::{
fmt::Debug,
mem,
ops::{Deref, DerefMut},
os::raw::c_char,
ptr::{self, NonNull},
};
@ -19,6 +20,7 @@ use crate::{
},
flags::DataType,
types::{ZendObject, Zval},
zend::ClassEntry,
};
/// Representation of a Zend class object in memory.
@ -43,7 +45,7 @@ impl<T: RegisteredClass> ZendClassObject<T> {
/// Panics if memory was unable to be allocated for the new object.
pub fn new(val: T) -> ZBox<Self> {
// SAFETY: We are providing a value to initialize the object with.
unsafe { Self::internal_new(Some(val)) }
unsafe { Self::internal_new(Some(val), None) }
}
/// Creates a new [`ZendClassObject`] of type `T`, with an uninitialized
@ -67,8 +69,8 @@ impl<T: RegisteredClass> ZendClassObject<T> {
/// # Panics
///
/// Panics if memory was unable to be allocated for the new object.
pub unsafe fn new_uninit() -> ZBox<Self> {
Self::internal_new(None)
pub unsafe fn new_uninit(ce: Option<&'static ClassEntry>) -> ZBox<Self> {
Self::internal_new(None, ce)
}
/// Creates a new [`ZendObject`] of type `T`, storing the given (and
@ -102,10 +104,10 @@ impl<T: RegisteredClass> ZendClassObject<T> {
/// # Panics
///
/// Panics if memory was unable to be allocated for the new object.
unsafe fn internal_new(val: Option<T>) -> ZBox<Self> {
unsafe fn internal_new(val: Option<T>, ce: Option<&'static ClassEntry>) -> ZBox<Self> {
let size = mem::size_of::<ZendClassObject<T>>();
let meta = T::get_metadata();
let ce = meta.ce() as *const _ as *mut _;
let ce = ce.unwrap_or_else(|| meta.ce()) as *const _ as *mut _;
let obj = ext_php_rs_zend_object_alloc(size as _, ce) as *mut ZendClassObject<T>;
let obj = obj
.as_mut()
@ -155,18 +157,19 @@ impl<T: RegisteredClass> ZendClassObject<T> {
/// # Parameters
///
/// * `obj` - The zend object to get the [`ZendClassObject`] for.
#[allow(clippy::needless_pass_by_ref_mut)]
pub fn from_zend_obj_mut(std: &mut zend_object) -> Option<&mut Self> {
Self::_from_zend_obj(std)
}
fn _from_zend_obj(std: &zend_object) -> Option<&mut Self> {
let std = std as *const zend_object as *const i8;
let std = std as *const zend_object as *const c_char;
let ptr = unsafe {
let ptr = std.offset(0 - Self::std_offset() as isize) as *const Self;
(ptr as *mut Self).as_mut()?
};
if ptr.std.is_instance::<T>() {
if ptr.std.instance_of(T::get_metadata().ce()) {
Some(ptr)
} else {
None

View File

@ -1,16 +1,17 @@
//! Represents an object in PHP. Allows for overriding the internal object used
//! by classes, allowing users to store Rust data inside a PHP object.
use std::{convert::TryInto, fmt::Debug, ops::DerefMut};
use std::{convert::TryInto, fmt::Debug, ops::DerefMut, os::raw::c_char};
use crate::{
boxed::{ZBox, ZBoxable},
class::RegisteredClass,
convert::{FromZendObject, FromZval, FromZvalMut, IntoZval},
convert::{FromZendObject, FromZval, FromZvalMut, IntoZval, IntoZvalDyn},
error::{Error, Result},
ffi::{
ext_php_rs_zend_object_release, zend_call_known_function, zend_object, zend_objects_new,
HashTable, ZEND_ISEMPTY, ZEND_PROPERTY_EXISTS, ZEND_PROPERTY_ISSET,
ext_php_rs_zend_object_release, object_properties_init, zend_call_known_function,
zend_function, zend_hash_str_find_ptr_lc, zend_object, zend_objects_new, HashTable,
ZEND_ISEMPTY, ZEND_PROPERTY_EXISTS, ZEND_PROPERTY_ISSET,
},
flags::DataType,
rc::PhpRc,
@ -41,7 +42,18 @@ impl ZendObject {
// SAFETY: Using emalloc to allocate memory inside Zend arena. Casting `ce` to
// `*mut` is valid as the function will not mutate `ce`.
unsafe {
let ptr = zend_objects_new(ce as *const _ as *mut _);
let ptr = match ce.__bindgen_anon_2.create_object {
None => {
let ptr = zend_objects_new(ce as *const _ as *mut _);
if ptr.is_null() {
panic!("Failed to allocate memory for Zend object")
}
object_properties_init(ptr, ce as *const _ as *mut _);
ptr
}
Some(v) => v(ce as *const _ as *mut _),
};
ZBox::from_raw(
ptr.as_mut()
.expect("Failed to allocate memory for Zend object"),
@ -121,6 +133,38 @@ impl ZendObject {
(self.ce as *const ClassEntry).eq(&(T::get_metadata().ce() as *const _))
}
#[inline(always)]
pub fn try_call_method(&self, name: &str, params: Vec<&dyn IntoZvalDyn>) -> Result<Zval> {
let mut retval = Zval::new();
let len = params.len();
let params = params
.into_iter()
.map(|val| val.as_zval(false))
.collect::<Result<Vec<_>>>()?;
let packed = params.into_boxed_slice();
unsafe {
let res = zend_hash_str_find_ptr_lc(
&(*self.ce).function_table,
name.as_ptr() as *const c_char,
name.len(),
) as *mut zend_function;
if res.is_null() {
return Err(Error::Callable);
}
zend_call_known_function(
res,
self as *const _ as *mut _,
self.ce,
&mut retval,
len as _,
packed.as_ptr() as *mut _,
std::ptr::null_mut(),
)
};
Ok(retval)
}
/// Attempts to read a property from the Object. Returns a result containing
/// the value of the property if it exists and can be read, and an
/// [`Error`] otherwise.

View File

@ -453,3 +453,23 @@ impl<'a> FromZval<'a> for &'a 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");
});
}
}

View File

@ -206,6 +206,31 @@ impl Zval {
}
}
#[inline(always)]
pub fn try_call_method(&self, name: &str, params: Vec<&dyn IntoZvalDyn>) -> Result<Zval> {
self.object()
.ok_or(Error::Object)?
.try_call_method(name, params)
}
/// Returns the value of the zval if it is an internal indirect reference.
pub fn indirect(&self) -> Option<&Zval> {
if self.is_indirect() {
Some(unsafe { &*(self.value.zv as *mut Zval) })
} else {
None
}
}
/// Returns a mutable reference to the zval if it is an internal indirect reference.
pub fn indirect_mut(&self) -> Option<&mut Zval> {
if self.is_indirect() {
Some(unsafe { &mut *(self.value.zv as *mut Zval) })
} else {
None
}
}
/// Returns the value of the zval if it is a reference.
pub fn reference(&self) -> Option<&Zval> {
if self.is_reference() {
@ -257,6 +282,7 @@ impl Zval {
/// # Parameters
///
/// * `params` - A list of parameters to call the function with.
#[inline(always)]
pub fn try_call(&self, params: Vec<&dyn IntoZvalDyn>) -> Result<Zval> {
self.callable().ok_or(Error::Callable)?.try_call(params)
}
@ -321,6 +347,11 @@ impl Zval {
self.get_type() == DataType::Reference
}
/// Returns true if the zval is a reference, false otherwise.
pub fn is_indirect(&self) -> bool {
self.get_type() == DataType::Indirect
}
/// Returns true if the zval is callable, false otherwise.
pub fn is_callable(&self) -> bool {
let ptr: *const Self = self;
@ -593,6 +624,7 @@ impl Debug for Zval {
DataType::ConstantExpression => field!(Option::<()>::None),
DataType::Void => field!(Option::<()>::None),
DataType::Bool => field!(self.bool()),
DataType::Indirect => field!(self.indirect()),
// SAFETY: We are not accessing the pointer.
DataType::Ptr => field!(unsafe { self.ptr::<c_void>() }),
};

View File

@ -52,7 +52,6 @@ php_core_globals *ext_php_rs_process_globals() {
#endif
}
sapi_globals_struct *ext_php_rs_sapi_globals() {
#ifdef ZTS
#ifdef ZEND_ENABLE_STATIC_TSRMLS_CACHE
@ -65,7 +64,6 @@ sapi_globals_struct *ext_php_rs_sapi_globals() {
#endif
}
php_file_globals *ext_php_rs_file_globals() {
#ifdef ZTS
return TSRMG_FAST_BULK(file_globals_id, php_file_globals *);
@ -73,3 +71,21 @@ php_file_globals *ext_php_rs_file_globals() {
return &file_globals;
#endif
}
sapi_module_struct *ext_php_rs_sapi_module() {
return &sapi_module;
}
bool ext_php_rs_zend_try_catch(void* (*callback)(void *), void *ctx, void **result) {
zend_try {
*result = callback(ctx);
} zend_catch {
return true;
} zend_end_try();
return false;
}
void ext_php_rs_zend_bailout() {
zend_bailout();
}

View File

@ -22,8 +22,9 @@
#include "zend_exceptions.h"
#include "zend_inheritance.h"
#include "zend_interfaces.h"
#include "SAPI.h"
#include "php_variables.h"
#include "zend_ini.h"
#include "main/SAPI.h"
zend_string *ext_php_rs_zend_string_init(const char *str, size_t len, bool persistent);
void ext_php_rs_zend_string_release(zend_string *zs);
@ -37,3 +38,6 @@ zend_executor_globals *ext_php_rs_executor_globals();
php_core_globals *ext_php_rs_process_globals();
sapi_globals_struct *ext_php_rs_sapi_globals();
php_file_globals *ext_php_rs_file_globals();
sapi_module_struct *ext_php_rs_sapi_module();
bool ext_php_rs_zend_try_catch(void* (*callback)(void *), void *ctx, void **result);
void ext_php_rs_zend_bailout();

View File

@ -1,6 +1,12 @@
//! Builder and objects for creating classes in the PHP world.
use crate::{ffi::zend_class_entry, flags::ClassFlags, types::ZendStr, zend::ExecutorGlobals};
use crate::{
boxed::ZBox,
ffi::zend_class_entry,
flags::ClassFlags,
types::{ZendObject, ZendStr},
zend::ExecutorGlobals,
};
use std::{convert::TryInto, fmt::Debug, ops::DerefMut};
/// A PHP class entry.
@ -22,6 +28,17 @@ impl ClassEntry {
}
}
/// Creates a new [`ZendObject`], returned inside an [`ZBox<ZendObject>`]
/// wrapper.
///
/// # Panics
///
/// Panics when allocating memory for the new object fails.
#[allow(clippy::new_ret_no_self)]
pub fn new(&self) -> ZBox<ZendObject> {
ZendObject::new(self)
}
/// Returns the class flags.
pub fn flags(&self) -> ClassFlags {
ClassFlags::from_bits_truncate(self.ce_flags)

View File

@ -6,6 +6,8 @@ use crate::{
types::{ZendClassObject, ZendObject, Zval},
};
use super::function::Function;
/// Execute data passed when a function is called from PHP.
///
/// This generally contains things related to the call, including but not
@ -194,6 +196,16 @@ impl ExecuteData {
self.This.object_mut()
}
/// Attempt to retrieve the function that is being called.
pub fn function(&self) -> Option<&Function> {
unsafe { self.func.as_ref() }
}
/// Attempt to retrieve the previous execute data on the call stack.
pub fn previous(&self) -> Option<&Self> {
unsafe { self.prev_execute_data.as_ref() }
}
/// Translation of macro `ZEND_CALL_ARG(call, n)`
/// zend_compile.h:578
///

View File

@ -2,7 +2,18 @@
use std::{fmt::Debug, os::raw::c_char, ptr};
use crate::ffi::zend_function_entry;
use crate::{
convert::IntoZvalDyn,
error::Result,
ffi::{
zend_call_known_function, zend_fetch_function_str, zend_function, zend_function_entry,
zend_hash_str_find_ptr_lc,
},
flags::FunctionType,
types::Zval,
};
use super::ClassEntry;
/// A Zend function entry.
pub type FunctionEntry = zend_function_entry;
@ -36,3 +47,86 @@ impl FunctionEntry {
Box::into_raw(Box::new(self))
}
}
pub type Function = zend_function;
impl Function {
pub fn function_type(&self) -> FunctionType {
FunctionType::from(unsafe { self.type_ })
}
pub fn try_from_function(name: &str) -> Option<Self> {
unsafe {
let res = zend_fetch_function_str(name.as_ptr() as *const c_char, name.len());
if res.is_null() {
return None;
}
Some(*res)
}
}
pub fn try_from_method(class: &str, name: &str) -> Option<Self> {
match ClassEntry::try_find(class) {
None => None,
Some(ce) => unsafe {
let res = zend_hash_str_find_ptr_lc(
&ce.function_table,
name.as_ptr() as *const c_char,
name.len(),
) as *mut zend_function;
if res.is_null() {
return None;
}
Some(*res)
},
}
}
/// Attempts to call the callable with a list of arguments to pass to the
/// function.
///
/// You should not call this function directly, rather through the
/// [`call_user_func`] macro.
///
/// # Parameters
///
/// * `params` - A list of parameters to call the function with.
///
/// # Returns
///
/// Returns the result wrapped in [`Ok`] upon success. If calling the
/// callable fails, or an exception is thrown, an [`Err`] is returned.
///
/// # Example
///
/// ```no_run
/// use ext_php_rs::types::ZendCallable;
///
/// let strpos = ZendCallable::try_from_name("strpos").unwrap();
/// let result = strpos.try_call(vec![&"hello", &"e"]).unwrap();
/// assert_eq!(result.long(), Some(1));
/// ```
#[inline(always)]
pub fn try_call(&self, params: Vec<&dyn IntoZvalDyn>) -> Result<Zval> {
let mut retval = Zval::new();
let len = params.len();
let params = params
.into_iter()
.map(|val| val.as_zval(false))
.collect::<Result<Vec<_>>>()?;
let packed = params.into_boxed_slice();
unsafe {
zend_call_known_function(
self as *const _ as *mut _,
std::ptr::null_mut(),
std::ptr::null_mut(),
&mut retval,
len as _,
packed.as_ptr() as *mut _,
std::ptr::null_mut(),
)
};
Ok(retval)
}
}

View File

@ -1,5 +1,6 @@
//! Types related to the PHP executor, sapi and process globals.
use std::ffi::CStr;
use std::collections::HashMap;
use std::ops::{Deref, DerefMut};
use std::slice;
use std::str;
@ -15,15 +16,24 @@ use crate::ffi::{
sapi_globals_struct, sapi_header_struct, sapi_headers_struct, sapi_request_info,
zend_is_auto_global, TRACK_VARS_COOKIE, TRACK_VARS_ENV, TRACK_VARS_FILES, TRACK_VARS_GET,
TRACK_VARS_POST, TRACK_VARS_REQUEST, TRACK_VARS_SERVER,
_sapi_globals_struct, _sapi_module_struct, _zend_executor_globals, ext_php_rs_executor_globals,
ext_php_rs_sapi_globals, ext_php_rs_sapi_module, zend_ini_entry,
};
use crate::types::{ZendHashTable, ZendObject, ZendStr};
use super::linked_list::ZendLinkedListIterator;
use crate::types::{ZendHashTable, ZendObject};
/// Stores global variables used in the PHP executor.
pub type ExecutorGlobals = _zend_executor_globals;
/// Stores global SAPI variables used in the PHP executor.
pub type SapiGlobals = _sapi_globals_struct;
/// Stores the SAPI module used in the PHP executor.
pub type SapiModule = _sapi_module_struct;
impl ExecutorGlobals {
/// Returns a reference to the PHP executor globals.
///
@ -62,6 +72,41 @@ impl ExecutorGlobals {
unsafe { self.class_table.as_ref() }
}
/// Attempts to retrieve the global functions hash table.
pub fn function_table(&self) -> Option<&ZendHashTable> {
unsafe { self.function_table.as_ref() }
}
/// Attempts to retrieve the global functions hash table as mutable.
pub fn function_table_mut(&self) -> Option<&mut ZendHashTable> {
unsafe { self.function_table.as_mut() }
}
/// Retrieves the ini values for all ini directives in the current executor
/// context..
pub fn ini_values(&self) -> HashMap<String, Option<String>> {
let hash_table = unsafe { &*self.ini_directives };
let mut ini_hash_map: HashMap<String, Option<String>> = HashMap::new();
for (_index, key, value) in hash_table.iter() {
if let Some(key) = key {
ini_hash_map.insert(key, unsafe {
let ini_entry = &*value.ptr::<zend_ini_entry>().expect("Invalid ini entry");
if ini_entry.value.is_null() {
None
} else {
Some(
(*ini_entry.value)
.as_str()
.expect("Ini value is not a string")
.to_owned(),
)
}
});
}
}
ini_hash_map
}
/// Attempts to retrieve the global constants table.
pub fn constants(&self) -> Option<&ZendHashTable> {
unsafe { self.zend_constants.as_ref() }
@ -97,6 +142,87 @@ impl ExecutorGlobals {
}
}
}
/// Cancel a requested an interrupt of the PHP VM.
pub fn cancel_interrupt(&mut self) {
cfg_if::cfg_if! {
if #[cfg(php82)] {
unsafe {
zend_atomic_bool_store(&mut self.vm_interrupt, false);
}
} else {
self.vm_interrupt = true;
}
}
}
}
impl SapiGlobals {
/// Returns a reference to the PHP SAPI globals.
///
/// The executor globals are guarded by a RwLock. There can be multiple
/// immutable references at one time but only ever one mutable reference.
/// Attempting to retrieve the globals while already holding the global
/// guard will lead to a deadlock. Dropping the globals guard will release
/// the lock.
pub fn get() -> GlobalReadGuard<Self> {
// SAFETY: PHP executor globals are statically declared therefore should never
// return an invalid pointer.
let globals = unsafe { ext_php_rs_sapi_globals().as_ref() }
.expect("Static executor globals were invalid");
let guard = SAPI_LOCK.read();
GlobalReadGuard { globals, guard }
}
/// Returns a mutable reference to the PHP executor globals.
///
/// The executor globals are guarded by a RwLock. There can be multiple
/// immutable references at one time but only ever one mutable reference.
/// Attempting to retrieve the globals while already holding the global
/// guard will lead to a deadlock. Dropping the globals guard will release
/// the lock.
pub fn get_mut() -> GlobalWriteGuard<Self> {
// SAFETY: PHP executor globals are statically declared therefore should never
// return an invalid pointer.
let globals = unsafe { ext_php_rs_sapi_globals().as_mut() }
.expect("Static executor globals were invalid");
let guard = SAPI_LOCK.write();
GlobalWriteGuard { globals, guard }
}
}
impl SapiModule {
/// Returns a reference to the PHP SAPI module.
///
/// The executor globals are guarded by a RwLock. There can be multiple
/// immutable references at one time but only ever one mutable reference.
/// Attempting to retrieve the globals while already holding the global
/// guard will lead to a deadlock. Dropping the globals guard will release
/// the lock.
pub fn get() -> GlobalReadGuard<Self> {
// SAFETY: PHP executor globals are statically declared therefore should never
// return an invalid pointer.
let globals = unsafe { ext_php_rs_sapi_module().as_ref() }
.expect("Static executor globals were invalid");
let guard = SAPI_MODULE_LOCK.read();
GlobalReadGuard { globals, guard }
}
/// Returns a mutable reference to the PHP executor globals.
///
/// The executor globals are guarded by a RwLock. There can be multiple
/// immutable references at one time but only ever one mutable reference.
/// Attempting to retrieve the globals while already holding the global
/// guard will lead to a deadlock. Dropping the globals guard will release
/// the lock.
pub fn get_mut() -> GlobalWriteGuard<Self> {
// SAFETY: PHP executor globals are statically declared therefore should never
// return an invalid pointer.
let globals = unsafe { ext_php_rs_sapi_module().as_mut() }
.expect("Static executor globals were invalid");
let guard = SAPI_MODULE_LOCK.write();
GlobalWriteGuard { globals, guard }
}
}
/// Stores global variables used in the PHP executor.
@ -430,6 +556,18 @@ static PROCESS_GLOBALS_LOCK: RwLock<()> = const_rwlock(());
static SAPI_GLOBALS_LOCK: RwLock<()> = const_rwlock(());
static FILE_GLOBALS_LOCK: RwLock<()> = const_rwlock(());
/// SAPI globals rwlock.
///
/// PHP provides no indication if the executor globals are being accessed so
/// this is only effective on the Rust side.
static SAPI_LOCK: RwLock<()> = const_rwlock(());
/// SAPI globals rwlock.
///
/// PHP provides no indication if the executor globals are being accessed so
/// this is only effective on the Rust side.
static SAPI_MODULE_LOCK: RwLock<()> = const_rwlock(());
/// Wrapper guard that contains a reference to a given type `T`. Dropping a
/// guard releases the lock on the relevant rwlock.
pub struct GlobalReadGuard<T: 'static> {

View File

@ -238,6 +238,7 @@ impl ZendObjectHandlers {
let mut zv = Zval::new();
val.get(self_, &mut zv)?;
#[allow(clippy::unnecessary_mut_passed)]
if zend_is_true(&mut zv) == 1 {
return Ok(1);
}

58
src/zend/ini_entry_def.rs Normal file
View File

@ -0,0 +1,58 @@
//! Builder for creating inis and methods in PHP.
//! See <https://www.phpinternalsbook.com/php7/extensions_design/ini_settings.html> for details.
use std::{ffi::CString, os::raw::c_char, ptr};
use crate::{ffi::zend_ini_entry_def, ffi::zend_register_ini_entries, flags::IniEntryPermission};
/// A Zend ini entry definition.
///
/// To register ini definitions for extensions, the IniEntryDef builder should
/// be used. Ini entries should be registered in your module's startup_function
/// via `IniEntryDef::register(Vec<IniEntryDef>)`.
pub type IniEntryDef = zend_ini_entry_def;
impl IniEntryDef {
/// Returns an empty ini entry, signifying the end of a ini list.
pub fn new(name: String, default_value: String, permission: IniEntryPermission) -> Self {
let mut template = Self::end();
let name = CString::new(name).expect("Unable to create CString from name");
let value = CString::new(default_value).expect("Unable to create CString from value");
template.name_length = name.as_bytes().len() as _;
template.name = name.into_raw();
template.value_length = value.as_bytes().len() as _;
template.value = value.into_raw();
template.modifiable = IniEntryPermission::PerDir.bits() as _;
template.modifiable = permission.bits() as _;
template
}
/// Returns an empty ini entry def, signifying the end of a ini list.
pub fn end() -> Self {
Self {
name: ptr::null() as *const c_char,
on_modify: None,
mh_arg1: std::ptr::null_mut(),
mh_arg2: std::ptr::null_mut(),
mh_arg3: std::ptr::null_mut(),
value: std::ptr::null_mut(),
displayer: None,
modifiable: 0,
value_length: 0,
name_length: 0,
}
}
/// Converts the ini entry into a raw and pointer, releasing it to the
/// C world.
pub fn into_raw(self) -> *mut Self {
Box::into_raw(Box::new(self))
}
pub fn register(mut entries: Vec<Self>, module_number: i32) {
entries.push(Self::end());
let entries = Box::into_raw(entries.into_boxed_slice()) as *const Self;
unsafe { zend_register_ini_entries(entries, module_number) };
}
}

View File

@ -8,14 +8,20 @@ mod function;
mod globals;
mod handlers;
mod linked_list;
mod ini_entry_def;
mod module;
mod try_catch;
use crate::{error::Result, ffi::php_printf};
use crate::{
error::Result,
ffi::{php_printf, sapi_module},
};
use std::ffi::CString;
pub use _type::ZendType;
pub use class::ClassEntry;
pub use ex::ExecuteData;
pub use function::Function;
pub use function::FunctionEntry;
pub use globals::ExecutorGlobals;
pub use globals::FileGlobals;
@ -23,7 +29,14 @@ pub use globals::ProcessGlobals;
pub use globals::SapiGlobals;
pub use handlers::ZendObjectHandlers;
pub use linked_list::ZendLinkedList;
pub use globals::SapiGlobals;
pub use globals::SapiModule;
pub use handlers::ZendObjectHandlers;
pub use ini_entry_def::IniEntryDef;
pub use module::ModuleEntry;
#[cfg(feature = "embed")]
pub(crate) use try_catch::panic_wrapper;
pub use try_catch::{bailout, try_catch};
// Used as the format string for `php_printf`.
const FORMAT_STR: &[u8] = b"%s\0";
@ -47,3 +60,9 @@ pub fn printf(message: &str) -> Result<()> {
};
Ok(())
}
/// Get the name of the SAPI module.
pub fn php_sapi_name() -> String {
let c_str = unsafe { std::ffi::CStr::from_ptr(sapi_module.name) };
c_str.to_str().expect("Unable to parse CStr").to_string()
}

166
src/zend/try_catch.rs Normal file
View File

@ -0,0 +1,166 @@
use crate::ffi::{ext_php_rs_zend_bailout, ext_php_rs_zend_try_catch};
use std::ffi::c_void;
use std::panic::{catch_unwind, resume_unwind, RefUnwindSafe};
use std::ptr::null_mut;
#[derive(Debug)]
pub struct CatchError;
pub(crate) unsafe extern "C" fn panic_wrapper<R, F: FnMut() -> R + RefUnwindSafe>(
ctx: *const c_void,
) -> *const c_void {
// we try to catch panic here so we correctly shutdown php if it happens
// mandatory when we do assert on test as other test would not run correctly
let panic = catch_unwind(|| (*(ctx as *mut F))());
Box::into_raw(Box::new(panic)) as *mut c_void
}
/// PHP propose a try catch mechanism in C using setjmp and longjmp (bailout)
/// It store the arg of setjmp into the bailout field of the global executor
/// If a bailout is triggered, the executor will jump to the setjmp and restore
/// the previous setjmp
///
/// try_catch allow to use this mechanism
///
/// # Returns
///
/// * `Ok(R)` - The result of the function
/// * `Err(CatchError)` - A bailout occurred during the execution
pub fn try_catch<R, F: FnMut() -> R + RefUnwindSafe>(func: F) -> Result<R, CatchError> {
let mut panic_ptr = null_mut();
let has_bailout = unsafe {
ext_php_rs_zend_try_catch(
panic_wrapper::<R, F>,
&func as *const F as *const c_void,
(&mut panic_ptr) as *mut *mut c_void,
)
};
let panic = panic_ptr as *mut std::thread::Result<R>;
// can be null if there is a bailout
if panic.is_null() || has_bailout {
return Err(CatchError);
}
match unsafe { *Box::from_raw(panic as *mut std::thread::Result<R>) } {
Ok(r) => Ok(r),
Err(err) => {
// we resume the panic here so it can be catched correctly by the test framework
resume_unwind(err);
}
}
}
/// Trigger a bailout
///
/// This function will stop the execution of the current script
/// and jump to the last try catch block
///
/// # Safety
///
/// This function is unsafe because it can cause memory leaks
/// Since it will jump to the last try catch block, it will not call the
/// destructor of the current scope
///
/// When using this function you should ensure that all the memory allocated in
/// the current scope is released
pub unsafe fn bailout() -> ! {
ext_php_rs_zend_bailout();
}
#[cfg(feature = "embed")]
#[cfg(test)]
mod tests {
use crate::embed::Embed;
use crate::zend::{bailout, try_catch};
use std::ptr::null_mut;
#[test]
fn test_catch() {
Embed::run(|| {
let catch = try_catch(|| {
unsafe {
bailout();
}
#[allow(unreachable_code)]
{
assert!(false);
}
});
assert!(catch.is_err());
});
}
#[test]
fn test_no_catch() {
Embed::run(|| {
let catch = try_catch(|| {
assert!(true);
});
assert!(catch.is_ok());
});
}
#[test]
fn test_bailout() {
Embed::run(|| {
unsafe {
bailout();
}
#[allow(unreachable_code)]
{
assert!(false);
}
});
}
#[test]
#[should_panic]
fn test_panic() {
Embed::run(|| {
let _ = try_catch(|| {
panic!("should panic");
});
});
}
#[test]
fn test_return() {
let foo = Embed::run(|| {
let result = try_catch(|| {
return "foo";
});
assert!(result.is_ok());
result.unwrap()
});
assert_eq!(foo, "foo");
}
#[test]
fn test_memory_leak() {
let mut ptr = null_mut();
let _ = try_catch(|| {
let mut result = "foo".to_string();
ptr = &mut result;
unsafe {
bailout();
}
});
// Check that the string is never released
let result = unsafe { &*ptr as &str };
assert_eq!(result, "foo");
}
}

44
tests/module.rs Normal file
View File

@ -0,0 +1,44 @@
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
#[cfg(feature = "embed")]
use ext_php_rs::embed::Embed;
#[cfg(feature = "embed")]
use ext_php_rs::ffi::zend_register_module_ex;
use ext_php_rs::prelude::*;
#[test]
#[cfg(feature = "embed")]
fn test_module() {
Embed::run(|| {
// Allow to load the module
unsafe { zend_register_module_ex(get_module()) };
let result = Embed::eval("$foo = hello_world('foo');");
assert!(result.is_ok());
let zval = result.unwrap();
assert!(zval.is_string());
let string = zval.string().unwrap();
assert_eq!(string.to_string(), "Hello, foo!");
});
}
/// Gives you a nice greeting!
///
/// @param string $name Your name.
///
/// @return string Nice greeting!
#[php_function]
pub fn hello_world(name: String) -> String {
format!("Hello, {}!", name)
}
#[php_module]
pub fn module(module: ModuleBuilder) -> ModuleBuilder {
module
}

View File

@ -55,4 +55,11 @@ impl<'a> PHPProvider<'a> for Provider {
fn get_defines(&self) -> Result<Vec<(&'static str, &'static str)>> {
Ok(vec![])
}
fn print_extra_link_args(&self) -> Result<()> {
#[cfg(feature = "embed")]
println!("cargo:rustc-link-lib=php");
Ok(())
}
}