Added #[php_extern] macro (#51)

* Fixed memory leak with `phpinfo()` table

* Added `#[php_extern]` macro
This commit is contained in:
David 2021-08-19 12:36:33 +12:00 committed by GitHub
parent a2781b794a
commit 626c944218
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 283 additions and 22 deletions

View File

@ -1,6 +1,6 @@
#![doc(hidden)]
use crate::bindings::{_efree, _emalloc};
use ext_php_rs::bindings::{_efree, _emalloc};
use std::alloc::GlobalAlloc;
/// Global allocator which uses the Zend memory management APIs to allocate memory.

View File

@ -1,21 +1,29 @@
mod allocator;
use std::collections::HashMap;
use allocator::PhpAllocator;
use ext_php_rs::{
call_user_func, info_table_end, info_table_row, info_table_start,
php::{
exceptions::PhpException,
module::ModuleEntry,
types::{array::ZendHashTable, callable::Callable},
types::{array::ZendHashTable, callable::Callable, zval::Zval},
},
prelude::*,
};
// #[global_allocator]
// static GLOBAL: PhpAllocator = PhpAllocator::new();
#[global_allocator]
static GLOBAL: PhpAllocator = PhpAllocator::new();
#[php_function]
pub fn hello_world(name: String) -> String {
format!("Hello, {}!", name)
pub fn hello_world() -> String {
let call = Callable::try_from_name("strpos").unwrap();
eprintln!("im callin");
let val = call.try_call(vec![&"hello world", &"w"]);
dbg!(val);
"Ok".into()
}
#[derive(Debug, Default, ZendObjectHandler)]
@ -132,6 +140,15 @@ pub fn skel_unpack<'a>(
Ok(arr)
}
#[php_function]
pub fn test_extern() -> i32 {
let y = unsafe { strpos("hello", "e", None) };
dbg!(y);
// let x = unsafe { test_func() };
// dbg!(x.try_call(vec![]));
0
}
#[no_mangle]
pub extern "C" fn php_module_info(_module: *mut ModuleEntry) {
info_table_start!();
@ -146,3 +163,10 @@ pub fn startup() {}
pub fn module(module: ModuleBuilder) -> ModuleBuilder {
module.info_function(php_module_info)
}
#[php_extern]
extern "C" {
fn test_func<'a>() -> Callable<'a>;
fn strpos2<'a>(haystack: &str, needle: &str, offset: Option<i32>) -> Zval;
pub fn strpos<'a>(haystack: &str, needle: &str, offset: Option<i32>) -> Zval;
}

View File

@ -2,4 +2,9 @@
include __DIR__.'/vendor/autoload.php';
var_dump(hello_world(5));
function test_func() {
return "Hello wqorld";
}
// var_dump(hello_world(5));
var_dump(test_extern());

View File

@ -0,0 +1,72 @@
use proc_macro2::TokenStream;
use quote::quote;
use syn::{punctuated::Punctuated, ForeignItemFn, ItemForeignMod, ReturnType, Signature, Token};
use crate::error::Result;
pub fn parser(input: ItemForeignMod) -> Result<TokenStream> {
input
.items
.into_iter()
.map(|item| match item {
syn::ForeignItem::Fn(func) => parse_function(func),
_ => Err("Only `extern` functions are supported by PHP.".into()),
})
.collect::<Result<Vec<_>>>()
.map(|vec| quote! { #(#vec)* })
}
fn parse_function(mut func: ForeignItemFn) -> Result<TokenStream> {
let ForeignItemFn {
attrs, vis, sig, ..
} = &mut func;
sig.unsafety = Some(Default::default()); // Function must be unsafe.
let Signature { ident, .. } = &sig;
let name = ident.to_string();
let params = sig
.inputs
.iter()
.map(|input| match input {
syn::FnArg::Typed(arg) => {
let pat = &arg.pat;
Some(quote! { &#pat })
}
_ => None,
})
.collect::<Option<Punctuated<_, Token![,]>>>()
.ok_or("`self` parameters are not permitted inside `#[php_extern]` blocks.")?;
let ret = build_return(&name, &sig.output, params);
Ok(quote! {
#(#attrs)* #vis #sig {
use ::std::convert::TryInto;
let callable = ::ext_php_rs::php::types::callable::Callable::try_from_name(
#name
).expect(concat!("Unable to find callable function `", #name, "`."));
#ret
}
})
}
fn build_return(
name: &str,
return_type: &ReturnType,
params: Punctuated<TokenStream, Token![,]>,
) -> TokenStream {
match return_type {
ReturnType::Default => quote! {
callable.try_call(vec![ #params ]);
},
ReturnType::Type(_, _) => quote! {
callable
.try_call(vec![ #params ])
.ok()
.and_then(|zv| zv.try_into().ok())
.expect(concat!("Failed to call function `", #name, "`."))
},
}
}

View File

@ -1,6 +1,7 @@
mod class;
mod constant;
mod error;
mod extern_;
mod function;
mod impl_;
mod method;
@ -12,7 +13,9 @@ use std::{collections::HashMap, sync::Mutex};
use constant::Constant;
use proc_macro::TokenStream;
use proc_macro2::Span;
use syn::{parse_macro_input, AttributeArgs, DeriveInput, ItemConst, ItemFn, ItemImpl};
use syn::{
parse_macro_input, AttributeArgs, DeriveInput, ItemConst, ItemFn, ItemForeignMod, ItemImpl,
};
extern crate proc_macro;
@ -95,3 +98,14 @@ pub fn php_const(_: TokenStream, input: TokenStream) -> TokenStream {
}
.into()
}
#[proc_macro_attribute]
pub fn php_extern(_: TokenStream, input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as ItemForeignMod);
match extern_::parser(input) {
Ok(parsed) => parsed,
Err(e) => syn::Error::new(Span::call_site(), e).to_compile_error(),
}
.into()
}

View File

@ -30,6 +30,49 @@ pub mod php;
/// ```
pub use ext_php_rs_derive::php_const;
/// Attribute used to annotate `extern` blocks which are deemed as PHP functions.
///
/// This allows you to 'import' PHP functions into Rust so that they can be called like regular
/// Rust functions. Parameters can be any type that implements [`IntoZval`], and the return type
/// can be anything that implements [`From<Zval>`] (notice how [`Zval`] is consumed rather than
/// borrowed in this case).
///
/// # Panics
///
/// The function can panic when called under a few circumstances:
///
/// * The function could not be found or was not callable.
/// * One of the parameters could not be converted into a [`Zval`].
/// * The actual function call failed internally.
/// * The output [`Zval`] could not be parsed into the output type.
///
/// The last point can be important when interacting with functions that return unions, such as
/// [`strpos`] which can return an integer or a boolean. In this case, a [`Zval`] should be
/// returned as parsing a boolean to an integer is invalid, and vice versa.
///
/// # Example
///
/// This `extern` block imports the [`strpos`] function from PHP. Notice that the string parameters
/// can take either [`String`] or [`&str`], the optional parameter `offset` is an [`Option<i64>`],
/// and the return value is a [`Zval`] as the return type is an integer-boolean union.
///
/// ```ignore
/// #[php_extern]
/// extern "C" {
/// fn strpos(haystack: &str, needle: &str, offset: Option<i64>) -> Zval;
/// }
///
/// #[php_function]
/// pub fn my_strpos() {
/// assert_eq!(unsafe { strpos("Hello", "e", None) }, 1);
/// }
/// ```
///
/// [`strpos`]: https://www.php.net/manual/en/function.strpos.php
/// [`IntoZval`]: ext_php_rs::php::types::zval::IntoZval
/// [`Zval`]: ext_php_rs::php::types::zval::Zval
pub use ext_php_rs_derive::php_extern;
/// Attribute used to annotate a function as a PHP function.
///
/// Only types that implement [`FromZval`] can be used as parameter and return types. These include
@ -300,6 +343,7 @@ pub use ext_php_rs_derive::ZendObjectHandler;
pub mod prelude {
pub use crate::php::module::ModuleBuilder;
pub use crate::php_const;
pub use crate::php_extern;
pub use crate::php_function;
pub use crate::php_impl;
pub use crate::php_module;

View File

@ -37,7 +37,7 @@ macro_rules! info_table_row {
macro_rules! _info_table_row {
($fn: ident, $($element: expr),*) => {
unsafe {
$crate::bindings::$fn($crate::_info_table_row!(@COUNT; $($element),*) as i32, $(::std::ffi::CString::new($element).unwrap().into_raw()),*);
$crate::bindings::$fn($crate::_info_table_row!(@COUNT; $($element),*) as i32, $(::std::ffi::CString::new($element).unwrap().as_ptr()),*);
}
};

View File

@ -1,7 +1,7 @@
//! Types relating to binary data transmission between Rust and PHP.
use std::{
convert::TryFrom,
convert::{TryFrom, TryInto},
ops::{Deref, DerefMut},
};
@ -43,6 +43,10 @@ impl<T: Pack> DerefMut for Binary<T> {
}
}
impl<'a, T: Pack> FromZval<'a> for Binary<T> {
const TYPE: DataType = DataType::String;
}
impl<T: Pack> TryFrom<&Zval> for Binary<T> {
type Error = Error;
@ -54,8 +58,12 @@ impl<T: Pack> TryFrom<&Zval> for Binary<T> {
}
}
impl<'a, T: Pack> FromZval<'a> for Binary<T> {
const TYPE: DataType = DataType::String;
impl<T: Pack> TryFrom<Zval> for Binary<T> {
type Error = Error;
fn try_from(value: Zval) -> Result<Self> {
(&value).try_into()
}
}
impl<T: Pack> IntoZval for Binary<T> {

View File

@ -1,5 +1,7 @@
//! Types related to callables in PHP (anonymous functions, functions, etc).
use std::ops::Deref;
use super::zval::{IntoZval, Zval};
use crate::{
bindings::_call_user_function_impl,
@ -8,7 +10,32 @@ use crate::{
/// Acts as a wrapper around a callable [`Zval`]. Allows the owner to call the [`Zval`] as if it
/// was a PHP function through the [`try_call`](Callable::try_call) method.
pub struct Callable<'a>(&'a Zval);
#[derive(Debug)]
pub struct Callable<'a>(OwnedZval<'a>);
/// A container for a zval. Either contains a reference to a zval or an owned zval.
#[derive(Debug)]
enum OwnedZval<'a> {
Reference(&'a Zval),
Owned(Zval),
}
impl<'a> OwnedZval<'a> {
fn as_ref(&self) -> &Zval {
match self {
OwnedZval::Reference(zv) => *zv,
OwnedZval::Owned(zv) => zv,
}
}
}
impl<'a> Deref for OwnedZval<'a> {
type Target = Zval;
fn deref(&self) -> &Self::Target {
self.as_ref()
}
}
impl<'a> Callable<'a> {
/// Attempts to create a new [`Callable`]. Should not need to be used directly, but through the
@ -23,12 +50,39 @@ impl<'a> Callable<'a> {
/// Returns an error if the [`Zval`] was not callable.
pub fn new(callable: &'a Zval) -> Result<Self> {
if callable.is_callable() {
Ok(Self(callable))
Ok(Self(OwnedZval::Reference(callable)))
} else {
Err(Error::Callable)
}
}
/// Attempts to create a new [`Callable`] by taking ownership of a Zval. Returns a result
/// containing the callable if the zval was callable.
///
/// # Parameters
///
/// * `callable` - TThe underlying [`Zval`] that is callable.
pub fn new_owned(callable: Zval) -> Result<Self> {
if callable.is_callable() {
Ok(Self(OwnedZval::Owned(callable)))
} else {
Err(Error::Callable)
}
}
/// Attempts to create a new [`Callable`] from a function name. Returns a result containing the
/// callable if the function existed and was callable.
///
/// # Parameters
///
/// * `name` - Name of the callable function.
pub fn try_from_name(name: &str) -> Result<Self> {
let mut callable = Zval::new();
callable.set_string(name, false)?;
Self::new_owned(callable)
}
/// Attempts to call the callable with a list of arguments to pass to the function.
/// Note that a thrown exception inside the callable is not detectable, therefore you should
/// check if the return value is valid rather than unwrapping. Returns a result containing the
@ -50,23 +104,19 @@ impl<'a> Callable<'a> {
.into_iter()
.map(|val| val.as_zval(false))
.collect::<Result<Vec<_>>>()?;
let packed = Box::into_raw(params.into_boxed_slice()) as *mut Zval;
let packed = params.into_boxed_slice();
let result = unsafe {
_call_user_function_impl(
std::ptr::null_mut(),
std::mem::transmute(self.0),
std::mem::transmute(self.0.as_ref()),
&mut retval,
len as _,
packed,
packed.as_ptr() as *mut _,
std::ptr::null_mut(),
)
};
// SAFETY: We just boxed this vector, and the `_call_user_function_impl` does not modify the parameters.
// We can safely reclaim the memory knowing it will have the same length and size.
unsafe { Vec::from_raw_parts(packed, len, len) };
if result < 0 {
Err(Error::Callable)
} else {

View File

@ -577,6 +577,14 @@ macro_rules! try_from_zval {
.ok_or(Error::ZvalConversion(value.get_type()?))
}
}
impl TryFrom<Zval> for $type {
type Error = Error;
fn try_from(value: Zval) -> Result<Self> {
(&value).try_into()
}
}
};
}
@ -632,6 +640,20 @@ where
}
}
impl<'a, T> TryFrom<Zval> for Vec<T>
where
T: FromZval<'a>,
{
type Error = Error;
fn try_from(value: Zval) -> Result<Self> {
value
.array()
.ok_or(Error::ZvalConversion(value.get_type()?))?
.try_into()
}
}
impl<'a, T> FromZval<'a> for HashMap<String, T>
where
T: FromZval<'a>,
@ -653,6 +675,20 @@ where
}
}
impl<'a, T> TryFrom<Zval> for HashMap<String, T>
where
T: FromZval<'a>,
{
type Error = Error;
fn try_from(value: Zval) -> Result<Self> {
value
.array()
.ok_or(Error::ZvalConversion(value.get_type()?))?
.try_into()
}
}
impl<'a> FromZval<'a> for Callable<'a> {
const TYPE: DataType = DataType::Callable;
}
@ -661,6 +697,14 @@ impl<'a> TryFrom<&'a Zval> for Callable<'a> {
type Error = Error;
fn try_from(value: &'a Zval) -> Result<Self> {
value.callable().ok_or(Error::Callable)
Callable::new(value)
}
}
impl<'a> TryFrom<Zval> for Callable<'a> {
type Error = Error;
fn try_from(value: Zval) -> Result<Self> {
Callable::new_owned(value)
}
}