diff --git a/Cargo.toml b/Cargo.toml index 0deea94671..29553ff7a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,7 +41,11 @@ closure = [] embed = [] [workspace] -members = ["crates/macros", "crates/cli"] +members = [ + "crates/macros", + "crates/cli", + "tests" +] [package.metadata.docs.rs] rustdoc-args = ["--cfg", "docs"] diff --git a/guide/src/types/binary.md b/guide/src/types/binary.md index aa296b16cd..74f9f9de3b 100644 --- a/guide/src/types/binary.md +++ b/guide/src/types/binary.md @@ -45,7 +45,7 @@ pub fn test_binary(input: Binary) -> Binary { ```php 5, [1] => 4, [2] => 3, [3] => 2, [4] => 1 } ``` diff --git a/src/args.rs b/src/args.rs index 7cd3f62d4a..4bc928f1f0 100644 --- a/src/args.rs +++ b/src/args.rs @@ -87,7 +87,7 @@ impl<'a> Arg<'a> { { self.zval .as_mut() - .and_then(|zv| T::from_zval_mut(zv)) + .and_then(|zv| T::from_zval_mut(zv.dereference_mut())) .ok_or(self) } @@ -98,7 +98,9 @@ impl<'a> Arg<'a> { where T: FromZvalMut<'a>, { - self.zval.as_mut().and_then(|zv| T::from_zval_mut(zv)) + self.zval + .as_mut() + .and_then(|zv| T::from_zval_mut(zv.dereference_mut())) } /// Attempts to return a reference to the arguments internal Zval. diff --git a/src/closure.rs b/src/closure.rs index 03e02653c7..d9884d133b 100644 --- a/src/closure.rs +++ b/src/closure.rs @@ -171,6 +171,7 @@ class_derives!(Closure); /// /// This trait is automatically implemented on functions with up to 8 /// parameters. +#[allow(clippy::missing_safety_doc)] pub unsafe trait PhpClosure { /// Invokes the closure. fn invoke<'a>(&'a mut self, parser: ArgParser<'a, '_>, ret: &mut Zval); diff --git a/src/types/zval.rs b/src/types/zval.rs index f031214f32..d997d53d1f 100644 --- a/src/types/zval.rs +++ b/src/types/zval.rs @@ -51,6 +51,25 @@ impl Zval { } } + /// Dereference the zval, if it is a reference. + pub fn dereference(&self) -> &Self { + return self.reference().or_else(|| self.indirect()).unwrap_or(self); + } + + /// Dereference the zval mutable, if it is a reference. + pub fn dereference_mut(&mut self) -> &mut Self { + // TODO: probably more ZTS work is needed here + if self.is_reference() { + #[allow(clippy::unwrap_used)] + return self.reference_mut().unwrap(); + } + if self.is_indirect() { + #[allow(clippy::unwrap_used)] + return self.indirect_mut().unwrap(); + } + self + } + /// Returns the value of the zval if it is a long. pub fn long(&self) -> Option { if self.is_long() { diff --git a/src/zend/_type.rs b/src/zend/_type.rs index 4342e3fcf4..dc2ba47830 100644 --- a/src/zend/_type.rs +++ b/src/zend/_type.rs @@ -1,7 +1,4 @@ -use std::{ - ffi::{c_void, CString}, - ptr, -}; +use std::{ffi::c_void, ptr}; use crate::{ ffi::{ @@ -9,6 +6,7 @@ use crate::{ _ZEND_SEND_MODE_SHIFT, _ZEND_TYPE_NAME_BIT, _ZEND_TYPE_NULLABLE_BIT, }, flags::DataType, + types::ZendStr, }; /// Internal Zend type. @@ -82,7 +80,7 @@ impl ZendType { allow_null: bool, ) -> Option { Some(Self { - ptr: CString::new(class_name).ok()?.into_raw() as *mut c_void, + ptr: ZendStr::new(class_name, true).into_raw().as_ptr() as *mut c_void, type_mask: _ZEND_TYPE_NAME_BIT | (if allow_null { _ZEND_TYPE_NULLABLE_BIT diff --git a/src/zend/try_catch.rs b/src/zend/try_catch.rs index 2f0696ce72..5d3f56889b 100644 --- a/src/zend/try_catch.rs +++ b/src/zend/try_catch.rs @@ -35,12 +35,13 @@ pub fn try_catch R + RefUnwindSafe>(func: F) -> Result '1', + 'b' => '2', + 'c' => '3' +]); + +assert(array_key_exists('a', $assoc)); +assert(array_key_exists('b', $assoc)); +assert(array_key_exists('c', $assoc)); +assert(in_array('1', $assoc)); +assert(in_array('2', $assoc)); +assert(in_array('3', $assoc)); diff --git a/tests/src/integration/array.rs b/tests/src/integration/array.rs new file mode 100644 index 0000000000..447ece1bde --- /dev/null +++ b/tests/src/integration/array.rs @@ -0,0 +1,4 @@ +#[test] +fn binary_works() { + assert!(crate::integration::run_php("array.php")); +} diff --git a/tests/src/integration/binary.php b/tests/src/integration/binary.php new file mode 100644 index 0000000000..30cbd429f7 --- /dev/null +++ b/tests/src/integration/binary.php @@ -0,0 +1,13 @@ + $a, 'test') === 'test'); diff --git a/tests/src/integration/callable.rs b/tests/src/integration/callable.rs new file mode 100644 index 0000000000..1d4ac2d512 --- /dev/null +++ b/tests/src/integration/callable.rs @@ -0,0 +1,4 @@ +#[test] +fn callable_works() { + assert!(crate::integration::run_php("callable.php")); +} diff --git a/tests/src/integration/class.php b/tests/src/integration/class.php new file mode 100644 index 0000000000..568cafbb17 --- /dev/null +++ b/tests/src/integration/class.php @@ -0,0 +1,21 @@ +getString() === 'lorem ipsum'); +$class->setString('dolor et'); +assert($class->getString() === 'dolor et'); + +assert($class->getNumber() === 2022); +$class->setNumber(2023); +assert($class->getNumber() === 2023); + +// Tests #prop decorator +assert($class->boolean); +$class->boolean = false; +assert($class->boolean === false); diff --git a/tests/src/integration/class.rs b/tests/src/integration/class.rs new file mode 100644 index 0000000000..525220e859 --- /dev/null +++ b/tests/src/integration/class.rs @@ -0,0 +1,4 @@ +#[test] +fn class_works() { + assert!(crate::integration::run_php("class.php")); +} diff --git a/tests/src/integration/closure.php b/tests/src/integration/closure.php new file mode 100644 index 0000000000..7ebe6b79b2 --- /dev/null +++ b/tests/src/integration/closure.php @@ -0,0 +1,14 @@ + test_number_unsigned(-12)); + +// Float +assert(round(test_number_float(-1.2), 2) === round(-1.2, 2)); +assert(round(test_number_float(0.0), 2) === round(0.0, 2)); +assert(round(test_number_float(1.2), 2) === round(1.2, 2)); diff --git a/tests/src/integration/number.rs b/tests/src/integration/number.rs new file mode 100644 index 0000000000..94a518c4c8 --- /dev/null +++ b/tests/src/integration/number.rs @@ -0,0 +1,4 @@ +#[test] +fn number_works() { + assert!(crate::integration::run_php("number.php")); +} diff --git a/tests/src/integration/object.php b/tests/src/integration/object.php new file mode 100644 index 0000000000..c0b4c3fac5 --- /dev/null +++ b/tests/src/integration/object.php @@ -0,0 +1,16 @@ +string = 'string'; +$obj->bool = true; +$obj->number = 2022; +$obj->array = [ + 1, 2, 3 +]; + +$test = test_object($obj); + +assert($test->string === 'string'); +assert($test->bool === true); +assert($test->number === 2022); +assert($test->array === [1, 2, 3]); diff --git a/tests/src/integration/object.rs b/tests/src/integration/object.rs new file mode 100644 index 0000000000..11f70dbb7d --- /dev/null +++ b/tests/src/integration/object.rs @@ -0,0 +1,4 @@ +#[test] +fn object_works() { + assert!(crate::integration::run_php("object.php")); +} diff --git a/tests/src/integration/string.php b/tests/src/integration/string.php new file mode 100644 index 0000000000..63e111f56d --- /dev/null +++ b/tests/src/integration/string.php @@ -0,0 +1,6 @@ + &str { + a +} + +#[php_function] +pub fn test_string(a: String) -> String { + a +} + +#[php_function] +pub fn test_bool(a: bool) -> bool { + a +} + +#[php_function] +pub fn test_number_signed(a: i32) -> i32 { + a +} + +#[php_function] +pub fn test_number_unsigned(a: u32) -> u32 { + a +} + +#[php_function] +pub fn test_number_float(a: f32) -> f32 { + a +} + +#[php_function] +pub fn test_array(a: Vec) -> Vec { + a +} + +#[php_function] +pub fn test_array_assoc(a: HashMap) -> HashMap { + a +} + +#[php_function] +pub fn test_binary(a: Binary) -> Binary { + a +} + +#[php_function] +pub fn test_nullable(a: Option) -> Option { + a +} + +#[php_function] +pub fn test_object(a: &mut ZendObject) -> &mut ZendObject { + a +} + +#[php_function] +pub fn test_closure() -> Closure { + Closure::wrap(Box::new(|a| a) as Box String>) +} + +#[php_function] +pub fn test_closure_once(a: String) -> Closure { + Closure::wrap_once(Box::new(move || a) as Box String>) +} + +#[php_function] +pub fn test_callable(call: ZendCallable, a: String) -> Zval { + call.try_call(vec![&a]).expect("Failed to call function") +} + +#[php_class] +pub struct TestClass { + string: String, + number: i32, + #[prop] + boolean: bool, +} + +#[php_impl] +impl TestClass { + #[getter] + pub fn get_string(&self) -> String { + self.string.to_string() + } + + #[setter] + pub fn set_string(&mut self, string: String) { + self.string = string; + } + + #[getter] + pub fn get_number(&self) -> i32 { + self.number + } + + #[setter] + pub fn set_number(&mut self, number: i32) { + self.number = number; + } +} + +#[php_function] +pub fn test_class(string: String, number: i32) -> TestClass { + TestClass { + string, + number, + boolean: true, + } +} + +#[php_module] +pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { + module +} + +#[cfg(test)] +mod integration { + use std::process::Command; + use std::sync::Once; + + static BUILD: Once = Once::new(); + + fn setup() { + BUILD.call_once(|| { + assert!(Command::new("cargo") + .arg("build") + .output() + .expect("failed to build extension") + .status + .success()); + }); + } + + pub fn run_php(file: &str) -> bool { + setup(); + let output = Command::new("php") + .arg(format!( + "-dextension=../target/debug/libtests.{}", + std::env::consts::DLL_EXTENSION + )) + .arg("-dassert.active=1") + .arg("-dassert.exception=1") + .arg("-dzend.assertions=1") + .arg(format!("src/integration/{}", file)) + .output() + .expect("failed to run php file"); + if output.status.success() { + true + } else { + panic!( + " + status: {} + stdout: {} + stderr: {} + ", + output.status, + String::from_utf8(output.stdout).unwrap(), + String::from_utf8(output.stderr).unwrap() + ); + } + } + + mod array; + mod binary; + mod bool; + mod callable; + mod class; + mod closure; + mod nullable; + mod number; + mod object; + mod string; +}