├── .envrc ├── guide ├── .gitignore ├── book.toml └── src │ ├── types │ ├── functions.md │ ├── vec.md │ ├── string.md │ ├── option.md │ ├── str.md │ ├── iterable.md │ ├── bool.md │ ├── numbers.md │ ├── binary_slice.md │ ├── iterator.md │ ├── index.md │ ├── binary.md │ ├── class_object.md │ ├── object.md │ └── hashmap.md │ ├── advanced │ └── allowed_bindings.md │ ├── macros │ ├── index.md │ ├── constant.md │ ├── interface.md │ ├── extern.md │ ├── module.md │ ├── enum.md │ └── php.md │ ├── SUMMARY.md │ ├── ini-builder.md │ ├── introduction.md │ ├── ini-settings.md │ ├── exceptions.md │ └── getting-started │ └── installation.md ├── rustfmt.toml ├── .dockerignore ├── crates ├── macros │ ├── README.md │ ├── LICENSE_MIT │ ├── LICENSE_APACHE │ ├── .gitignore │ ├── tests │ │ └── expand │ │ │ ├── class.rs │ │ │ ├── const.rs │ │ │ ├── const.expanded.rs │ │ │ ├── enum.rs │ │ │ ├── interface.rs │ │ │ └── class.expanded.rs │ ├── src │ │ ├── fastcall.rs │ │ ├── helpers.rs │ │ ├── constant.rs │ │ ├── extern_.rs │ │ └── module.rs │ └── Cargo.toml └── cli │ ├── LICENSE_MIT │ ├── LICENSE_APACHE │ ├── allowed_bindings.rs │ ├── .gitignore │ ├── build.rs │ ├── src │ ├── main.rs │ └── ext.rs │ ├── Cargo.toml │ └── CHANGELOG.md ├── .github ├── FUNDING.yml ├── workflows │ ├── merge_request.yml │ ├── stale-issues.yml │ ├── master.yml │ ├── docsrs_bindings.yml │ ├── macro_docs.yml │ ├── docs.yml │ ├── coverage.yml │ └── release-plz.yml ├── dependabot.yml ├── PULL_REQUEST_TEMPLATE.md ├── action │ └── musl │ │ └── Dockerfile └── ISSUE_TEMPLATE │ ├── 02_feature_request.yml │ ├── 03_help.yml │ └── 01_bug_report.yml ├── .gitignore ├── src ├── embed │ ├── test-script-exception.php │ ├── test-script.php │ ├── embed.h │ ├── sapi.rs │ ├── ffi.rs │ └── embed.c ├── util │ ├── mod.rs │ └── cstring_scope.rs ├── internal │ ├── property.rs │ ├── function.rs │ ├── mod.rs │ └── class.rs ├── zend │ ├── module.rs │ ├── linked_list.rs │ ├── mod.rs │ └── ini_entry_def.rs ├── types │ ├── array │ │ └── conversions │ │ │ ├── mod.rs │ │ │ └── hash_map.rs │ ├── iterator.test.php │ ├── mod.rs │ ├── long.rs │ └── iterable.rs ├── builders │ └── mod.rs ├── test │ └── mod.rs ├── rc.rs ├── ffi.rs ├── lib.rs ├── wrapper.h ├── wrapper.c └── alloc.rs ├── tests ├── src │ ├── integration │ │ ├── bool │ │ │ ├── bool.php │ │ │ └── mod.rs │ │ ├── callable │ │ │ ├── callable.php │ │ │ └── mod.rs │ │ ├── string │ │ │ ├── string.php │ │ │ └── mod.rs │ │ ├── nullable │ │ │ ├── nullable.php │ │ │ └── mod.rs │ │ ├── types │ │ │ ├── mod.rs │ │ │ └── types.php │ │ ├── _utils.php │ │ ├── globals │ │ │ ├── globals.php │ │ │ └── mod.rs │ │ ├── binary │ │ │ ├── binary.php │ │ │ └── mod.rs │ │ ├── object │ │ │ ├── object.php │ │ │ └── mod.rs │ │ ├── exception │ │ │ ├── exception.php │ │ │ └── mod.rs │ │ ├── defaults │ │ │ ├── defaults.php │ │ │ └── mod.rs │ │ ├── iterator │ │ │ ├── iterator.php │ │ │ └── mod.rs │ │ ├── closure │ │ │ ├── closure.php │ │ │ └── mod.rs │ │ ├── number │ │ │ ├── number.php │ │ │ └── mod.rs │ │ ├── enum_ │ │ │ ├── enum.php │ │ │ └── mod.rs │ │ ├── magic_method │ │ │ ├── magic_method.php │ │ │ └── mod.rs │ │ ├── interface │ │ │ ├── mod.rs │ │ │ └── interface.php │ │ ├── array │ │ │ ├── mod.rs │ │ │ └── array.php │ │ ├── class │ │ │ └── class.php │ │ └── variadic_args │ │ │ ├── mod.rs │ │ │ └── variadic_args.php │ └── lib.rs ├── guide.rs ├── Cargo.toml ├── module.rs └── sapi.rs ├── .cargo └── config.toml ├── tools ├── update_bindings.sh └── update_lib_docs.sh ├── .lefthook.yml ├── flake.nix ├── LICENSE_MIT ├── Dockerfile ├── flake.lock ├── Cargo.toml ├── examples └── hello_world.rs ├── .release-plz.toml ├── unix_build.rs └── CONTRIBUTING.md /.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /guide/.gitignore: -------------------------------------------------------------------------------- 1 | book 2 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | wrap_comments = true -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | /target 2 | /guide 3 | -------------------------------------------------------------------------------- /crates/macros/README.md: -------------------------------------------------------------------------------- 1 | ../../README.md -------------------------------------------------------------------------------- /crates/cli/LICENSE_MIT: -------------------------------------------------------------------------------- 1 | ../../LICENSE_MIT -------------------------------------------------------------------------------- /crates/macros/LICENSE_MIT: -------------------------------------------------------------------------------- 1 | ../../LICENSE_MIT -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | liberapay: ext-php-rs 2 | -------------------------------------------------------------------------------- /crates/cli/LICENSE_APACHE: -------------------------------------------------------------------------------- 1 | ../../LICENSE_APACHE -------------------------------------------------------------------------------- /crates/macros/LICENSE_APACHE: -------------------------------------------------------------------------------- 1 | ../../LICENSE_APACHE -------------------------------------------------------------------------------- /crates/cli/allowed_bindings.rs: -------------------------------------------------------------------------------- 1 | ../../allowed_bindings.rs -------------------------------------------------------------------------------- /crates/cli/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | /.vscode 4 | /.idea 5 | /tmp 6 | expand.rs -------------------------------------------------------------------------------- /crates/macros/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | /.vscode 4 | /.idea 5 | /tmp 6 | expand.rs -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | /.vscode 4 | /.idea 5 | /tmp 6 | /.direnv 7 | expand.rs 8 | -------------------------------------------------------------------------------- /src/embed/test-script-exception.php: -------------------------------------------------------------------------------- 1 | $a, 'test') === 'test'); 4 | -------------------------------------------------------------------------------- /src/embed/test-script.php: -------------------------------------------------------------------------------- 1 | { 4 | pub prop: Property<'a, T>, 5 | pub flags: PropertyFlags, 6 | pub docs: DocComments, 7 | } 8 | -------------------------------------------------------------------------------- /tests/src/integration/_utils.php: -------------------------------------------------------------------------------- 1 | FunctionBuilder<'static>; 8 | } 9 | -------------------------------------------------------------------------------- /tests/src/integration/binary/binary.php: -------------------------------------------------------------------------------- 1 | string = 'string'; 5 | $obj->bool = true; 6 | $obj->number = 2022; 7 | $obj->array = [ 8 | 1, 2, 3 9 | ]; 10 | 11 | $test = test_object($obj); 12 | 13 | assert($test->string === 'string'); 14 | assert($test->bool === true); 15 | assert($test->number === 2022); 16 | assert($test->array === [1, 2, 3]); 17 | -------------------------------------------------------------------------------- /tools/update_bindings.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # This script updates the bindings for the docs.rs documentation. 3 | # It should be run from the root of the repository. 4 | # 5 | # This script requires docker to be installed and running. 6 | set -e 7 | 8 | docker buildx build \ 9 | --platform linux/amd64 \ 10 | --target docsrs_bindings \ 11 | -o type=local,dest=. \ 12 | --build-arg PHP_VERSION=8.5 \ 13 | . 14 | -------------------------------------------------------------------------------- /tests/src/integration/exception/exception.php: -------------------------------------------------------------------------------- 1 | throw_default_exception(), \Exception::class); 6 | 7 | try { 8 | throw_custom_exception(); 9 | } catch (\Throwable $e) { 10 | // Check if object is initiated 11 | assert($e instanceof \Test\TestException); 12 | assert("Not good custom!" === $e->getMessage()); 13 | } 14 | -------------------------------------------------------------------------------- /crates/macros/tests/expand/const.expanded.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate ext_php_rs_derive; 3 | const MY_CONST: &str = "Hello, world!"; 4 | #[allow(non_upper_case_globals)] 5 | const _internal_const_docs_MY_CONST: &[&str] = &[]; 6 | #[allow(non_upper_case_globals)] 7 | const _internal_const_name_MY_CONST: &str = "MY_CONST"; 8 | fn main() { 9 | (_internal_const_name_MY_CONST, MY_CONST, _internal_const_docs_MY_CONST); 10 | } 11 | -------------------------------------------------------------------------------- /src/embed/embed.h: -------------------------------------------------------------------------------- 1 | #include "zend.h" 2 | #include "sapi/embed/php_embed.h" 3 | #ifdef EXT_PHP_RS_PHP_82 4 | #include "php_ini_builder.h" 5 | #endif 6 | 7 | void* ext_php_rs_embed_callback(int argc, char** argv, void* (*callback)(void *), void *ctx); 8 | 9 | void ext_php_rs_sapi_startup(); 10 | void ext_php_rs_sapi_shutdown(); 11 | void ext_php_rs_sapi_per_thread_init(); 12 | 13 | void ext_php_rs_php_error(int type, const char *format, ...); 14 | -------------------------------------------------------------------------------- /tests/src/integration/bool/mod.rs: -------------------------------------------------------------------------------- 1 | use ext_php_rs::prelude::*; 2 | 3 | #[php_function] 4 | pub fn test_bool(a: bool) -> bool { 5 | a 6 | } 7 | 8 | pub fn build_module(builder: ModuleBuilder) -> ModuleBuilder { 9 | builder.function(wrap_function!(test_bool)) 10 | } 11 | 12 | #[cfg(test)] 13 | mod tests { 14 | #[test] 15 | fn bool_works() { 16 | assert!(crate::integration::test::run_php("bool/bool.php")); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/src/integration/defaults/defaults.php: -------------------------------------------------------------------------------- 1 | ) -> Binary { 5 | a 6 | } 7 | 8 | pub fn build_module(builder: ModuleBuilder) -> ModuleBuilder { 9 | builder.function(wrap_function!(test_binary)) 10 | } 11 | 12 | #[cfg(test)] 13 | mod tests { 14 | #[test] 15 | fn binary_works() { 16 | assert!(crate::integration::test::run_php("binary/binary.php")); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/src/integration/nullable/mod.rs: -------------------------------------------------------------------------------- 1 | use ext_php_rs::prelude::*; 2 | 3 | #[php_function] 4 | pub fn test_nullable(a: Option) -> Option { 5 | a 6 | } 7 | 8 | pub fn build_module(builder: ModuleBuilder) -> ModuleBuilder { 9 | builder.function(wrap_function!(test_nullable)) 10 | } 11 | 12 | #[cfg(test)] 13 | mod tests { 14 | #[test] 15 | fn nullable_works() { 16 | assert!(crate::integration::test::run_php("nullable/nullable.php")); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/src/integration/object/mod.rs: -------------------------------------------------------------------------------- 1 | use ext_php_rs::{prelude::*, types::ZendObject}; 2 | 3 | #[php_function] 4 | pub fn test_object(a: &mut ZendObject) -> &mut ZendObject { 5 | a 6 | } 7 | 8 | pub fn build_module(builder: ModuleBuilder) -> ModuleBuilder { 9 | builder.function(wrap_function!(test_object)) 10 | } 11 | 12 | #[cfg(test)] 13 | mod tests { 14 | #[test] 15 | fn object_works() { 16 | assert!(crate::integration::test::run_php("object/object.php")); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tests" 3 | version = "0.0.0" 4 | edition = "2024" 5 | publish = false 6 | license = "MIT OR Apache-2.0" 7 | 8 | [dependencies] 9 | ext-php-rs = { path = "../", default-features = false } 10 | 11 | [features] 12 | default = ["enum", "runtime", "closure"] 13 | enum = ["ext-php-rs/enum"] 14 | anyhow = ["ext-php-rs/anyhow"] 15 | runtime = ["ext-php-rs/runtime"] 16 | closure = ["ext-php-rs/closure"] 17 | static = ["ext-php-rs/static"] 18 | 19 | [lib] 20 | crate-type = ["cdylib"] 21 | -------------------------------------------------------------------------------- /tests/src/integration/iterator/iterator.php: -------------------------------------------------------------------------------- 1 | { 10 | cargo_php::stub_symbols!($($s),*); 11 | } 12 | } 13 | 14 | #[cfg(not(windows))] 15 | include!("../allowed_bindings.rs"); 16 | 17 | fn main() -> cargo_php::CrateResult { 18 | cargo_php::run() 19 | } 20 | -------------------------------------------------------------------------------- /tests/src/integration/callable/mod.rs: -------------------------------------------------------------------------------- 1 | use ext_php_rs::{prelude::*, types::Zval}; 2 | 3 | #[php_function] 4 | pub fn test_callable(call: ZendCallable, a: String) -> Zval { 5 | call.try_call(vec![&a]).expect("Failed to call function") 6 | } 7 | 8 | pub fn build_module(builder: ModuleBuilder) -> ModuleBuilder { 9 | builder.function(wrap_function!(test_callable)) 10 | } 11 | 12 | #[cfg(test)] 13 | mod tests { 14 | #[test] 15 | fn callable_works() { 16 | assert!(crate::integration::test::run_php("callable/callable.php")); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/zend/module.rs: -------------------------------------------------------------------------------- 1 | //! Builder and objects for creating modules in PHP. A module is the base of a 2 | //! PHP extension. 3 | 4 | use crate::ffi::zend_module_entry; 5 | 6 | /// A Zend module entry, also known as an extension. 7 | pub type ModuleEntry = zend_module_entry; 8 | 9 | impl ModuleEntry { 10 | /// Allocates the module entry on the heap, returning a pointer to the 11 | /// memory location. The caller is responsible for the memory pointed to. 12 | #[must_use] 13 | pub fn into_raw(self) -> *mut Self { 14 | Box::into_raw(Box::new(self)) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/src/integration/closure/closure.php: -------------------------------------------------------------------------------- 1 | getMessage(), 'take(): Argument #1 ($rs) must be of type stdClass, RustClosure given, called in ')); 22 | } 23 | -------------------------------------------------------------------------------- /tests/src/integration/string/mod.rs: -------------------------------------------------------------------------------- 1 | use ext_php_rs::prelude::*; 2 | 3 | #[php_function] 4 | pub fn test_str(a: &str) -> &str { 5 | a 6 | } 7 | 8 | #[php_function] 9 | pub fn test_string(a: String) -> String { 10 | a 11 | } 12 | 13 | pub fn build_module(builder: ModuleBuilder) -> ModuleBuilder { 14 | builder 15 | .function(wrap_function!(test_str)) 16 | .function(wrap_function!(test_string)) 17 | } 18 | 19 | #[cfg(test)] 20 | mod tests { 21 | #[test] 22 | fn string_works() { 23 | assert!(crate::integration::test::run_php("string/string.php")); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/src/integration/number/number.php: -------------------------------------------------------------------------------- 1 | test_number_unsigned(-12)); 14 | 15 | // Float 16 | assert(round(test_number_float(-1.2), 2) === round(-1.2, 2)); 17 | assert(round(test_number_float(0.0), 2) === round(0.0, 2)); 18 | assert(round(test_number_float(1.2), 2) === round(1.2, 2)); 19 | -------------------------------------------------------------------------------- /src/types/array/conversions/mod.rs: -------------------------------------------------------------------------------- 1 | //! Collection type conversions for `ZendHashTable`. 2 | //! 3 | //! This module provides conversions between Rust collection types and PHP arrays 4 | //! (represented as `ZendHashTable`). Each collection type has its own module for 5 | //! better organization and maintainability. 6 | //! 7 | //! ## Supported Collections 8 | //! 9 | //! - `BTreeMap` ↔ `ZendHashTable` (via `btree_map` module) 10 | //! - `HashMap` ↔ `ZendHashTable` (via `hash_map` module) 11 | //! - `Vec` and `Vec<(K, V)>` ↔ `ZendHashTable` (via `vec` module) 12 | 13 | mod btree_map; 14 | mod hash_map; 15 | mod vec; 16 | -------------------------------------------------------------------------------- /src/embed/sapi.rs: -------------------------------------------------------------------------------- 1 | //! Builder and objects for creating modules in PHP. A module is the base of a 2 | //! PHP extension. 3 | 4 | use crate::ffi::sapi_module_struct; 5 | 6 | /// A Zend module entry, also known as an extension. 7 | pub type SapiModule = sapi_module_struct; 8 | 9 | unsafe impl Send for SapiModule {} 10 | unsafe impl Sync for SapiModule {} 11 | 12 | impl SapiModule { 13 | /// Allocates the module entry on the heap, returning a pointer to the 14 | /// memory location. The caller is responsible for the memory pointed to. 15 | #[must_use] 16 | pub fn into_raw(self) -> *mut Self { 17 | Box::into_raw(Box::new(self)) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /crates/macros/src/fastcall.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Span, TokenStream}; 2 | use quote::ToTokens; 3 | use syn::{ItemFn, LitStr}; 4 | 5 | #[cfg(windows)] 6 | const ABI: &str = "vectorcall"; 7 | #[cfg(not(windows))] 8 | const ABI: &str = "C"; 9 | 10 | /// Parses a function and sets the correct ABI to interact with PHP depending 11 | /// on the OS. 12 | /// 13 | /// On Windows, this sets the extern ABI to vectorcall while on all other OS 14 | /// it sets it to C. 15 | pub fn parser(mut input: ItemFn) -> TokenStream { 16 | if let Some(abi) = &mut input.sig.abi { 17 | abi.name = Some(LitStr::new(ABI, Span::call_site())); 18 | } 19 | input.to_token_stream() 20 | } 21 | -------------------------------------------------------------------------------- /src/builders/mod.rs: -------------------------------------------------------------------------------- 1 | //! Structures that are used to construct other, more complicated types. 2 | //! Generally zero-cost abstractions. 3 | 4 | mod class; 5 | #[cfg(feature = "enum")] 6 | mod enum_builder; 7 | mod function; 8 | #[cfg(all(php82, feature = "embed"))] 9 | mod ini; 10 | mod module; 11 | #[cfg(feature = "embed")] 12 | mod sapi; 13 | 14 | pub use class::ClassBuilder; 15 | #[cfg(feature = "enum")] 16 | pub use enum_builder::EnumBuilder; 17 | pub use function::FunctionBuilder; 18 | #[cfg(all(php82, feature = "embed"))] 19 | pub use ini::IniBuilder; 20 | pub use module::{ModuleBuilder, ModuleStartup}; 21 | #[cfg(feature = "embed")] 22 | pub use sapi::SapiBuilder; 23 | -------------------------------------------------------------------------------- /.github/workflows/stale-issues.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues' 2 | on: 3 | schedule: 4 | - cron: '30 1 * * *' 5 | 6 | permissions: 7 | issues: write 8 | 9 | jobs: 10 | stale-issues: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/stale@v10 14 | with: 15 | days-before-issue-stale: -1 16 | days-before-issue-close: 7 17 | days-before-pr-stale: -1 18 | days-before-pr-close: -1 19 | stale-issue-label: 'waiting for feedback' 20 | close-issue-message: 'This issue has been automatically closed due to inactivity. Please feel free to reopen it if you are still experiencing the problem.' 21 | -------------------------------------------------------------------------------- /tests/src/integration/closure/mod.rs: -------------------------------------------------------------------------------- 1 | use ext_php_rs::prelude::*; 2 | 3 | #[php_function] 4 | pub fn test_closure() -> Closure { 5 | Closure::wrap(Box::new(|a| a) as Box String>) 6 | } 7 | 8 | #[php_function] 9 | pub fn test_closure_once(a: String) -> Closure { 10 | Closure::wrap_once(Box::new(move || a) as Box String>) 11 | } 12 | 13 | pub fn build_module(builder: ModuleBuilder) -> ModuleBuilder { 14 | builder 15 | .function(wrap_function!(test_closure)) 16 | .function(wrap_function!(test_closure_once)) 17 | } 18 | 19 | #[cfg(test)] 20 | mod tests { 21 | #[test] 22 | fn closure_works() { 23 | assert!(crate::integration::test::run_php("closure/closure.php")); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/src/integration/number/mod.rs: -------------------------------------------------------------------------------- 1 | use ext_php_rs::prelude::*; 2 | 3 | #[php_function] 4 | pub fn test_number_signed(a: i32) -> i32 { 5 | a 6 | } 7 | 8 | #[php_function] 9 | pub fn test_number_unsigned(a: u32) -> u32 { 10 | a 11 | } 12 | 13 | #[php_function] 14 | pub fn test_number_float(a: f32) -> f32 { 15 | a 16 | } 17 | 18 | pub fn build_module(builder: ModuleBuilder) -> ModuleBuilder { 19 | builder 20 | .function(wrap_function!(test_number_signed)) 21 | .function(wrap_function!(test_number_unsigned)) 22 | .function(wrap_function!(test_number_float)) 23 | } 24 | 25 | #[cfg(test)] 26 | mod tests { 27 | #[test] 28 | fn number_works() { 29 | assert!(crate::integration::test::run_php("number/number.php")); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.github/workflows/master.yml: -------------------------------------------------------------------------------- 1 | name: Master Branch Workflow 2 | on: 3 | push: 4 | branches: 5 | - master 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build_and_lint: 10 | name: Build and Lint 11 | uses: ./.github/workflows/build.yml 12 | release: 13 | name: Create Release (MR) 14 | needs: [build_and_lint] 15 | permissions: 16 | id-token: write 17 | contents: write 18 | pull-requests: write 19 | uses: ./.github/workflows/release-plz.yml 20 | secrets: 21 | RELEASE_PLZ_TOKEN: ${{ secrets.RELEASE_PLZ_TOKEN }} 22 | docs: 23 | name: Build and Deploy Documentation 24 | needs: [release] 25 | if: needs.release.outputs.release_created == 'true' 26 | permissions: 27 | contents: write 28 | uses: ./.github/workflows/docs.yml 29 | -------------------------------------------------------------------------------- /src/util/cstring_scope.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | ffi::{CString, NulError, c_char}, 3 | ops::Deref, 4 | }; 5 | 6 | // Helpful for CString which only needs to live until immediately after C call. 7 | pub struct CStringScope(*mut c_char); 8 | 9 | impl CStringScope { 10 | #[allow(dead_code)] 11 | pub fn new>>(string: T) -> Result { 12 | Ok(Self(CString::new(string)?.into_raw())) 13 | } 14 | } 15 | 16 | impl Deref for CStringScope { 17 | type Target = *mut c_char; 18 | 19 | fn deref(&self) -> &Self::Target { 20 | &self.0 21 | } 22 | } 23 | 24 | impl Drop for CStringScope { 25 | fn drop(&mut self) { 26 | // Convert back to a CString to ensure it gets dropped 27 | drop(unsafe { CString::from_raw(self.0) }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/docsrs_bindings.yml: -------------------------------------------------------------------------------- 1 | name: docsrs bindings 2 | on: 3 | pull_request: 4 | paths: 5 | - 'allowed-bindings.rs' 6 | - 'docsrs_bindings.rs' 7 | - 'Cargo.toml' 8 | - 'build.rs' 9 | - 'Dockerfile' 10 | - '.dockerignore' 11 | - 'tools/update_bindings.sh' 12 | - '.github/workflows/docsrs_bindings.yml' 13 | 14 | concurrency: 15 | group: ${{ github.workflow }}-${{ github.ref }} 16 | cancel-in-progress: true 17 | 18 | jobs: 19 | lint-bindings: 20 | name: Lint bindings 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v5 25 | with: 26 | fetch-depth: 0 27 | - name: Check docsrs_bindings.rs 28 | run: tools/update_bindings.sh && git diff --exit-code docsrs_bindings.rs 29 | -------------------------------------------------------------------------------- /crates/macros/tests/expand/enum.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate ext_php_rs_derive; 3 | 4 | /// Doc comments for MyEnum. 5 | /// This is a basic enum example. 6 | #[php_enum] 7 | enum MyEnum { 8 | /// Variant1 of MyEnum. 9 | /// This variant represents the first case. 10 | Variant1, 11 | #[php(name = "Variant_2")] 12 | Variant2, 13 | /// Variant3 of MyEnum. 14 | #[php(change_case = "UPPER_CASE")] 15 | Variant3, 16 | } 17 | 18 | #[php_enum] 19 | #[php(name = "MyIntValuesEnum")] 20 | enum MyEnumWithIntValues { 21 | #[php(value = 1)] 22 | Variant1, 23 | #[php(value = 42)] 24 | Variant2, 25 | } 26 | 27 | #[php_enum] 28 | #[php(change_case = "UPPER_CASE")] 29 | enum MyEnumWithStringValues { 30 | #[php(value = "foo")] 31 | Variant1, 32 | #[php(value = "bar")] 33 | Variant2, 34 | } 35 | -------------------------------------------------------------------------------- /crates/macros/tests/expand/interface.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate ext_php_rs_derive; 3 | 4 | /// Doc comments for MyInterface. 5 | /// This is a basic interface example. 6 | #[php_interface] 7 | trait MyInterface { 8 | /// Doc comments for MY_CONST. 9 | const MY_CONST: i32 = 42; 10 | /// Doc comments for my_method. 11 | /// This method does something. 12 | fn my_method(&self, arg: i32) -> String; 13 | } 14 | 15 | #[php_interface] 16 | #[php(change_method_case = "UPPER_CASE")] 17 | #[php(change_constant_case = "snake_case")] 18 | trait MyInterface2 { 19 | const MY_CONST: i32 = 42; 20 | #[php(change_case = "PascalCase")] 21 | const ANOTHER_CONST: &'static str = "Hello"; 22 | fn my_method(&self, arg: i32) -> String; 23 | #[php(change_case = "PascalCase")] 24 | fn anotherMethod(&self) -> i32; 25 | } 26 | -------------------------------------------------------------------------------- /tests/src/integration/enum_/enum.php: -------------------------------------------------------------------------------- 1 | value === 1); 11 | assert(IntBackedEnum::from(2) === IntBackedEnum::Variant2); 12 | assert(IntBackedEnum::tryFrom(1) === IntBackedEnum::Variant1); 13 | assert(IntBackedEnum::tryFrom(3) === null); 14 | 15 | assert(StringBackedEnum::Variant1->value === 'foo'); 16 | assert(StringBackedEnum::from('bar') === StringBackedEnum::Variant2); 17 | assert(StringBackedEnum::tryFrom('foo') === StringBackedEnum::Variant1); 18 | assert(StringBackedEnum::tryFrom('baz') === null); 19 | 20 | assert(test_enum(TestEnum::Variant1) === StringBackedEnum::Variant2); 21 | -------------------------------------------------------------------------------- /crates/macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ext-php-rs-derive" 3 | description = "Derive macros for ext-php-rs." 4 | repository = "https://github.com/extphprs/ext-php-rs" 5 | homepage = "https://ext-php.rs" 6 | license = "MIT OR Apache-2.0" 7 | version = "0.11.5" 8 | authors = [ 9 | "Xenira ", 10 | "David Cole ", 11 | "Pierre Tondereau ", 12 | ] 13 | edition = "2024" 14 | 15 | [lib] 16 | proc-macro = true 17 | 18 | [dependencies] 19 | syn = { version = "2.0.100", features = ["full", "extra-traits", "printing"] } 20 | darling = "0.21" 21 | quote = "1.0.9" 22 | proc-macro2 = "1.0.26" 23 | convert_case = "0.10.0" 24 | itertools = "0.14.0" 25 | 26 | [lints.rust] 27 | missing_docs = "warn" 28 | 29 | [dev-dependencies] 30 | glob = "0.3.2" 31 | macrotest = "1.1.0" 32 | runtime-macros = "1.1.1" 33 | -------------------------------------------------------------------------------- /.github/workflows/macro_docs.yml: -------------------------------------------------------------------------------- 1 | name: macro docs 2 | on: 3 | pull_request: 4 | paths: 5 | - "guide/src/macros/*.md" 6 | 7 | concurrency: 8 | group: ${{ github.workflow }}-${{ github.ref }} 9 | cancel-in-progress: true 10 | 11 | jobs: 12 | lint-bindings: 13 | name: Lint bindings 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v5 18 | with: 19 | fetch-depth: 0 20 | - name: Setup Rust 21 | uses: dtolnay/rust-toolchain@master 22 | with: 23 | components: rustfmt 24 | toolchain: nightly 25 | - name: Cache cargo dependencies 26 | uses: Swatinem/rust-cache@v2 27 | with: 28 | prefix-key: ${{ env.RUST_CACHE_PREFIX }} 29 | - name: Macro docs 30 | run: tools/update_lib_docs.sh && git diff --exit-code crates/macros/src/lib.rs 31 | -------------------------------------------------------------------------------- /src/embed/ffi.rs: -------------------------------------------------------------------------------- 1 | //! Raw FFI bindings to the Zend API. 2 | 3 | #![allow(clippy::all)] 4 | #![allow(warnings)] 5 | 6 | #[cfg(php82)] 7 | use crate::ffi::php_ini_builder; 8 | 9 | use std::ffi::{c_char, c_int, c_void}; 10 | 11 | #[link(name = "wrapper")] 12 | unsafe extern "C" { 13 | pub fn ext_php_rs_embed_callback( 14 | argc: c_int, 15 | argv: *mut *mut c_char, 16 | func: unsafe extern "C" fn(*const c_void) -> *const c_void, 17 | ctx: *const c_void, 18 | ) -> *mut c_void; 19 | 20 | pub fn ext_php_rs_sapi_startup(); 21 | pub fn ext_php_rs_sapi_shutdown(); 22 | pub fn ext_php_rs_sapi_per_thread_init(); 23 | 24 | pub fn ext_php_rs_php_error( 25 | type_: ::std::os::raw::c_int, 26 | error_msg: *const ::std::os::raw::c_char, 27 | ... 28 | ); 29 | 30 | #[cfg(php82)] 31 | pub fn ext_php_rs_php_ini_builder_deinit(builder: *mut php_ini_builder); 32 | } 33 | -------------------------------------------------------------------------------- /.lefthook.yml: -------------------------------------------------------------------------------- 1 | pre-commit: 2 | parallel: true 3 | jobs: 4 | - name: fmt 5 | run: rustfmt --edition 2021 {staged_files} 6 | glob: "*.rs" 7 | exclude: 8 | - "crates/macros/tests/expand/*.expanded.rs" 9 | stage_fixed: true 10 | - name: clippy 11 | run: cargo clippy --workspace --all-targets --features closure,embed,anyhow -- -W clippy::pedantic -D warnings 12 | glob: "*.rs" 13 | - name: bindings 14 | run: tools/update_bindings.sh && git diff --exit-code docsrs_bindings.rs 15 | glob: "allowed_bindings.rs" 16 | fail_text: | 17 | The `docsrs_bindings.rs` file seems to be out of date. 18 | Please check the updated bindings in `docsrs_bindings.rs` and commit the changes. 19 | - name: "macro docs" 20 | run: tools/update_lib_docs.sh 21 | glob: 22 | - "guide/src/macros/*.md" 23 | - "crates/macros/src/lib.rs" 24 | stage_fixed: true 25 | -------------------------------------------------------------------------------- /crates/cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cargo-php" 3 | description = "Installs extensions and generates stub files for PHP extensions generated with `ext-php-rs`." 4 | repository = "https://github.com/extphprs/ext-php-rs" 5 | homepage = "https://ext-php.rs" 6 | license = "MIT OR Apache-2.0" 7 | keywords = ["php", "ffi", "zend"] 8 | version = "0.1.14" 9 | authors = [ 10 | "Xenira ", 11 | "David Cole ", 12 | "Pierre Tondereau ", 13 | ] 14 | edition = "2024" 15 | categories = ["api-bindings", "command-line-interface"] 16 | 17 | [dependencies] 18 | ext-php-rs = { version = "0.15", default-features = false, path = "../../" } 19 | 20 | clap = { version = "4.0", features = ["derive"] } 21 | anyhow = "1" 22 | dialoguer = "0.12" 23 | libloading = "0.9" 24 | cargo_metadata = "0.23" 25 | semver = "1.0" 26 | 27 | [lints.rust] 28 | missing_docs = "warn" 29 | 30 | [features] 31 | default = ["enum"] 32 | enum = ["ext-php-rs/enum"] 33 | -------------------------------------------------------------------------------- /guide/src/types/functions.md: -------------------------------------------------------------------------------- 1 | # Functions & methods 2 | 3 | PHP functions and methods are represented by the `Function` struct. 4 | 5 | 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. 6 | It's heavily recommended you reuse returned `Function` objects, to avoid the overhead of looking up the function/method name. 7 | 8 | ```rust,no_run 9 | # #![cfg_attr(windows, feature(abi_vectorcall))] 10 | # extern crate ext_php_rs; 11 | use ext_php_rs::prelude::*; 12 | 13 | use ext_php_rs::zend::Function; 14 | 15 | #[php_function] 16 | pub fn test_function() -> () { 17 | let var_dump = Function::try_from_function("var_dump").unwrap(); 18 | let _ = var_dump.try_call(vec![&"abc"]); 19 | } 20 | 21 | #[php_function] 22 | pub fn test_method() -> () { 23 | let f = Function::try_from_method("ClassName", "staticMethod").unwrap(); 24 | let _ = f.try_call(vec![&"abc"]); 25 | } 26 | 27 | # fn main() {} 28 | ``` 29 | -------------------------------------------------------------------------------- /tests/src/integration/exception/mod.rs: -------------------------------------------------------------------------------- 1 | use ext_php_rs::{prelude::*, zend::ce}; 2 | 3 | #[php_class] 4 | #[php(name = "Test\\TestException")] 5 | #[php(extends(ce = ce::exception, stub = "\\Exception"))] 6 | #[derive(Debug)] 7 | pub struct TestException; 8 | 9 | #[php_function] 10 | pub fn throw_custom_exception() -> PhpResult { 11 | Err(PhpException::from_class::( 12 | "Not good custom!".into(), 13 | )) 14 | } 15 | 16 | #[php_function] 17 | pub fn throw_default_exception() -> PhpResult { 18 | Err(PhpException::default("Not good!".into())) 19 | } 20 | 21 | pub fn build_module(builder: ModuleBuilder) -> ModuleBuilder { 22 | builder 23 | .class::() 24 | .function(wrap_function!(throw_default_exception)) 25 | .function(wrap_function!(throw_custom_exception)) 26 | } 27 | 28 | #[cfg(test)] 29 | mod tests { 30 | #[test] 31 | fn exception_works() { 32 | assert!(crate::integration::test::run_php("exception/exception.php")); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/internal/mod.rs: -------------------------------------------------------------------------------- 1 | //! Internal, public functions that are called from downstream extensions. 2 | use parking_lot::{Mutex, const_mutex}; 3 | 4 | use crate::builders::ModuleStartup; 5 | 6 | pub mod class; 7 | pub mod function; 8 | pub mod property; 9 | 10 | /// A mutex type that contains a [`ModuleStartup`] instance. 11 | pub type ModuleStartupMutex = Mutex>; 12 | 13 | /// The initialisation value for [`ModuleStartupMutex`]. By default the mutex 14 | /// contains [`None`]. 15 | #[allow(clippy::declare_interior_mutable_const)] 16 | pub const MODULE_STARTUP_INIT: ModuleStartupMutex = const_mutex(None); 17 | 18 | /// Called by startup functions registered with the [`#[php_startup]`] macro. 19 | /// Initializes all classes that are defined by ext-php-rs (i.e. `Closure`). 20 | /// 21 | /// [`#[php_startup]`]: `crate::php_startup` 22 | // TODO: Measure this 23 | #[allow(clippy::inline_always)] 24 | #[inline(always)] 25 | pub fn ext_php_rs_startup() { 26 | #[cfg(feature = "closure")] 27 | crate::closure::Closure::build(); 28 | } 29 | -------------------------------------------------------------------------------- /src/test/mod.rs: -------------------------------------------------------------------------------- 1 | //! Utility functions for testing 2 | #![allow(clippy::must_use_candidate)] 3 | use crate::{ffi::_zend_execute_data, types::Zval, zend::ModuleEntry}; 4 | 5 | /// Dummy function for testing 6 | #[cfg(not(windows))] 7 | pub extern "C" fn test_function(_: &mut _zend_execute_data, _: &mut Zval) { 8 | // Dummy function for testing 9 | } 10 | 11 | /// Dummy function for testing on windows 12 | #[cfg(windows)] 13 | pub extern "vectorcall" fn test_function(_: &mut _zend_execute_data, _: &mut Zval) { 14 | // Dummy function for testing 15 | } 16 | 17 | /// Dummy function for testing 18 | pub extern "C" fn test_startup_shutdown_function(_type: i32, _module_number: i32) -> i32 { 19 | // Dummy function for testing 20 | 0 21 | } 22 | 23 | /// Dummy function for testing 24 | pub extern "C" fn test_info_function(_zend_module: *mut ModuleEntry) { 25 | // Dummy function for testing 26 | } 27 | 28 | /// Dummy function for testing 29 | pub extern "C" fn test_deactivate_function() -> i32 { 30 | // Dummy function for testing 31 | 0 32 | } 33 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | 13 | 14 | ## Checklist 15 | 16 | _Check the boxes that apply (put an `x` in the brackets, like `[x]`). You can also check boxes after the PR is created._ 17 | 18 | - [ ] I have read the [contribution guidelines](../CONTRIBUTING.md). 19 | - [ ] I have added tests that prove my code works as expected. 20 | - [ ] I have added documentation if applicable. 21 | - [ ] I have added a [migration guide](../CONTRIBUTING.md#breaking-changes) if applicable. 22 | 23 | :heart: Thank you for your contribution! 24 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "ext-php-rs dev environment"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; 6 | rust-overlay = { 7 | url = "github:oxalica/rust-overlay"; 8 | inputs = { 9 | nixpkgs.follows = "nixpkgs"; 10 | }; 11 | }; 12 | }; 13 | 14 | outputs = 15 | { nixpkgs, rust-overlay, ... }: 16 | let 17 | system = "x86_64-linux"; 18 | overlays = [ (import rust-overlay) ]; 19 | pkgs = import nixpkgs { inherit system overlays; }; 20 | php = pkgs.php.buildEnv { embedSupport = true; }; 21 | php-dev = php.unwrapped.dev; 22 | in 23 | { 24 | devShells.${system} = { 25 | default = pkgs.mkShell { 26 | buildInputs = with pkgs; [ 27 | php 28 | php-dev 29 | libclang.lib 30 | clang 31 | ]; 32 | 33 | nativeBuildInputs = [ pkgs.rust-bin.stable.latest.default ]; 34 | 35 | shellHook = '' 36 | export LIBCLANG_PATH="${pkgs.libclang.lib}/lib" 37 | ''; 38 | }; 39 | }; 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /tests/src/integration/defaults/mod.rs: -------------------------------------------------------------------------------- 1 | use ext_php_rs::prelude::*; 2 | 3 | #[php_function] 4 | #[php(defaults(a = 42))] 5 | pub fn test_defaults_integer(a: i32) -> i32 { 6 | a 7 | } 8 | 9 | #[php_function] 10 | #[php(defaults(a = None))] 11 | pub fn test_defaults_nullable_string(a: Option) -> Option { 12 | a 13 | } 14 | 15 | #[allow(clippy::unnecessary_wraps)] 16 | #[php_function] 17 | #[php(defaults(a = None, b = None))] 18 | pub fn test_defaults_multiple_option_arguments( 19 | a: Option, 20 | b: Option, 21 | ) -> PhpResult { 22 | Ok(a.or(b).unwrap_or_else(|| "Default".to_string())) 23 | } 24 | 25 | pub fn build_module(builder: ModuleBuilder) -> ModuleBuilder { 26 | builder 27 | .function(wrap_function!(test_defaults_integer)) 28 | .function(wrap_function!(test_defaults_nullable_string)) 29 | .function(wrap_function!(test_defaults_multiple_option_arguments)) 30 | } 31 | 32 | #[cfg(test)] 33 | mod tests { 34 | #[test] 35 | fn defaults_works() { 36 | assert!(crate::integration::test::run_php("defaults/defaults.php")); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/src/integration/magic_method/magic_method.php: -------------------------------------------------------------------------------- 1 | count = 10; 7 | // __get 8 | assert(10 === $magicMethod->count); 9 | assert(null === $magicMethod->test); 10 | 11 | //__isset 12 | assert(true === isset($magicMethod->count)); 13 | assert(false === isset($magicMethod->noCount)); 14 | 15 | // __unset 16 | unset($magicMethod->count); 17 | assert(0 === $magicMethod->count); 18 | 19 | // __toString 20 | assert("0" === $magicMethod->__toString()); 21 | assert("0" === (string) $magicMethod); 22 | 23 | // __invoke 24 | assert(34 === $magicMethod(34)); 25 | 26 | // __debugInfo 27 | $debug = print_r($magicMethod, true); 28 | $expectedDebug = "MagicMethod Object\n(\n [count] => 0\n)\n"; 29 | assert($expectedDebug === $debug); 30 | 31 | // __call 32 | assert("Hello" === $magicMethod->callMagicMethod(1, 2, 3)); 33 | assert(null === $magicMethod->callUndefinedMagicMethod()); 34 | 35 | // __call_static 36 | assert("Hello from static call 1, 2, 3" === MagicMethod::callStaticSomeMagic(1, 2, 3)); 37 | assert(null === MagicMethod::callUndefinedStaticSomeMagic()); 38 | -------------------------------------------------------------------------------- /.github/action/musl/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG PHP_VERSION=8.5 2 | ARG TS=-zts 3 | 4 | FROM php:${PHP_VERSION}${TS}-alpine3.22 5 | 6 | RUN apk add --no-cache \ 7 | llvm17 \ 8 | llvm17-dev \ 9 | llvm17-libs \ 10 | llvm17-static \ 11 | clang17 \ 12 | clang17-dev \ 13 | clang17-static \ 14 | curl 15 | 16 | RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable 17 | ENV PATH="/root/.cargo/bin:${PATH}" 18 | 19 | RUN rustup target add x86_64-unknown-linux-musl 20 | RUN cargo install cargo-expand --locked 21 | 22 | ENV PHP=/usr/local/bin/php 23 | ENV PHP_CONFIG=/usr/local/bin/php-config 24 | 25 | ENV LLVM_CONFIG_PATH=/usr/lib/llvm17/bin/llvm-config 26 | ENV LIBCLANG_PATH=/usr/lib/llvm17/lib 27 | ENV LD_LIBRARY_PATH=/usr/local/lib:/usr/lib 28 | ENV RUSTFLAGS="-C target-feature=-crt-static -C link-arg=-Wl,-rpath,/usr/local/lib -L /usr/local/lib" 29 | 30 | WORKDIR /workspace 31 | 32 | COPY . . 33 | 34 | ENTRYPOINT ["cargo"] 35 | CMD ["build", "--release", "--no-default-features", "--features", "closure,anyhow,runtime,enum", "--workspace", "--target", "x86_64-unknown-linux-musl"] 36 | -------------------------------------------------------------------------------- /tests/src/integration/interface/mod.rs: -------------------------------------------------------------------------------- 1 | use ext_php_rs::php_interface; 2 | use ext_php_rs::prelude::ModuleBuilder; 3 | use ext_php_rs::types::ZendClassObject; 4 | use ext_php_rs::zend::ce; 5 | 6 | #[php_interface] 7 | #[php(extends(ce = ce::throwable, stub = "\\Throwable"))] 8 | #[php(name = "ExtPhpRs\\Interface\\EmptyObjectInterface")] 9 | #[allow(dead_code)] 10 | pub trait EmptyObjectTrait { 11 | const STRING_CONST: &'static str = "STRING_CONST"; 12 | 13 | const USIZE_CONST: u64 = 200; 14 | 15 | fn void(); 16 | 17 | fn non_static(&self, data: String) -> String; 18 | 19 | fn ref_to_like_this_class( 20 | &self, 21 | data: String, 22 | other: &ZendClassObject, 23 | ) -> String; 24 | 25 | #[php(defaults(value = 0))] 26 | fn set_value(&mut self, value: i32); 27 | } 28 | 29 | pub fn build_module(builder: ModuleBuilder) -> ModuleBuilder { 30 | builder.interface::() 31 | } 32 | 33 | #[cfg(test)] 34 | mod tests { 35 | #[test] 36 | fn interface_work() { 37 | assert!(crate::integration::test::run_php("interface/interface.php")); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/types/iterator.test.php: -------------------------------------------------------------------------------- 1 | new class {}; 8 | } 9 | 10 | class TestIterator implements \Iterator { 11 | private $count = 0; 12 | 13 | public function current(): mixed 14 | { 15 | return match ($this->count) { 16 | 0 => 'foo', 17 | 1 => 'bar', 18 | 2 => 'baz', 19 | 3 => new class {}, 20 | default => null, 21 | }; 22 | } 23 | 24 | public function next(): void 25 | { 26 | $this->count++; 27 | } 28 | 29 | public function key(): mixed 30 | { 31 | return match ($this->count) { 32 | 0 => 'key', 33 | 1 => 10, 34 | 2 => 2, 35 | 3 => new class {}, 36 | default => null, 37 | }; 38 | } 39 | 40 | public function valid(): bool 41 | { 42 | return $this->count < 4; 43 | } 44 | 45 | public function rewind(): void 46 | { 47 | $this->count = 0; 48 | } 49 | } 50 | 51 | $generator = create_generator(); 52 | $iterator = new TestIterator(); 53 | -------------------------------------------------------------------------------- /LICENSE_MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 David Cole and all contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /guide/src/advanced/allowed_bindings.md: -------------------------------------------------------------------------------- 1 | # Allowed Bindings 2 | 3 | The extension limits the bindings that are generated by `bindgen` to a subset of the original bindings. 4 | Those bindings are defined in the `allowed_bindings.rs` file. 5 | 6 | Should you need to add more bindings, you can do so by defining them as a comma-separated list in the `EXT_PHP_RS_ALLOWED_BINDINGS` environment variable. 7 | 8 | This can be configured in your `.cargo/config.toml` file: 9 | 10 | ```toml 11 | [env] 12 | EXT_PHP_RS_ALLOWED_BINDINGS = "php_foo,php_bar" 13 | ``` 14 | 15 | Your bindings should now appear in the `ext_php_rs::ffi` module. 16 | 17 |
18 | Pay attention to the PHP version 19 | 20 | Be aware, that bindings that do not exist in the PHP version you are targeting will not be available. 21 | 22 | Some bindings may also change between PHP versions, so make sure to test your extension with all PHP versions you are targeting. 23 |
24 | 25 | ## Contributing 26 | 27 | If you think that a binding should be added to the allowed bindings, please open an issue or a pull request on the [GitHub repository](https://github.com/extphprs/ext-php-rs) so that everyone can benefit from it. 28 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:latest AS base 2 | ARG PHP_VERSION=8.5 3 | WORKDIR /tmp 4 | RUN < Result> { 7 | attrs 8 | .iter() 9 | .filter(|attr| attr.path().is_ident("doc")) 10 | .map(|attr| { 11 | let Meta::NameValue(meta) = &attr.meta else { 12 | bail!(attr.meta => "Invalid format for `#[doc]` attribute."); 13 | }; 14 | 15 | let Expr::Lit(value) = &meta.value else { 16 | bail!(attr.meta => "Invalid format for `#[doc]` attribute."); 17 | }; 18 | 19 | let Lit::Str(doc) = &value.lit else { 20 | bail!(value.lit => "Invalid format for `#[doc]` attribute."); 21 | }; 22 | 23 | Ok(doc.value()) 24 | }) 25 | .collect::>>() 26 | } 27 | 28 | pub trait CleanPhpAttr { 29 | fn clean_php(&mut self); 30 | } 31 | 32 | impl CleanPhpAttr for Vec { 33 | fn clean_php(&mut self) { 34 | self.retain(|attr| !attr.path().is_ident("php")); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/types/mod.rs: -------------------------------------------------------------------------------- 1 | //! Types defined by the Zend engine used in PHP. 2 | //! 3 | //! Generally, it is easier to work directly with Rust types, converting into 4 | //! these PHP types when required. 5 | 6 | mod array; 7 | mod callable; 8 | mod class_object; 9 | mod iterable; 10 | mod iterator; 11 | mod long; 12 | mod object; 13 | mod string; 14 | mod zval; 15 | 16 | pub use array::{ArrayKey, ZendHashTable}; 17 | pub use callable::ZendCallable; 18 | pub use class_object::ZendClassObject; 19 | pub use iterable::Iterable; 20 | pub use iterator::ZendIterator; 21 | pub use long::ZendLong; 22 | pub use object::{PropertyQuery, ZendObject}; 23 | pub use string::ZendStr; 24 | pub use zval::Zval; 25 | 26 | use crate::{convert::FromZval, flags::DataType, macros::into_zval}; 27 | 28 | into_zval!(f32, set_double, Double); 29 | into_zval!(f64, set_double, Double); 30 | into_zval!(bool, set_bool, Bool); 31 | 32 | try_from_zval!(f64, double, Double); 33 | try_from_zval!(bool, bool, Bool); 34 | 35 | impl FromZval<'_> for f32 { 36 | const TYPE: DataType = DataType::Double; 37 | 38 | fn from_zval(zval: &Zval) -> Option { 39 | #[allow(clippy::cast_possible_truncation)] 40 | zval.double().map(|v| v as f32) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/02_feature_request.yml: -------------------------------------------------------------------------------- 1 | name: 🚀 Feature request 2 | description: Suggest an idea for this project 3 | labels: ["enhancement"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thanks for taking the time to suggest a feature! Please fill out the following sections to help us understand your request. 9 | - type: textarea 10 | attributes: 11 | label: Description 12 | description: "Please provide a clear and concise description of the feature you would like to see." 13 | value: | 14 | ## Description 15 | 16 | 17 | ## Use Case 18 | 19 | 20 | ## Example 21 | 22 | validations: 23 | required: true 24 | - type: checkboxes 25 | attributes: 26 | label: Additional Context 27 | description: "Please select any additional context that applies to your feature request." 28 | options: 29 | - label: "I can try implementing this feature myself (please assign me to this issue)" 30 | -------------------------------------------------------------------------------- /tests/src/integration/interface/interface.php: -------------------------------------------------------------------------------- 1 | nonStatic($data), $other->nonStatic($data)); 26 | } 27 | 28 | public function setValue(?int $value = 0) { 29 | 30 | } 31 | } 32 | $f = new Test(); 33 | 34 | assert(is_a($f, Throwable::class)); 35 | assert($f->nonStatic('Rust') === 'Rust - TEST'); 36 | assert($f->refToLikeThisClass('TEST', $f) === 'TEST - TEST | TEST - TEST'); 37 | assert(ExtPhpRs\Interface\EmptyObjectInterface::STRING_CONST === 'STRING_CONST'); 38 | assert(ExtPhpRs\Interface\EmptyObjectInterface::USIZE_CONST === 200); 39 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1762977756, 6 | "narHash": "sha256-4PqRErxfe+2toFJFgcRKZ0UI9NSIOJa+7RXVtBhy4KE=", 7 | "owner": "nixos", 8 | "repo": "nixpkgs", 9 | "rev": "c5ae371f1a6a7fd27823bc500d9390b38c05fa55", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "nixos", 14 | "ref": "nixos-unstable", 15 | "repo": "nixpkgs", 16 | "type": "github" 17 | } 18 | }, 19 | "root": { 20 | "inputs": { 21 | "nixpkgs": "nixpkgs", 22 | "rust-overlay": "rust-overlay" 23 | } 24 | }, 25 | "rust-overlay": { 26 | "inputs": { 27 | "nixpkgs": [ 28 | "nixpkgs" 29 | ] 30 | }, 31 | "locked": { 32 | "lastModified": 1763087910, 33 | "narHash": "sha256-eB9Z1mWd1U6N61+F8qwDggX0ihM55s4E0CluwNukJRU=", 34 | "owner": "oxalica", 35 | "repo": "rust-overlay", 36 | "rev": "cf4a68749733d45c0420726596367acd708eb2e8", 37 | "type": "github" 38 | }, 39 | "original": { 40 | "owner": "oxalica", 41 | "repo": "rust-overlay", 42 | "type": "github" 43 | } 44 | } 45 | }, 46 | "root": "root", 47 | "version": 7 48 | } 49 | -------------------------------------------------------------------------------- /crates/cli/src/ext.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use anyhow::{Context, Result}; 4 | use ext_php_rs::describe::Description; 5 | use libloading::os::unix::{Library, Symbol}; 6 | 7 | #[allow(improper_ctypes_definitions)] 8 | pub struct Ext { 9 | // These need to be here to keep the libraries alive. The extension library needs to be alive 10 | // to access the describe function. Missing here is the lifetime on `Symbol<'a, fn() -> 11 | // Module>` where `ext_lib: 'a`. 12 | #[allow(dead_code)] 13 | ext_lib: Library, 14 | describe_fn: Symbol Description>, 15 | } 16 | 17 | impl Ext { 18 | /// Loads an extension. 19 | pub fn load(ext_path: PathBuf) -> Result { 20 | let ext_lib = unsafe { Library::new(ext_path) } 21 | .with_context(|| "Failed to load extension library")?; 22 | 23 | let describe_fn = unsafe { 24 | ext_lib 25 | .get(b"ext_php_rs_describe_module") 26 | .with_context(|| "Failed to load describe function symbol from extension library")? 27 | }; 28 | 29 | Ok(Self { 30 | ext_lib, 31 | describe_fn, 32 | }) 33 | } 34 | 35 | /// Describes the extension. 36 | pub fn describe(&self) -> Description { 37 | (self.describe_fn)() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /guide/src/macros/index.md: -------------------------------------------------------------------------------- 1 | # Macros 2 | 3 | `ext-php-rs` comes with a set of macros that are used to annotate types which 4 | are to be exported to PHP. This allows you to write Rust-like APIs that can be 5 | used from PHP without fiddling around with zvals. 6 | 7 | - [`php_module`] - Defines the function used by PHP to retrieve your extension. 8 | - [`php_startup`] - Defines the extension startup function used by PHP to 9 | initialize your extension. 10 | - [`php_function`] - Used to export a Rust function to PHP. 11 | - [`php_class`] - Used to export a Rust struct or enum as a PHP class. 12 | - [`php_impl`] - Used to export a Rust `impl` block to PHP, including all 13 | methods and constants. 14 | - [`php_const`] - Used to export a Rust constant to PHP as a global constant. 15 | - [`php_extern`] - Attribute used to annotate `extern` blocks which are deemed as 16 | PHP functions. 17 | - [`php_interface`] - Attribute used to export Rust Trait as PHP interface 18 | - [`php`] - Used to modify the default behavior of the above macros. This is a 19 | generic attribute that can be used on most of the above macros. 20 | 21 | [`php_module`]: ./module.md 22 | [`php_function`]: ./function.md 23 | [`php_class`]: ./classes.md 24 | [`php_impl`]: ./impl.md 25 | [`php_const`]: ./constant.md 26 | [`php_extern`]: ./extern.md 27 | [`php_interface`]: ./interface.md 28 | [`php`]: ./php.md 29 | -------------------------------------------------------------------------------- /guide/src/types/vec.md: -------------------------------------------------------------------------------- 1 | # `Vec` 2 | 3 | Vectors can contain any type that can be represented as a zval. Note that the 4 | data contained in the array will be copied into Rust types and stored inside the 5 | vector. The internal representation of a PHP array is discussed below. 6 | 7 | | `T` parameter | `&T` parameter | `T` Return type | `&T` Return type | PHP representation | 8 | | ------------- | -------------- | --------------- | ---------------- | ------------------ | 9 | | Yes | No | Yes | No | `ZendHashTable` | 10 | 11 | Internally, PHP arrays are hash tables where the key can be an unsigned long or 12 | a string. Zvals are contained inside arrays therefore the data does not have to 13 | contain only one type. 14 | 15 | When converting into a vector, all values are converted from zvals into the 16 | given generic type. If any of the conversions fail, the whole conversion will 17 | fail. 18 | 19 | ## Rust example 20 | 21 | ```rust,no_run 22 | # #![cfg_attr(windows, feature(abi_vectorcall))] 23 | # extern crate ext_php_rs; 24 | # use ext_php_rs::prelude::*; 25 | #[php_function] 26 | pub fn test_vec(vec: Vec) -> String { 27 | vec.join(" ") 28 | } 29 | # fn main() {} 30 | ``` 31 | 32 | ## PHP example 33 | 34 | ```php 35 | String { 25 | format!("Hello {}", input) 26 | } 27 | # fn main() {} 28 | ``` 29 | 30 | ## PHP example 31 | 32 | ```php 33 | 16 | 17 | ## Example 18 | 19 | **Extension Code:** 20 | ```rs 21 | ``` 22 | 23 | **PHP Code:** 24 | ```php 25 | ` 2 | 3 | Options are used for optional and nullable parameters, as well as null returns. 4 | It is valid to be converted to/from a zval as long as the underlying `T` generic 5 | is also able to be converted to/from a zval. 6 | 7 | | `T` parameter | `&T` parameter | `T` Return type | `&T` Return type | PHP representation | 8 | | ------------- | -------------- | --------------- | ---------------- | ---------------------------------- | 9 | | Yes | No | Yes | No | Depends on `T`, `null` for `None`. | 10 | 11 | Using `Option` as a parameter indicates that the parameter is nullable. If 12 | null is passed, a `None` value will be supplied. It is also used in the place of 13 | optional parameters. If the parameter is not given, a `None` value will also be 14 | supplied. 15 | 16 | Returning `Option` is a nullable return type. Returning `None` will return 17 | null to PHP. 18 | 19 | ## Rust example 20 | 21 | ```rust,no_run 22 | # #![cfg_attr(windows, feature(abi_vectorcall))] 23 | # extern crate ext_php_rs; 24 | # use ext_php_rs::prelude::*; 25 | #[php_function] 26 | pub fn test_option_null(input: Option) -> Option { 27 | input.map(|input| format!("Hello {}", input).into()) 28 | } 29 | # fn main() {} 30 | ``` 31 | 32 | ## PHP example 33 | 34 | ```php 35 | (&self) -> ZendLinkedListIterator<'_, T> { 12 | ZendLinkedListIterator::new(self) 13 | } 14 | } 15 | 16 | pub struct ZendLinkedListIterator<'a, T> { 17 | list: &'a zend_llist, 18 | position: *mut zend_llist_element, 19 | _marker: PhantomData, 20 | } 21 | 22 | impl<'a, T> ZendLinkedListIterator<'a, T> { 23 | fn new(list: &'a ZendLinkedList) -> Self { 24 | ZendLinkedListIterator { 25 | list, 26 | position: list.head, 27 | _marker: PhantomData, 28 | } 29 | } 30 | } 31 | 32 | impl<'a, T: 'a> Iterator for ZendLinkedListIterator<'a, T> { 33 | type Item = &'a T; 34 | 35 | fn next(&mut self) -> Option { 36 | if self.position.is_null() { 37 | return None; 38 | } 39 | let ptr = unsafe { (*self.position).data.as_mut_ptr() }; 40 | let value = unsafe { &*((ptr as *const T).cast_mut()) }; 41 | unsafe { 42 | zend_llist_get_next_ex( 43 | ptr::from_ref::(self.list).cast_mut(), 44 | &raw mut self.position, 45 | ) 46 | }; 47 | Some(value) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /guide/src/types/str.md: -------------------------------------------------------------------------------- 1 | # `&str` 2 | 3 | A borrowed string. When this type is encountered, you are given a reference to 4 | the actual zend string memory, rather than copying the contents like if you were 5 | taking an owned `String` argument. 6 | 7 | | `T` parameter | `&T` parameter | `T` Return type | `&T` Return type | PHP representation | 8 | | ------------- | -------------- | --------------- | ---------------- | ------------------------ | 9 | | No | Yes | No | Yes | `zend_string` (C-string) | 10 | 11 | Note that you cannot expect the function to operate the same by swapping out 12 | `String` and `&str` - since the zend string memory is read directly, this 13 | library does not attempt to parse `double` types as strings. 14 | 15 | See the [`String`](./string.md) for a deeper dive into the internal structure of 16 | PHP strings. 17 | 18 | ## Rust example 19 | 20 | ```rust,no_run 21 | # #![cfg_attr(windows, feature(abi_vectorcall))] 22 | # extern crate ext_php_rs; 23 | # use ext_php_rs::prelude::*; 24 | #[php_function] 25 | pub fn str_example(input: &str) -> String { 26 | format!("Hello {}", input) 27 | } 28 | 29 | #[php_function] 30 | pub fn str_return_example() -> &'static str { 31 | "Hello from Rust" 32 | } 33 | # fn main() {} 34 | ``` 35 | 36 | ## PHP example 37 | 38 | ```php 39 | ModuleBuilder { 35 | module 36 | .constant(wrap_constant!(TEST_CONSTANT)) 37 | .constant(wrap_constant!(TEST_CONSTANT_THE_SECOND)) 38 | .constant(("MANUAL_CONSTANT", ANOTHER_STRING_CONST, &[])) 39 | } 40 | # fn main() {} 41 | ``` 42 | 43 | ## PHP usage 44 | 45 | ```php 46 | ModuleBuilder { 14 | let mut module = integration::array::build_module(module); 15 | module = integration::binary::build_module(module); 16 | module = integration::bool::build_module(module); 17 | module = integration::callable::build_module(module); 18 | module = integration::class::build_module(module); 19 | module = integration::closure::build_module(module); 20 | module = integration::defaults::build_module(module); 21 | #[cfg(feature = "enum")] 22 | { 23 | module = integration::enum_::build_module(module); 24 | } 25 | module = integration::exception::build_module(module); 26 | module = integration::globals::build_module(module); 27 | module = integration::iterator::build_module(module); 28 | module = integration::magic_method::build_module(module); 29 | module = integration::nullable::build_module(module); 30 | module = integration::number::build_module(module); 31 | module = integration::object::build_module(module); 32 | module = integration::string::build_module(module); 33 | module = integration::variadic_args::build_module(module); 34 | module = integration::interface::build_module(module); 35 | 36 | module 37 | } 38 | -------------------------------------------------------------------------------- /src/embed/embed.c: -------------------------------------------------------------------------------- 1 | #include "embed.h" 2 | 3 | // We actually use the PHP embed API to run PHP code in test 4 | // At some point we might want to use our own SAPI to do that 5 | void* ext_php_rs_embed_callback(int argc, char** argv, void* (*callback)(void *), void *ctx) { 6 | void *result = NULL; 7 | 8 | PHP_EMBED_START_BLOCK(argc, argv) 9 | 10 | result = callback(ctx); 11 | 12 | PHP_EMBED_END_BLOCK() 13 | 14 | return result; 15 | } 16 | 17 | void ext_php_rs_sapi_startup() { 18 | #if defined(SIGPIPE) && defined(SIG_IGN) 19 | signal(SIGPIPE, SIG_IGN); 20 | #endif 21 | 22 | #ifdef ZTS 23 | php_tsrm_startup(); 24 | #ifdef PHP_WIN32 25 | ZEND_TSRMLS_CACHE_UPDATE(); 26 | #endif 27 | #endif 28 | 29 | zend_signal_startup(); 30 | } 31 | 32 | SAPI_API void ext_php_rs_sapi_shutdown() { 33 | #ifdef ZTS 34 | tsrm_shutdown(); 35 | #endif 36 | } 37 | 38 | SAPI_API void ext_php_rs_sapi_per_thread_init() { 39 | #ifdef ZTS 40 | (void)ts_resource(0); 41 | #ifdef PHP_WIN32 42 | ZEND_TSRMLS_CACHE_UPDATE(); 43 | #endif 44 | #endif 45 | } 46 | 47 | void ext_php_rs_php_error(int type, const char *format, ...) { 48 | va_list args; 49 | va_start(args, format); 50 | php_error(type, format, args); 51 | vprintf(format, args); 52 | va_end(args); 53 | } 54 | 55 | // Wrap `php_ini_builder_deinit` as it's `static inline` which gets discarded 56 | // by cbindgen. 57 | #ifdef EXT_PHP_RS_PHP_82 58 | void ext_php_rs_php_ini_builder_deinit(struct php_ini_builder *b) { 59 | php_ini_builder_deinit(b); 60 | } 61 | #endif 62 | -------------------------------------------------------------------------------- /guide/src/types/iterable.md: -------------------------------------------------------------------------------- 1 | # `Iterable` 2 | 3 | `Iterable`s are represented either by an `array` or `Traversable` type. 4 | 5 | | `T` parameter | `&T` parameter | `T` Return type | `&T` Return type | PHP representation | 6 | |---------------|----------------|-----------------| ---------------- |----------------------------------| 7 | | Yes | No | No | No | `ZendHashTable` or `ZendIterator` | 8 | 9 | Converting from a zval to a `Iterable` is valid when the value is either an array or an object 10 | that implements the `Traversable` interface. This means that any value that can be used in a 11 | `foreach` loop can be converted into a `Iterable`. 12 | 13 | ## Rust example 14 | 15 | ```rust,no_run 16 | # #![cfg_attr(windows, feature(abi_vectorcall))] 17 | # extern crate ext_php_rs; 18 | # use ext_php_rs::prelude::*; 19 | # use ext_php_rs::types::Iterable; 20 | #[php_function] 21 | pub fn test_iterable(mut iterable: Iterable) { 22 | for (k, v) in iterable.iter().expect("cannot rewind iterator") { 23 | println!("k: {} v: {}", k.string().unwrap(), v.string().unwrap()); 24 | } 25 | } 26 | # fn main() {} 27 | ``` 28 | 29 | ## PHP example 30 | 31 | ```php 32 | 'world'; 36 | yield 'rust' => 'php'; 37 | }; 38 | 39 | $array = [ 40 | 'hello' => 'world', 41 | 'rust' => 'php', 42 | ]; 43 | 44 | test_iterable($generator()); 45 | test_iterable($array); 46 | ``` 47 | 48 | Output: 49 | 50 | ```text 51 | k: hello v: world 52 | k: rust v: php 53 | k: hello v: world 54 | k: rust v: php 55 | ``` 56 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Deploy documentation 2 | on: 3 | workflow_call: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | docs: 8 | name: Build and Deploy 9 | runs-on: ${{ matrix.os }} 10 | strategy: 11 | matrix: 12 | os: ["ubuntu-latest"] 13 | php: ["8.2"] 14 | clang: ["17"] 15 | mdbook: ["latest"] 16 | permissions: 17 | contents: write 18 | steps: 19 | - name: Checkout code 20 | uses: actions/checkout@v5 21 | - name: Setup PHP 22 | uses: shivammathur/setup-php@v2 23 | with: 24 | php-version: ${{ matrix.php }} 25 | - name: Setup Rust 26 | uses: dtolnay/rust-toolchain@nightly 27 | - name: Cache LLVM and Clang 28 | id: cache-llvm 29 | uses: actions/cache@v4 30 | with: 31 | path: ${{ runner.temp }}/llvm-${{ matrix.clang }} 32 | key: ${{ matrix.os }}-llvm-${{ matrix.clang }} 33 | - name: Setup LLVM & Clang 34 | uses: KyleMayes/install-llvm-action@v2 35 | with: 36 | version: ${{ matrix.clang }} 37 | directory: ${{ runner.temp }}/llvm-${{ matrix.clang }} 38 | cached: ${{ steps.cache-llvm.outputs.cache-hit }} 39 | - name: Install mdbook 40 | uses: peaceiris/actions-mdbook@v2 41 | with: 42 | mdbook-version: ${{ matrix.mdbook }} 43 | - name: Build guide 44 | run: mdbook build guide 45 | - name: Publish docs 46 | uses: JamesIves/github-pages-deploy-action@v4.7.4 47 | with: 48 | branch: gh-pages 49 | folder: guide/book 50 | clean: true 51 | -------------------------------------------------------------------------------- /guide/src/types/bool.md: -------------------------------------------------------------------------------- 1 | # `bool` 2 | 3 | A boolean. Not much else to say here. 4 | 5 | | `T` parameter | `&T` parameter | `T` Return type | `&T` Return type | PHP representation | 6 | | ------------- | -------------- | --------------- | ---------------- | ------------------ | 7 | | Yes | No | Yes | No | Union flag | 8 | 9 | Booleans are not actually stored inside the zval. Instead, they are treated as 10 | two different union types (the zval can be in a true or false state). An 11 | equivalent structure in Rust would look like: 12 | 13 | ```rs 14 | enum Zval { 15 | True, 16 | False, 17 | String(&mut ZendString), 18 | Long(i64), 19 | // ... 20 | } 21 | ``` 22 | 23 | ## Rust example 24 | 25 | ```rust,no_run 26 | # #![cfg_attr(windows, feature(abi_vectorcall))] 27 | # extern crate ext_php_rs; 28 | # use ext_php_rs::prelude::*; 29 | #[php_function] 30 | pub fn test_bool(input: bool) -> String { 31 | if input { 32 | "Yes!".into() 33 | } else { 34 | "No!".into() 35 | } 36 | } 37 | # fn main() {} 38 | ``` 39 | 40 | ## PHP example 41 | 42 | ```php 43 | Result { 34 | let str: &str = StringBackedEnum::Variant2.into(); 35 | match a { 36 | TestEnum::Variant1 => str.try_into(), 37 | TestEnum::Variant2 => Ok(StringBackedEnum::Variant1), 38 | } 39 | } 40 | 41 | pub fn build_module(builder: ModuleBuilder) -> ModuleBuilder { 42 | builder 43 | .enumeration::() 44 | .enumeration::() 45 | .enumeration::() 46 | .function(wrap_function!(test_enum)) 47 | } 48 | 49 | #[cfg(test)] 50 | mod tests { 51 | #[test] 52 | fn enum_works() { 53 | assert!(crate::integration::test::run_php("enum_/enum.php")); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/src/integration/array/mod.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{BTreeMap, HashMap}; 2 | 3 | use ext_php_rs::{ 4 | convert::IntoZval, 5 | ffi::HashTable, 6 | php_function, 7 | prelude::ModuleBuilder, 8 | types::{ArrayKey, Zval}, 9 | wrap_function, 10 | }; 11 | 12 | #[php_function] 13 | pub fn test_array(a: Vec) -> Vec { 14 | a 15 | } 16 | 17 | #[php_function] 18 | pub fn test_array_assoc(a: HashMap) -> HashMap { 19 | a 20 | } 21 | 22 | #[php_function] 23 | pub fn test_array_assoc_array_keys(a: Vec<(ArrayKey, String)>) -> Vec<(ArrayKey, String)> { 24 | a 25 | } 26 | 27 | #[php_function] 28 | pub fn test_btree_map(a: BTreeMap) -> BTreeMap { 29 | a 30 | } 31 | 32 | #[php_function] 33 | pub fn test_array_keys() -> Zval { 34 | let mut ht = HashTable::new(); 35 | ht.insert(-42, "foo").unwrap(); 36 | ht.insert(0, "bar").unwrap(); 37 | ht.insert(5, "baz").unwrap(); 38 | ht.insert("10", "qux").unwrap(); 39 | ht.insert("quux", "quuux").unwrap(); 40 | 41 | ht.into_zval(false).unwrap() 42 | } 43 | 44 | pub fn build_module(builder: ModuleBuilder) -> ModuleBuilder { 45 | builder 46 | .function(wrap_function!(test_array)) 47 | .function(wrap_function!(test_array_assoc)) 48 | .function(wrap_function!(test_array_assoc_array_keys)) 49 | .function(wrap_function!(test_btree_map)) 50 | .function(wrap_function!(test_array_keys)) 51 | } 52 | 53 | #[cfg(test)] 54 | mod tests { 55 | #[test] 56 | fn array_works() { 57 | assert!(crate::integration::test::run_php("array/array.php")); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /guide/src/types/numbers.md: -------------------------------------------------------------------------------- 1 | # Primitive Numbers 2 | 3 | Primitive integers include `i8`, `i16`, `i32`, `i64`, `u8`, `u16`, `u32`, `u64`, 4 | `isize`, `usize`, `f32` and `f64`. 5 | 6 | | `T` parameter | `&T` parameter | `T` Return type | `&T` Return type | PHP representation | 7 | | ------------- | -------------- | --------------- | ---------------- | -------------------------------------------------------------------------------- | 8 | | Yes | No | Yes | No | `i32` on 32-bit platforms, `i64` on 64-bit platforms, `f64` platform-independent | 9 | 10 | Note that internally, PHP treats **all** of these integers the same (a 'long'), 11 | and therefore it must be converted into a long to be stored inside the zval. A 12 | long is always signed, and the size will be 32-bits on 32-bit platforms and 13 | 64-bits on 64-bit platforms. 14 | 15 | Floating point numbers are always stored in a `double` type (`f64`), regardless 16 | of platform. Note that converting a zval into a `f32` will lose accuracy. 17 | 18 | This means that converting `i64`, `u32`, `u64`, `isize` and `usize` _can_ fail 19 | depending on the value and the platform, which is why all zval conversions are 20 | fallible. 21 | 22 | ## Rust example 23 | 24 | ```rust,no_run 25 | # #![cfg_attr(windows, feature(abi_vectorcall))] 26 | # extern crate ext_php_rs; 27 | # use ext_php_rs::prelude::*; 28 | #[php_function] 29 | pub fn test_numbers(a: i32, b: u32, c: f32) -> u8 { 30 | println!("a {} b {} c {}", a, b, c); 31 | 0 32 | } 33 | # fn main() {} 34 | ``` 35 | 36 | ## PHP example 37 | 38 | ```php 39 | ` is valid when `T` implements `PackSlice`. This is currently 16 | implemented on most primitive numbers (i8, i16, i32, i64, u8, u16, u32, u64, 17 | isize, usize, f32, f64). 18 | 19 | [`pack`]: https://www.php.net/manual/en/function.pack.php 20 | [`unpack`]: https://www.php.net/manual/en/function.unpack.php 21 | 22 | ## Rust Usage 23 | 24 | ```rust,no_run 25 | # #![cfg_attr(windows, feature(abi_vectorcall))] 26 | # extern crate ext_php_rs; 27 | use ext_php_rs::prelude::*; 28 | use ext_php_rs::binary_slice::BinarySlice; 29 | 30 | #[php_function] 31 | pub fn test_binary_slice(input: BinarySlice) -> u8 { 32 | let mut sum = 0; 33 | for i in input.iter() { 34 | sum += i; 35 | } 36 | 37 | sum 38 | } 39 | # fn main() {} 40 | ``` 41 | 42 | ## PHP Usage 43 | 44 | ```php 45 | ZBox { 5 | ProcessGlobals::get().http_get_vars().to_owned() 6 | } 7 | 8 | #[php_function] 9 | pub fn test_globals_http_post() -> ZBox { 10 | ProcessGlobals::get().http_post_vars().to_owned() 11 | } 12 | 13 | #[php_function] 14 | pub fn test_globals_http_cookie() -> ZBox { 15 | ProcessGlobals::get().http_cookie_vars().to_owned() 16 | } 17 | 18 | #[php_function] 19 | pub fn test_globals_http_server() -> ZBox { 20 | ProcessGlobals::get().http_server_vars().unwrap().to_owned() 21 | } 22 | 23 | #[php_function] 24 | pub fn test_globals_http_request() -> ZBox { 25 | ProcessGlobals::get() 26 | .http_request_vars() 27 | .unwrap() 28 | .to_owned() 29 | } 30 | 31 | #[php_function] 32 | pub fn test_globals_http_files() -> ZBox { 33 | ProcessGlobals::get().http_files_vars().to_owned() 34 | } 35 | 36 | pub fn build_module(builder: ModuleBuilder) -> ModuleBuilder { 37 | builder 38 | .function(wrap_function!(test_globals_http_get)) 39 | .function(wrap_function!(test_globals_http_post)) 40 | .function(wrap_function!(test_globals_http_cookie)) 41 | .function(wrap_function!(test_globals_http_server)) 42 | .function(wrap_function!(test_globals_http_request)) 43 | .function(wrap_function!(test_globals_http_files)) 44 | } 45 | 46 | #[cfg(test)] 47 | mod tests { 48 | #[test] 49 | fn globals_works() { 50 | assert!(crate::integration::test::run_php("globals/globals.php")); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /guide/src/introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | `ext-php-rs` is a Rust library containing bindings and abstractions for the PHP 4 | extension API, which allows users to build extensions natively in Rust. 5 | 6 | ## Features 7 | 8 | - **Easy to use:** The built-in macros can abstract away the need to interact 9 | with the Zend API, such as Rust-type function parameter abstracting away 10 | interacting with Zend values. 11 | - **Lightweight:** You don't have to use the built-in helper macros. It's 12 | possible to write your own glue code around your own functions. 13 | - **Extensible:** Implement `IntoZval` and `FromZval` for your own custom types, 14 | allowing the type to be used as function parameters and return types. 15 | 16 | ## Goals 17 | 18 | Our main goal is to **make extension development easier.** 19 | 20 | - Writing extensions in C can be tedious, and with the Zend APIs limited 21 | documentation can be intimidating. 22 | - Rust's modern language features and feature-full standard library are big 23 | improvements on C. 24 | - Abstracting away the raw Zend APIs allows extensions to be developed faster 25 | and with more confidence. 26 | - Abstractions also allow us to support future (and potentially past) versions 27 | of PHP without significant changes to extension code. 28 | 29 | ## Versioning 30 | 31 | `ext-php-rs` follows semantic versioning, however, no backwards compatibility is 32 | guaranteed while we are at major version `0`, which is for the foreseeable 33 | future. It's recommended to lock the version at the patch level. 34 | 35 | When introducing breaking changes a migration guide will be provided in this 36 | guide. 37 | 38 | ## Documentation 39 | 40 | - This guide! 41 | - [Rust docs](https://docs.rs/ext-php-rs) 42 | -------------------------------------------------------------------------------- /guide/src/types/iterator.md: -------------------------------------------------------------------------------- 1 | # `ZendIterator` 2 | 3 | `ZendIterator`s are represented by the `Traversable` type. 4 | 5 | | `T` parameter | `&T` parameter | `T` Return type | `&T` Return type | PHP representation | 6 | |---------------| -------------- |-----------------| ---------------- | ------------------ | 7 | | No | Yes | No | No | `ZendIterator` | 8 | 9 | Converting from a zval to a `ZendIterator` is valid when there is an associated iterator to 10 | the variable. This means that any value, at the exception of an `array`, that can be used in 11 | a `foreach` loop can be converted into a `ZendIterator`. As an example, a `Generator` can be 12 | used but also a the result of a `query` call with `PDO`. 13 | 14 | If you want a more universal `iterable` type that also supports arrays, see [Iterable](./iterable.md). 15 | 16 | ## Rust example 17 | 18 | ```rust,no_run 19 | # #![cfg_attr(windows, feature(abi_vectorcall))] 20 | # extern crate ext_php_rs; 21 | # use ext_php_rs::prelude::*; 22 | # use ext_php_rs::types::ZendIterator; 23 | #[php_function] 24 | pub fn test_iterator(iterator: &mut ZendIterator) { 25 | for (k, v) in iterator.iter().expect("cannot rewind iterator") { 26 | // Note that the key can be anything, even an object 27 | // when iterating over Traversables! 28 | println!("k: {} v: {}", k.string().unwrap(), v.string().unwrap()); 29 | } 30 | } 31 | # fn main() {} 32 | ``` 33 | 34 | ## PHP example 35 | 36 | ```php 37 | 'world'; 41 | yield 'rust' => 'php'; 42 | }; 43 | 44 | test_iterator($generator()); 45 | ``` 46 | 47 | Output: 48 | 49 | ```text 50 | k: hello v: world 51 | k: rust v: php 52 | ``` 53 | -------------------------------------------------------------------------------- /src/rc.rs: -------------------------------------------------------------------------------- 1 | //! Traits and types for interacting with reference counted PHP types. 2 | 3 | use std::fmt::Debug; 4 | 5 | use crate::{ 6 | ffi::{zend_refcounted_h, zend_string}, 7 | types::ZendObject, 8 | }; 9 | 10 | /// Object used to store Zend reference counter. 11 | pub type ZendRefcount = zend_refcounted_h; 12 | 13 | impl Debug for ZendRefcount { 14 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 15 | f.debug_struct("ZendRefcount") 16 | .field("refcount", &self.refcount) 17 | .finish() 18 | } 19 | } 20 | 21 | /// Implemented on refcounted types. 22 | pub trait PhpRc { 23 | /// Returns an immutable reference to the corresponding refcount object. 24 | fn get_rc(&self) -> &ZendRefcount; 25 | 26 | /// Returns a mutable reference to the corresponding refcount object. 27 | fn get_rc_mut(&mut self) -> &mut ZendRefcount; 28 | 29 | /// Returns the number of references to the object. 30 | fn get_count(&self) -> u32 { 31 | self.get_rc().refcount 32 | } 33 | 34 | /// Increments the reference counter by 1. 35 | fn inc_count(&mut self) { 36 | self.get_rc_mut().refcount += 1; 37 | } 38 | 39 | /// Decrements the reference counter by 1. 40 | fn dec_count(&mut self) { 41 | self.get_rc_mut().refcount -= 1; 42 | } 43 | } 44 | 45 | macro_rules! rc { 46 | ($($t: ty),*) => { 47 | $( 48 | impl PhpRc for $t { 49 | fn get_rc(&self) -> &ZendRefcount { 50 | &self.gc 51 | } 52 | 53 | fn get_rc_mut(&mut self) -> &mut ZendRefcount { 54 | &mut self.gc 55 | } 56 | } 57 | )* 58 | }; 59 | } 60 | 61 | rc!(ZendObject, zend_string); 62 | -------------------------------------------------------------------------------- /tests/src/integration/types/types.php: -------------------------------------------------------------------------------- 1 | [['string'], 'string'], 5 | 'test_string' => [['string'], 'string'], 6 | 'test_bool' => [['bool'], 'bool'], 7 | 'test_number_signed' => [['int'], 'int'], 8 | 'test_number_unsigned' => [['int'], 'int'], 9 | 'test_number_float' => [['float'], 'float'], 10 | 'test_array' => [['array'], 'array'], 11 | 'test_array' => [['array'], 'array'], 12 | 'test_array_assoc' => [['array'], 'array'], 13 | 'test_binary' => [['string'], 'string'], 14 | 'test_nullable' => [['?string'], '?string'], 15 | 'test_object' => [['object'], 'object'], 16 | 'test_closure' => [[], 'RustClosure'], 17 | 'test_closure_once' => [['string'], 'RustClosure'], 18 | 'test_callable' => [['callable', 'string'], 'mixed'] 19 | ]; 20 | 21 | function toStr(ReflectionNamedType|ReflectionUnionType|ReflectionIntersectionType|null $v): string { 22 | if ($v === null) { 23 | return ''; 24 | } 25 | return match (true) { 26 | $v instanceof ReflectionNamedType => $v->allowsNull() && $v->getName() !== 'mixed' ? '?'.$v->getName() : $v->getName(), 27 | $v instanceof ReflectionUnionType => $v->getName(), 28 | $v instanceof ReflectionIntersectionType => $v->getName(), 29 | }; 30 | } 31 | 32 | foreach (TYPES as $func => [$args, $return]) { 33 | $f = new ReflectionFunction($func); 34 | $tReturn = toStr($f->getReturnType()); 35 | assert($tReturn === $return, "Wrong return type of $func, expected $return, got $tReturn"); 36 | foreach ($f->getParameters() as $idx => $param) { 37 | $tParam = toStr($param->getType()); 38 | assert($tParam === $args[$idx], "Wrong arg type $idx of $func, expected {$args[$idx]}, got $tParam"); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /guide/src/types/index.md: -------------------------------------------------------------------------------- 1 | # Types 2 | 3 | In PHP, data is stored in containers called zvals (zend values). Internally, 4 | these are effectively tagged unions (enums in Rust) without the safety that Rust 5 | introduces. Passing data between Rust and PHP requires the data to become a 6 | zval. This is done through two traits: `FromZval` and `IntoZval`. These traits 7 | have been implemented on most regular Rust types: 8 | 9 | - Primitive integers (`i8`, `i16`, `i32`, `i64`, `u8`, `u16`, `u32`, `u64`, 10 | `usize`, `isize`). 11 | - Double and single-precision floating point numbers (`f32`, `f64`). 12 | - Booleans. 13 | - Strings (`String` and `&str`) 14 | - `Vec` where T implements `IntoZval` and/or `FromZval`. 15 | - `HashMap` where T implements `IntoZval` and/or `FromZval`. 16 | - `Binary` where T implements `Pack`, used for transferring binary string 17 | data. 18 | - `BinarySlice` where T implements `Pack`, used for exposing PHP binary 19 | strings as read-only slices. 20 | - A PHP callable closure or function wrapped with `Callable`. 21 | - `Option` where T implements `IntoZval` and/or `FromZval`, and where `None` 22 | is converted to a PHP `null`. 23 | 24 | Return types can also include: 25 | 26 | - Any class type which implements `RegisteredClass` (i.e. any struct you have 27 | registered with PHP). 28 | - An immutable reference to `self` when used in a method, through the `ClassRef` 29 | type. 30 | - A Rust closure wrapped with `Closure`. 31 | - `Result`, where `T: IntoZval` and `E: Into`. When the 32 | error variant is encountered, it is converted into a `PhpException` and thrown 33 | as an exception. 34 | 35 | For a type to be returnable, it must implement `IntoZval`, while for it to be 36 | valid as a parameter, it must implement `FromZval`. 37 | -------------------------------------------------------------------------------- /guide/src/types/binary.md: -------------------------------------------------------------------------------- 1 | # `Binary` 2 | 3 | Binary data is represented as a string in PHP. The most common source of this 4 | data is from the [`pack`] and [`unpack`] functions. It allows you to transfer 5 | arbitrary binary data between Rust and PHP. 6 | 7 | | `T` parameter | `&T` parameter | `T` Return type | `&T` Return type | PHP representation | 8 | | ------------- | -------------- | --------------- | ---------------- | ------------------ | 9 | | Yes | No | Yes | No | `zend_string` | 10 | 11 | The binary type is represented as a string in PHP. Although not encoded, the 12 | data is converted into an array and then the pointer to the data is set as the 13 | string pointer, with the length of the array being the length of the string. 14 | 15 | `Binary` is valid when `T` implements `Pack`. This is currently implemented 16 | on most primitive numbers (i8, i16, i32, i64, u8, u16, u32, u64, isize, usize, 17 | f32, f64). 18 | 19 | [`pack`]: https://www.php.net/manual/en/function.pack.php 20 | [`unpack`]: https://www.php.net/manual/en/function.unpack.php 21 | 22 | ## Rust Usage 23 | 24 | ```rust,no_run 25 | # #![cfg_attr(windows, feature(abi_vectorcall))] 26 | # extern crate ext_php_rs; 27 | use ext_php_rs::prelude::*; 28 | use ext_php_rs::binary::Binary; 29 | 30 | #[php_function] 31 | pub fn test_binary(input: Binary) -> Binary { 32 | for i in input.iter() { 33 | println!("{}", i); 34 | } 35 | 36 | vec![5, 4, 3, 2, 1] 37 | .into_iter() 38 | .collect::>() 39 | } 40 | # fn main() {} 41 | ``` 42 | 43 | ## PHP Usage 44 | 45 | ```php 46 | 5, [1] => 4, [2] => 3, [3] => 2, [4] => 1 } 51 | ``` 52 | -------------------------------------------------------------------------------- /tests/src/integration/class/class.php: -------------------------------------------------------------------------------- 1 | getString() === 'lorem ipsum'); 11 | $class->setString('dolor et'); 12 | assert($class->getString() === 'dolor et'); 13 | $class->selfRef("foo"); 14 | assert($class->getString() === 'Changed to foo'); 15 | $class->selfMultiRef("bar"); 16 | assert($class->getString() === 'Changed to bar'); 17 | 18 | assert($class->getNumber() === 2022); 19 | $class->setNumber(2023); 20 | assert($class->getNumber() === 2023); 21 | 22 | var_dump($class); 23 | // Tests #prop decorator 24 | assert($class->booleanProp); 25 | $class->booleanProp = false; 26 | assert($class->booleanProp === false); 27 | 28 | // Call regular from object 29 | assert($class->staticCall('Php') === 'Hello Php'); 30 | 31 | // Call static from object 32 | assert($class::staticCall('Php') === 'Hello Php'); 33 | 34 | // Call static from class 35 | assert(TestClass::staticCall('Php') === 'Hello Php'); 36 | 37 | $ex = new TestClassExtends(); 38 | assert_exception_thrown(fn() => throw $ex); 39 | assert_exception_thrown(fn() => throwException()); 40 | 41 | $arrayAccess = new TestClassArrayAccess(); 42 | assert_exception_thrown(fn() => $arrayAccess[0] = 'foo'); 43 | assert_exception_thrown(fn() => $arrayAccess['foo']); 44 | assert($arrayAccess[0] === true); 45 | assert($arrayAccess[1] === false); 46 | 47 | $classReflection = new ReflectionClass(TestClassMethodVisibility::class); 48 | assert($classReflection->getMethod('__construct')->isPrivate()); 49 | assert($classReflection->getMethod('private')->isPrivate()); 50 | assert($classReflection->getMethod('protected')->isProtected()); 51 | 52 | $classReflection = new ReflectionClass(TestClassProtectedConstruct::class); 53 | assert($classReflection->getMethod('__construct')->isProtected()); 54 | -------------------------------------------------------------------------------- /guide/src/macros/interface.md: -------------------------------------------------------------------------------- 1 | # `#[php_interface]` Attribute 2 | 3 | You can export a `Trait` block to PHP. This exports all methods as well as 4 | constants to PHP on the interface. Trait method SHOULD NOT contain default 5 | implementations, as these are not supported in PHP interfaces. 6 | 7 | ## Options 8 | 9 | By default all constants are renamed to `UPPER_CASE` and all methods are renamed to 10 | `camelCase`. This can be changed by passing the `change_method_case` and 11 | `change_constant_case` as `#[php]` attributes on the `impl` block. The options are: 12 | 13 | - `#[php(change_method_case = "snake_case")]` - Renames the method to snake case. 14 | - `#[php(change_constant_case = "snake_case")]` - Renames the constant to snake case. 15 | 16 | See the [`name` and `change_case`](./php.md#name-and-change_case) section for a list of all 17 | available cases. 18 | 19 | ## Methods 20 | 21 | See the [`php_impl`](./impl.md#) 22 | 23 | ## Constants 24 | 25 | See the [`php_impl`](./impl.md#) 26 | 27 | ## Example 28 | 29 | Define an example trait with methods and constant: 30 | 31 | ```rust,no_run 32 | # #![cfg_attr(windows, feature(abi_vectorcall))] 33 | # extern crate ext_php_rs; 34 | use ext_php_rs::{prelude::*, types::ZendClassObject}; 35 | 36 | 37 | #[php_interface] 38 | #[php(name = "Rust\\TestInterface")] 39 | trait Test { 40 | const TEST: &'static str = "TEST"; 41 | 42 | fn co(); 43 | 44 | #[php(defaults(value = 0))] 45 | fn set_value(&mut self, value: i32); 46 | } 47 | 48 | #[php_module] 49 | pub fn module(module: ModuleBuilder) -> ModuleBuilder { 50 | module 51 | .interface::() 52 | } 53 | 54 | # fn main() {} 55 | ``` 56 | 57 | Using our newly created interface in PHP: 58 | 59 | ```php 60 | i32 { 18 | let ini_entries: Vec = vec![ 19 | IniEntryDef::new( 20 | "my_extension.display_emoji".to_owned(), 21 | "yes".to_owned(), 22 | &IniEntryPermission::All, 23 | ), 24 | ]; 25 | IniEntryDef::register(ini_entries, mod_num); 26 | 27 | 0 28 | } 29 | 30 | #[php_module] 31 | #[php(startup = "startup")] 32 | pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { 33 | module 34 | } 35 | # fn main() {} 36 | ``` 37 | 38 | ## Getting INI Settings 39 | 40 | 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 41 | 42 | ```rust,no_run 43 | # #![cfg_attr(windows, feature(abi_vectorcall))] 44 | # extern crate ext_php_rs; 45 | use ext_php_rs::{ 46 | prelude::*, 47 | zend::ExecutorGlobals, 48 | }; 49 | 50 | pub fn startup(ty: i32, mod_num: i32) -> i32 { 51 | // Get all INI values 52 | let ini_values = ExecutorGlobals::get().ini_values(); // HashMap> 53 | let my_ini_value = ini_values.get("my_extension.display_emoji"); // Option> 54 | 55 | 0 56 | } 57 | 58 | #[php_module] 59 | #[php(startup = "startup")] 60 | pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { 61 | module 62 | } 63 | # fn main() {} 64 | ``` 65 | -------------------------------------------------------------------------------- /guide/src/exceptions.md: -------------------------------------------------------------------------------- 1 | # Exceptions 2 | 3 | Exceptions can be thrown from Rust to PHP. The inverse (catching a PHP exception 4 | in Rust) is currently being worked on. 5 | 6 | ## Throwing exceptions 7 | 8 | [`PhpException`] is the type that represents an exception. It contains the 9 | message contained in the exception, the type of exception and a status code to 10 | go along with the exception. 11 | 12 | You can create a new exception with the `new()`, `default()`, or 13 | `from_class::()` methods. `Into` is implemented for `String` 14 | and `&str`, which creates an exception of the type `Exception` with a code of 0. 15 | It may be useful to implement `Into` for your error type. 16 | 17 | Calling the `throw()` method on a `PhpException` attempts to throw the exception 18 | in PHP. This function can fail if the type of exception is invalid (i.e. does 19 | not implement `Exception` or `Throwable`). Upon success, nothing will be 20 | returned. 21 | 22 | `IntoZval` is also implemented for `Result`, where `T: IntoZval` and 23 | `E: Into`. If the result contains the error variant, the exception 24 | is thrown. This allows you to return a result from a PHP function annotated with 25 | the `#[php_function]` attribute. 26 | 27 | ### Examples 28 | 29 | ```rust,no_run 30 | # #![cfg_attr(windows, feature(abi_vectorcall))] 31 | # extern crate ext_php_rs; 32 | use ext_php_rs::prelude::*; 33 | use std::convert::TryInto; 34 | 35 | // Trivial example - PHP represents all integers as `u64` on 64-bit systems 36 | // so the `u32` would be converted back to `u64`, but that's okay for an example. 37 | #[php_function] 38 | pub fn something_fallible(n: u64) -> PhpResult { 39 | let n: u32 = n.try_into().map_err(|_| "Could not convert into u32")?; 40 | Ok(n) 41 | } 42 | 43 | #[php_module] 44 | pub fn module(module: ModuleBuilder) -> ModuleBuilder { 45 | module 46 | } 47 | # fn main() {} 48 | ``` 49 | 50 | [`PhpException`]: https://docs.rs/ext-php-rs/0.5.0/ext_php_rs/php/exceptions/struct.PhpException.html 51 | -------------------------------------------------------------------------------- /crates/macros/src/constant.rs: -------------------------------------------------------------------------------- 1 | use darling::FromAttributes; 2 | use proc_macro2::TokenStream; 3 | use quote::{format_ident, quote}; 4 | use syn::ItemConst; 5 | 6 | use crate::helpers::get_docs; 7 | use crate::parsing::{PhpRename, RenameRule}; 8 | use crate::prelude::*; 9 | 10 | const INTERNAL_CONST_DOC_PREFIX: &str = "_internal_const_docs_"; 11 | const INTERNAL_CONST_NAME_PREFIX: &str = "_internal_const_name_"; 12 | 13 | #[derive(FromAttributes, Default, Debug)] 14 | #[darling(default, attributes(php), forward_attrs(doc))] 15 | pub(crate) struct PhpConstAttribute { 16 | #[darling(flatten)] 17 | pub(crate) rename: PhpRename, 18 | // TODO: Implement const Visibility 19 | // pub(crate) vis: Option, 20 | pub(crate) attrs: Vec, 21 | } 22 | 23 | pub fn parser(mut item: ItemConst) -> Result { 24 | let attr = PhpConstAttribute::from_attributes(&item.attrs)?; 25 | 26 | let name = attr 27 | .rename 28 | .rename(item.ident.to_string(), RenameRule::ScreamingSnake); 29 | let name_ident = format_ident!("{INTERNAL_CONST_NAME_PREFIX}{}", item.ident); 30 | 31 | let docs = get_docs(&attr.attrs)?; 32 | let docs_ident = format_ident!("{INTERNAL_CONST_DOC_PREFIX}{}", item.ident); 33 | item.attrs.retain(|attr| !attr.path().is_ident("php")); 34 | 35 | Ok(quote! { 36 | #item 37 | #[allow(non_upper_case_globals)] 38 | const #docs_ident: &[&str] = &[#(#docs),*]; 39 | #[allow(non_upper_case_globals)] 40 | const #name_ident: &str = #name; 41 | }) 42 | } 43 | 44 | pub fn wrap(input: &syn::Path) -> Result { 45 | let Some(const_name) = input.get_ident().map(ToString::to_string) else { 46 | bail!(input => "Pass a PHP const into `wrap_constant!()`."); 47 | }; 48 | let doc_const = format_ident!("{INTERNAL_CONST_DOC_PREFIX}{const_name}"); 49 | let const_name = format_ident!("{INTERNAL_CONST_NAME_PREFIX}{const_name}"); 50 | 51 | Ok(quote! { 52 | (#const_name, #input, #doc_const) 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /tools/update_lib_docs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ### This script updates the documentation in the macros library by copying 3 | ### the content from the guide files into the macros library source code. 4 | ### 5 | ### This is needed, because importing them using the `#[doc]` attribute 6 | ### does not work with rust analyser, which is used by the IDEs. 7 | ### https://github.com/rust-lang/rust-analyzer/issues/11137 8 | 9 | set -e 10 | 11 | # Check if the script is run from the root directory 12 | if [ ! -f "tools/update_lib_docs.sh" ]; then 13 | echo "Please run this script from the root directory of the project." 14 | exit 1 15 | fi 16 | 17 | function update_docs() { 18 | file_name="$1.md" 19 | 20 | if [ ! -f "guide/src/macros/$file_name" ]; then 21 | echo "File guide/src/macros/$file_name does not exist." 22 | exit 1 23 | fi 24 | 25 | if ! grep -q "// BEGIN DOCS FROM $file_name" "crates/macros/src/lib.rs"; then 26 | echo "No start placeholder found for $file_name in crates/macros/src/lib.rs." 27 | exit 1 28 | fi 29 | 30 | if ! grep -q "// END DOCS FROM $file_name" "crates/macros/src/lib.rs"; then 31 | echo "No end placeholder found for $file_name in crates/macros/src/lib.rs." 32 | exit 1 33 | fi 34 | 35 | lead="^\/\/ BEGIN DOCS FROM $file_name$" 36 | tail="^\/\/ END DOCS FROM $file_name$" 37 | 38 | # Make content a doc comment 39 | sed -e "s/^/\/\/\/ /" "guide/src/macros/$file_name" | 40 | # Disable doc tests for the pasted content 41 | sed -e "s/rust,no_run/rust,no_run,ignore/" | 42 | # Replace the section in the macros library with the content from the guide 43 | sed -i -e "/$lead/,/$tail/{ /$lead/{p; r /dev/stdin" -e "}; /$tail/p; d }" "crates/macros/src/lib.rs" 44 | } 45 | 46 | update_docs "classes" 47 | update_docs "constant" 48 | update_docs "extern" 49 | update_docs "function" 50 | update_docs "impl" 51 | update_docs "module" 52 | update_docs "zval_convert" 53 | update_docs "enum" 54 | update_docs "interface" 55 | 56 | # Format to remove trailing whitespace 57 | rustup run nightly rustfmt crates/macros/src/lib.rs 58 | -------------------------------------------------------------------------------- /src/ffi.rs: -------------------------------------------------------------------------------- 1 | //! Raw FFI bindings to the Zend API. 2 | 3 | #![allow(clippy::all)] 4 | #![allow(warnings)] 5 | 6 | use std::{ffi::c_void, os::raw::c_char}; 7 | 8 | pub const ZEND_MM_ALIGNMENT: isize = 8; 9 | pub const ZEND_MM_ALIGNMENT_MASK: isize = -8; 10 | 11 | // These are not generated by Bindgen as everything in `bindings.rs` will have 12 | // the `#[link(name = "php")]` attribute appended. This will cause the wrapper 13 | // functions to fail to link. 14 | #[link(name = "wrapper")] 15 | unsafe extern "C" { 16 | pub fn ext_php_rs_zend_string_init( 17 | str_: *const c_char, 18 | len: usize, 19 | persistent: bool, 20 | ) -> *mut zend_string; 21 | pub fn ext_php_rs_zend_string_release(zs: *mut zend_string); 22 | pub fn ext_php_rs_is_known_valid_utf8(zs: *const zend_string) -> bool; 23 | pub fn ext_php_rs_set_known_valid_utf8(zs: *mut zend_string); 24 | 25 | pub fn ext_php_rs_php_build_id() -> *const c_char; 26 | pub fn ext_php_rs_zend_object_alloc(obj_size: usize, ce: *mut zend_class_entry) -> *mut c_void; 27 | pub fn ext_php_rs_zend_object_release(obj: *mut zend_object); 28 | pub fn ext_php_rs_executor_globals() -> *mut zend_executor_globals; 29 | pub fn ext_php_rs_compiler_globals() -> *mut zend_compiler_globals; 30 | pub fn ext_php_rs_process_globals() -> *mut php_core_globals; 31 | pub fn ext_php_rs_sapi_globals() -> *mut sapi_globals_struct; 32 | pub fn ext_php_rs_file_globals() -> *mut php_file_globals; 33 | pub fn ext_php_rs_sapi_module() -> *mut sapi_module_struct; 34 | pub fn ext_php_rs_zend_try_catch( 35 | func: unsafe extern "C" fn(*const c_void) -> *const c_void, 36 | ctx: *const c_void, 37 | result: *mut *mut c_void, 38 | ) -> bool; 39 | 40 | pub fn ext_php_rs_zend_first_try_catch( 41 | func: unsafe extern "C" fn(*const c_void) -> *const c_void, 42 | ctx: *const c_void, 43 | result: *mut *mut c_void, 44 | ) -> bool; 45 | 46 | pub fn ext_php_rs_zend_bailout() -> !; 47 | } 48 | 49 | include!(concat!(env!("OUT_DIR"), "/bindings.rs")); 50 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ext-php-rs" 3 | description = "Bindings for the Zend API to build PHP extensions natively in Rust." 4 | repository = "https://github.com/extphprs/ext-php-rs" 5 | homepage = "https://ext-php.rs" 6 | license = "MIT OR Apache-2.0" 7 | keywords = ["php", "ffi", "zend"] 8 | version = "0.15.2" 9 | authors = [ 10 | "Pierre Tondereau ", 11 | "Xenira ", 12 | "David Cole ", 13 | ] 14 | edition = "2024" 15 | categories = ["api-bindings"] 16 | exclude = ["/.github", "/.crates"] 17 | autotests = false 18 | 19 | [dependencies] 20 | bitflags = "2" 21 | parking_lot = { version = "0.12", features = ["arc_lock"] } 22 | cfg-if = "1.0" 23 | once_cell = "1.21" 24 | anyhow = { version = "1", optional = true } 25 | ext-php-rs-derive = { version = "=0.11.5", path = "./crates/macros" } 26 | 27 | [dev-dependencies] 28 | skeptic = "0.13" 29 | 30 | [build-dependencies] 31 | anyhow = "1" 32 | bindgen = { version = "0.72", default-features = false, features = [ 33 | "logging", 34 | "prettyplease", 35 | ] } 36 | cc = "1.2" 37 | skeptic = "0.13" 38 | 39 | [target.'cfg(windows)'.build-dependencies] 40 | ureq = { version = "3.0", features = [ 41 | "native-tls", 42 | "gzip", 43 | ], default-features = false } 44 | native-tls = "0.2" 45 | zip = "6.0" 46 | 47 | [features] 48 | default = ["enum", "runtime"] 49 | closure = [] 50 | embed = [] 51 | anyhow = ["dep:anyhow"] 52 | enum = [] 53 | runtime = ["bindgen/runtime"] 54 | static = ["bindgen/static"] 55 | 56 | [workspace] 57 | members = ["crates/macros", "crates/cli", "tests"] 58 | 59 | [package.metadata.docs.rs] 60 | rustdoc-args = ["--cfg", "docs"] 61 | 62 | [lints.rust] 63 | missing_docs = "warn" 64 | 65 | [[example]] 66 | name = "hello_world" 67 | crate-type = ["cdylib"] 68 | 69 | [[test]] 70 | name = "guide_tests" 71 | path = "tests/guide.rs" 72 | required-features = ["embed", "closure", "anyhow"] 73 | 74 | [[test]] 75 | name = "module_tests" 76 | path = "tests/module.rs" 77 | 78 | [[test]] 79 | name = "sapi_tests" 80 | path = "tests/sapi.rs" 81 | -------------------------------------------------------------------------------- /guide/src/types/class_object.md: -------------------------------------------------------------------------------- 1 | # Class Object 2 | 3 | A class object is an instance of a Rust struct (which has been registered as a 4 | PHP class) that has been allocated alongside an object. You can think of a class 5 | object as a superset of an object, as a class object contains a Zend object. 6 | 7 | | `T` parameter | `&T` parameter | `T` Return type | `&T` Return type | PHP representation | 8 | | ------------- | --------------------- | --------------- | ------------------------- | ------------------------------ | 9 | | No | `&ZendClassObject` | Yes | `&mut ZendClassObject` | Zend object and a Rust struct. | 10 | 11 | ## Examples 12 | 13 | ### Returning a reference to `self` 14 | 15 | ```rust,no_run 16 | # #![cfg_attr(windows, feature(abi_vectorcall))] 17 | # extern crate ext_php_rs; 18 | use ext_php_rs::{prelude::*, types::ZendClassObject}; 19 | 20 | #[php_class] 21 | pub struct Example { 22 | foo: i32, 23 | bar: i32 24 | } 25 | 26 | #[php_impl] 27 | impl Example { 28 | // ext-php-rs treats the method as associated due to the `self_` argument. 29 | // The argument _must_ be called `self_`. 30 | pub fn builder_pattern( 31 | self_: &mut ZendClassObject, 32 | ) -> &mut ZendClassObject { 33 | // do something with `self_` 34 | self_ 35 | } 36 | } 37 | 38 | #[php_module] 39 | pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { 40 | module.class::() 41 | } 42 | # fn main() {} 43 | ``` 44 | 45 | ### Creating a new class instance 46 | 47 | ```rust,no_run 48 | # #![cfg_attr(windows, feature(abi_vectorcall))] 49 | # extern crate ext_php_rs; 50 | use ext_php_rs::prelude::*; 51 | 52 | #[php_class] 53 | pub struct Example { 54 | foo: i32, 55 | bar: i32 56 | } 57 | 58 | #[php_impl] 59 | impl Example { 60 | pub fn make_new(foo: i32, bar: i32) -> Example { 61 | Example { foo, bar } 62 | } 63 | } 64 | 65 | #[php_module] 66 | pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { 67 | module.class::() 68 | } 69 | # fn main() {} 70 | ``` 71 | -------------------------------------------------------------------------------- /src/zend/mod.rs: -------------------------------------------------------------------------------- 1 | //! Types used to interact with the Zend engine. 2 | 3 | mod _type; 4 | pub mod ce; 5 | mod class; 6 | mod ex; 7 | mod function; 8 | mod globals; 9 | mod handlers; 10 | mod ini_entry_def; 11 | mod linked_list; 12 | mod module; 13 | mod streams; 14 | mod try_catch; 15 | 16 | use crate::{ 17 | error::Result, 18 | ffi::{php_printf, sapi_module}, 19 | }; 20 | use std::ffi::CString; 21 | 22 | pub use _type::ZendType; 23 | pub use class::ClassEntry; 24 | pub use ex::ExecuteData; 25 | pub use function::Function; 26 | pub use function::FunctionEntry; 27 | pub use globals::ExecutorGlobals; 28 | pub use globals::FileGlobals; 29 | pub use globals::ProcessGlobals; 30 | pub use globals::SapiGlobals; 31 | pub use globals::SapiHeader; 32 | pub use globals::SapiHeaders; 33 | pub use globals::SapiModule; 34 | pub use handlers::ZendObjectHandlers; 35 | pub use ini_entry_def::IniEntryDef; 36 | pub use linked_list::ZendLinkedList; 37 | pub use module::ModuleEntry; 38 | pub use streams::*; 39 | #[cfg(feature = "embed")] 40 | pub(crate) use try_catch::panic_wrapper; 41 | pub use try_catch::{CatchError, bailout, try_catch, try_catch_first}; 42 | 43 | // Used as the format string for `php_printf`. 44 | const FORMAT_STR: &[u8] = b"%s\0"; 45 | 46 | /// Prints to stdout using the `php_printf` function. 47 | /// 48 | /// Also see the [`php_print`] and [`php_println`] macros. 49 | /// 50 | /// # Arguments 51 | /// 52 | /// * message - The message to print to stdout. 53 | /// 54 | /// # Errors 55 | /// 56 | /// * If the message could not be converted to a [`CString`]. 57 | pub fn printf(message: &str) -> Result<()> { 58 | let message = CString::new(message)?; 59 | unsafe { 60 | php_printf(FORMAT_STR.as_ptr().cast(), message.as_ptr()); 61 | }; 62 | Ok(()) 63 | } 64 | 65 | /// Get the name of the SAPI module. 66 | /// 67 | /// # Panics 68 | /// 69 | /// * If the module name is not a valid [`CStr`] 70 | /// 71 | /// [`CStr`]: std::ffi::CStr 72 | pub fn php_sapi_name() -> String { 73 | let c_str = unsafe { std::ffi::CStr::from_ptr(sapi_module.name) }; 74 | c_str.to_str().expect("Unable to parse CStr").to_string() 75 | } 76 | -------------------------------------------------------------------------------- /tests/module.rs: -------------------------------------------------------------------------------- 1 | //! Module tests 2 | #![cfg_attr(windows, feature(abi_vectorcall))] 3 | #![cfg(feature = "embed")] 4 | #![allow( 5 | missing_docs, 6 | clippy::needless_pass_by_value, 7 | clippy::must_use_candidate 8 | )] 9 | extern crate ext_php_rs; 10 | 11 | use cfg_if::cfg_if; 12 | 13 | use ext_php_rs::embed::Embed; 14 | use ext_php_rs::ffi::zend_register_module_ex; 15 | use ext_php_rs::prelude::*; 16 | use ext_php_rs::zend::ExecutorGlobals; 17 | 18 | #[test] 19 | fn test_module() { 20 | Embed::run(|| { 21 | // Allow to load the module 22 | cfg_if! { 23 | if #[cfg(php84)] { 24 | // Register as temporary (2) module 25 | unsafe { zend_register_module_ex(get_module(), 2) }; 26 | // When registering temporary modules directly (bypassing dl()), 27 | // we must set full_tables_cleanup to ensure proper cleanup of 28 | // request-scoped interned strings used as module registry keys. 29 | // Without this, the interned string is freed during 30 | // zend_interned_strings_deactivate() while module_registry still 31 | // references it, causing heap corruption. 32 | ExecutorGlobals::get_mut().full_tables_cleanup = true; 33 | } else { 34 | unsafe { zend_register_module_ex(get_module()) }; 35 | } 36 | } 37 | 38 | let result = Embed::eval("$foo = hello_world('foo');"); 39 | 40 | assert!(result.is_ok()); 41 | 42 | let zval = result.unwrap(); 43 | 44 | assert!(zval.is_string()); 45 | 46 | let string = zval.string().unwrap(); 47 | 48 | assert_eq!(string.clone(), "Hello, foo!"); 49 | }); 50 | } 51 | 52 | /// Gives you a nice greeting! 53 | /// 54 | /// @param string $name Your name. 55 | /// 56 | /// @return string Nice greeting! 57 | #[php_function] 58 | pub fn hello_world(name: String) -> String { 59 | format!("Hello, {name}!") 60 | } 61 | 62 | #[php_module] 63 | pub fn module(module: ModuleBuilder) -> ModuleBuilder { 64 | module.function(wrap_function!(hello_world)) 65 | } 66 | -------------------------------------------------------------------------------- /tests/src/integration/iterator/mod.rs: -------------------------------------------------------------------------------- 1 | use ext_php_rs::{ 2 | prelude::*, 3 | types::{ArrayKey, ZendHashTable, Zval}, 4 | }; 5 | 6 | #[php_function] 7 | pub fn iter_next(ht: &ZendHashTable) -> Vec { 8 | ht.iter() 9 | .flat_map(|(k, v)| [key_to_zval(k), v.shallow_clone()]) 10 | .collect() 11 | } 12 | 13 | #[php_function] 14 | pub fn iter_back(ht: &ZendHashTable) -> Vec { 15 | ht.iter() 16 | .rev() 17 | .flat_map(|(k, v)| [key_to_zval(k), v.shallow_clone()]) 18 | .collect() 19 | } 20 | 21 | #[php_function] 22 | pub fn iter_next_back(ht: &ZendHashTable, modulus: usize) -> Vec> { 23 | let mut result = Vec::with_capacity(ht.len()); 24 | let mut iter = ht.iter(); 25 | 26 | for i in 0..ht.len() + modulus { 27 | let entry = if i % modulus == 0 { 28 | iter.next_back() 29 | } else { 30 | iter.next() 31 | }; 32 | 33 | if let Some((k, v)) = entry { 34 | result.push(Some(key_to_zval(k))); 35 | result.push(Some(v.shallow_clone())); 36 | } else { 37 | result.push(None); 38 | } 39 | } 40 | 41 | result 42 | } 43 | 44 | fn key_to_zval(key: ArrayKey) -> Zval { 45 | match key { 46 | ArrayKey::String(s) => { 47 | let mut zval = Zval::new(); 48 | let _ = zval.set_string(s.as_str(), false); 49 | zval 50 | } 51 | ArrayKey::Str(s) => { 52 | let mut zval = Zval::new(); 53 | let _ = zval.set_string(s, false); 54 | zval 55 | } 56 | ArrayKey::Long(l) => { 57 | let mut zval = Zval::new(); 58 | zval.set_long(l); 59 | zval 60 | } 61 | } 62 | } 63 | pub fn build_module(builder: ModuleBuilder) -> ModuleBuilder { 64 | builder 65 | .function(wrap_function!(iter_next)) 66 | .function(wrap_function!(iter_back)) 67 | .function(wrap_function!(iter_next_back)) 68 | } 69 | 70 | #[cfg(test)] 71 | mod tests { 72 | #[test] 73 | fn iterator_works() { 74 | assert!(crate::integration::test::run_php("iterator/iterator.php")); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /guide/src/macros/extern.md: -------------------------------------------------------------------------------- 1 | # `#[php_extern]` Attribute 2 | 3 | Attribute used to annotate `extern` blocks which are deemed as PHP 4 | functions. 5 | 6 | This allows you to 'import' PHP functions into Rust so that they can be 7 | called like regular Rust functions. Parameters can be any type that 8 | implements [`IntoZval`], and the return type can be anything that implements 9 | [`From`] (notice how [`Zval`] is consumed rather than borrowed in this 10 | case). 11 | 12 | Unlike most other attributes, this does not need to be placed inside a 13 | `#[php_module]` block. 14 | 15 | # Panics 16 | 17 | The function can panic when called under a few circumstances: 18 | 19 | * The function could not be found or was not callable. 20 | * One of the parameters could not be converted into a [`Zval`]. 21 | * The actual function call failed internally. 22 | * The output [`Zval`] could not be parsed into the output type. 23 | 24 | The last point can be important when interacting with functions that return 25 | unions, such as [`strpos`] which can return an integer or a boolean. In this 26 | case, a [`Zval`] should be returned as parsing a boolean to an integer is 27 | invalid, and vice versa. 28 | 29 | # Example 30 | 31 | This `extern` block imports the [`strpos`] function from PHP. Notice that 32 | the string parameters can take either [`String`] or [`&str`], the optional 33 | parameter `offset` is an [`Option`], and the return value is a [`Zval`] 34 | as the return type is an integer-boolean union. 35 | 36 | ```rust,no_run 37 | # #![cfg_attr(windows, feature(abi_vectorcall))] 38 | # extern crate ext_php_rs; 39 | use ext_php_rs::{ 40 | prelude::*, 41 | types::Zval, 42 | }; 43 | 44 | #[php_extern] 45 | extern "C" { 46 | fn strpos(haystack: &str, needle: &str, offset: Option) -> Zval; 47 | } 48 | 49 | #[php_function] 50 | pub fn my_strpos() { 51 | assert_eq!(unsafe { strpos("Hello", "e", None) }.long(), Some(1)); 52 | } 53 | 54 | #[php_module] 55 | pub fn module(module: ModuleBuilder) -> ModuleBuilder { 56 | module.function(wrap_function!(my_strpos)) 57 | } 58 | # fn main() {} 59 | ``` 60 | 61 | [`strpos`]: https://www.php.net/manual/en/function.strpos.php 62 | [`IntoZval`]: crate::convert::IntoZval 63 | [`Zval`]: crate::types::Zval 64 | -------------------------------------------------------------------------------- /examples/hello_world.rs: -------------------------------------------------------------------------------- 1 | #![allow(missing_docs, clippy::must_use_candidate)] 2 | #![cfg_attr(windows, feature(abi_vectorcall))] 3 | use ext_php_rs::{constant::IntoConst, prelude::*, types::ZendClassObject}; 4 | 5 | #[derive(Debug)] 6 | #[php_class] 7 | pub struct TestClass { 8 | #[php(prop)] 9 | a: i32, 10 | #[php(prop)] 11 | b: i32, 12 | } 13 | 14 | #[php_impl] 15 | impl TestClass { 16 | #[php(name = "NEW_CONSTANT_NAME")] 17 | pub const SOME_CONSTANT: i32 = 5; 18 | pub const SOME_OTHER_STR: &'static str = "Hello, world!"; 19 | 20 | pub fn __construct(a: i32, b: i32) -> Self { 21 | Self { 22 | a: a + 10, 23 | b: b + 10, 24 | } 25 | } 26 | 27 | #[php(defaults(a = 5, test = 100))] 28 | pub fn test_camel_case(&self, a: i32, test: i32) { 29 | println!("a: {a} test: {test}"); 30 | } 31 | 32 | fn x() -> i32 { 33 | 5 34 | } 35 | 36 | pub fn builder_pattern( 37 | self_: &mut ZendClassObject, 38 | ) -> &mut ZendClassObject { 39 | dbg!(self_) 40 | } 41 | } 42 | 43 | #[php_function] 44 | pub fn new_class() -> TestClass { 45 | TestClass { a: 1, b: 2 } 46 | } 47 | 48 | #[php_function] 49 | pub fn hello_world() -> &'static str { 50 | "Hello, world!" 51 | } 52 | 53 | #[php_const] 54 | pub const HELLO_WORLD: i32 = 100; 55 | 56 | #[php_extern] 57 | extern "C" { 58 | fn phpinfo() -> bool; 59 | } 60 | 61 | #[derive(Debug, ZvalConvert)] 62 | pub struct TestZvalConvert<'a> { 63 | a: i32, 64 | b: i32, 65 | c: &'a str, 66 | } 67 | 68 | #[php_function] 69 | pub fn get_zval_convert(z: TestZvalConvert) -> i32 { 70 | dbg!(z); 71 | 5 72 | } 73 | 74 | fn startup(_ty: i32, mod_num: i32) -> i32 { 75 | 5.register_constant("SOME_CONST", mod_num).unwrap(); 76 | 0 77 | } 78 | 79 | #[php_module] 80 | #[php(startup = startup)] 81 | pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { 82 | module 83 | .class::() 84 | .function(wrap_function!(hello_world)) 85 | .function(wrap_function!(new_class)) 86 | .function(wrap_function!(get_zval_convert)) 87 | .constant(wrap_constant!(HELLO_WORLD)) 88 | .constant(("CONST_NAME", HELLO_WORLD, &[])) 89 | } 90 | -------------------------------------------------------------------------------- /src/types/long.rs: -------------------------------------------------------------------------------- 1 | //! Represents an integer introduced in PHP. Note that the size of this integer 2 | //! differs. On a 32-bit system, a [`ZendLong`] is 32-bits, while on a 64-bit 3 | //! system it is 64-bits. 4 | 5 | use crate::{ 6 | convert::IntoZval, 7 | error::{Error, Result}, 8 | ffi::zend_long, 9 | flags::DataType, 10 | macros::{into_zval, try_from_zval}, 11 | types::Zval, 12 | }; 13 | 14 | use std::convert::{TryFrom, TryInto}; 15 | 16 | /// A PHP long. 17 | /// 18 | /// The type size depends on the system architecture. On 32-bit systems, it is 19 | /// 32-bits, while on a 64-bit system, it is 64-bits. 20 | pub type ZendLong = zend_long; 21 | 22 | into_zval!(i8, set_long, Long); 23 | into_zval!(i16, set_long, Long); 24 | into_zval!(i32, set_long, Long); 25 | 26 | into_zval!(u8, set_long, Long); 27 | into_zval!(u16, set_long, Long); 28 | 29 | macro_rules! try_into_zval_int { 30 | ($type: ty) => { 31 | impl TryFrom<$type> for Zval { 32 | type Error = Error; 33 | 34 | fn try_from(val: $type) -> Result { 35 | let mut zv = Self::new(); 36 | let val: ZendLong = val.try_into().map_err(|_| Error::IntegerOverflow)?; 37 | zv.set_long(val); 38 | Ok(zv) 39 | } 40 | } 41 | 42 | impl IntoZval for $type { 43 | const TYPE: DataType = DataType::Long; 44 | const NULLABLE: bool = false; 45 | 46 | fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> { 47 | let val: ZendLong = self.try_into().map_err(|_| Error::IntegerOverflow)?; 48 | zv.set_long(val); 49 | Ok(()) 50 | } 51 | } 52 | }; 53 | } 54 | 55 | try_into_zval_int!(i64); 56 | try_into_zval_int!(u32); 57 | try_into_zval_int!(u64); 58 | 59 | try_into_zval_int!(isize); 60 | try_into_zval_int!(usize); 61 | 62 | try_from_zval!(i8, long, Long); 63 | try_from_zval!(i16, long, Long); 64 | try_from_zval!(i32, long, Long); 65 | try_from_zval!(i64, long, Long); 66 | 67 | try_from_zval!(u8, long, Long); 68 | try_from_zval!(u16, long, Long); 69 | try_from_zval!(u32, long, Long); 70 | try_from_zval!(u64, long, Long); 71 | 72 | try_from_zval!(usize, long, Long); 73 | try_from_zval!(isize, long, Long); 74 | -------------------------------------------------------------------------------- /src/types/array/conversions/hash_map.rs: -------------------------------------------------------------------------------- 1 | use super::super::ZendHashTable; 2 | use crate::types::ArrayKey; 3 | use crate::{ 4 | boxed::ZBox, 5 | convert::{FromZval, IntoZval}, 6 | error::{Error, Result}, 7 | flags::DataType, 8 | types::Zval, 9 | }; 10 | use std::hash::{BuildHasher, Hash}; 11 | use std::{collections::HashMap, convert::TryFrom}; 12 | 13 | impl<'a, K, V, H> TryFrom<&'a ZendHashTable> for HashMap 14 | where 15 | K: TryFrom, Error = Error> + Eq + Hash, 16 | V: FromZval<'a>, 17 | H: BuildHasher + Default, 18 | { 19 | type Error = Error; 20 | 21 | fn try_from(value: &'a ZendHashTable) -> Result { 22 | let mut hm = Self::with_capacity_and_hasher(value.len(), H::default()); 23 | 24 | for (key, val) in value { 25 | hm.insert( 26 | key.try_into()?, 27 | V::from_zval(val).ok_or_else(|| Error::ZvalConversion(val.get_type()))?, 28 | ); 29 | } 30 | 31 | Ok(hm) 32 | } 33 | } 34 | 35 | impl TryFrom> for ZBox 36 | where 37 | K: AsRef, 38 | V: IntoZval, 39 | H: BuildHasher, 40 | { 41 | type Error = Error; 42 | 43 | fn try_from(value: HashMap) -> Result { 44 | let mut ht = ZendHashTable::with_capacity( 45 | value.len().try_into().map_err(|_| Error::IntegerOverflow)?, 46 | ); 47 | 48 | for (k, v) in value { 49 | ht.insert(k.as_ref(), v)?; 50 | } 51 | 52 | Ok(ht) 53 | } 54 | } 55 | 56 | impl IntoZval for HashMap 57 | where 58 | K: AsRef, 59 | V: IntoZval, 60 | H: BuildHasher, 61 | { 62 | const TYPE: DataType = DataType::Array; 63 | const NULLABLE: bool = false; 64 | 65 | fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> { 66 | let arr = self.try_into()?; 67 | zv.set_hashtable(arr); 68 | Ok(()) 69 | } 70 | } 71 | 72 | impl<'a, V, H> FromZval<'a> for HashMap 73 | where 74 | V: FromZval<'a>, 75 | H: BuildHasher + Default, 76 | { 77 | const TYPE: DataType = DataType::Array; 78 | 79 | fn from_zval(zval: &'a Zval) -> Option { 80 | zval.array().and_then(|arr| arr.try_into().ok()) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /guide/src/getting-started/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | To get started using `ext-php-rs` you will need both a Rust toolchain 4 | and a PHP development environment. We'll cover each of these below. 5 | 6 | ## Rust toolchain 7 | 8 | First, make sure you have rust installed on your system. 9 | If you haven't already done so you can do so by following the instructions [here](https://www.rust-lang.org/tools/install). 10 | `ext-php-rs` runs on both the stable and nightly versions so you can choose whichever one fits you best. 11 | 12 | ## PHP development environment 13 | 14 | In order to develop PHP extensions, you'll need the following installed on your system: 15 | 16 | 1. The PHP CLI executable itself 17 | 2. The PHP development headers 18 | 3. The `php-config` binary 19 | 20 | While the easiest way to get started is to use the packages provided by your distribution, 21 | we recommend building PHP from source. 22 | 23 | **NB:** To use `ext-php-rs` you'll need at least PHP 8.0. 24 | 25 | ### Using a package manager 26 | 27 | ```sh 28 | # Debian and derivatives 29 | apt install php-dev 30 | # Arch Linux 31 | pacman -S php 32 | # Fedora 33 | dnf install php-devel 34 | # Homebrew 35 | brew install php 36 | ``` 37 | 38 | ### Compiling PHP from source 39 | 40 | Please refer to this [PHP internals book chapter](https://www.phpinternalsbook.com/php7/build_system/building_php.html) 41 | for an in-depth guide on how to build PHP from source. 42 | 43 | **TL;DR;** use the following commands to build a minimal development version 44 | with debug symbols enabled. 45 | 46 | ```sh 47 | # clone the php-src repository 48 | git clone https://github.com/php/php-src.git 49 | cd php-src 50 | # by default you will be on the master branch, which is the current 51 | # development version. You can check out a stable branch instead: 52 | git checkout PHP-8.1 53 | ./buildconf 54 | PREFIX="${HOME}/build/php" 55 | ./configure --prefix="${PREFIX}" \ 56 | --enable-debug \ 57 | --disable-all --disable-cgi 58 | make -j "$(nproc)" 59 | make install 60 | ``` 61 | 62 | The PHP CLI binary should now be located at `${PREFIX}/bin/php` 63 | and the `php-config` binary at `${PREFIX}/bin/php-config`. 64 | 65 | ## Next steps 66 | 67 | Now that we have our development environment in place, 68 | let's go [build an extension](./hello_world.md) ! 69 | -------------------------------------------------------------------------------- /guide/src/macros/module.md: -------------------------------------------------------------------------------- 1 | # `#[php_module]` Attribute 2 | 3 | The module macro is used to annotate the `get_module` function, which is used by 4 | the PHP interpreter to retrieve information about your extension, including the 5 | name, version, functions and extra initialization functions. Regardless if you 6 | use this macro, your extension requires a `extern "C" fn get_module()` so that 7 | PHP can get this information. 8 | 9 | The function is renamed to `get_module` if you have used another name. The 10 | function is passed an instance of `ModuleBuilder` which allows you to register 11 | the following (if required): 12 | 13 | - Functions, classes, and constants 14 | - Extension and request startup and shutdown functions. 15 | - Read more about the PHP extension lifecycle 16 | [here](https://www.phpinternalsbook.com/php7/extensions_design/php_lifecycle.html). 17 | - PHP extension information function 18 | - Used by the `phpinfo()` function to get information about your extension. 19 | 20 | Classes and constants are not registered with PHP in the `get_module` function. These are 21 | registered inside the extension startup function. 22 | 23 | ## Usage 24 | 25 | ```rust,no_run 26 | # #![cfg_attr(windows, feature(abi_vectorcall))] 27 | # extern crate ext_php_rs; 28 | use ext_php_rs::{ 29 | prelude::*, 30 | zend::ModuleEntry, 31 | info_table_start, 32 | info_table_row, 33 | info_table_end 34 | }; 35 | 36 | #[php_const] 37 | pub const MY_CUSTOM_CONST: &'static str = "Hello, world!"; 38 | 39 | #[php_class] 40 | pub struct Test { 41 | a: i32, 42 | b: i32 43 | } 44 | #[php_function] 45 | pub fn hello_world() -> &'static str { 46 | "Hello, world!" 47 | } 48 | 49 | /// Used by the `phpinfo()` function and when you run `php -i`. 50 | /// This will probably be simplified with another macro eventually! 51 | pub extern "C" fn php_module_info(_module: *mut ModuleEntry) { 52 | info_table_start!(); 53 | info_table_row!("my extension", "enabled"); 54 | info_table_end!(); 55 | } 56 | 57 | #[php_module] 58 | pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { 59 | module 60 | .constant(wrap_constant!(MY_CUSTOM_CONST)) 61 | .class::() 62 | .function(wrap_function!(hello_world)) 63 | .info_function(php_module_info) 64 | } 65 | # fn main() {} 66 | ``` 67 | -------------------------------------------------------------------------------- /tests/src/integration/array/array.php: -------------------------------------------------------------------------------- 1 | '1', 16 | 'b' => '2', 17 | 'c' => '3' 18 | ]); 19 | 20 | assert(array_key_exists('a', $assoc)); 21 | assert(array_key_exists('b', $assoc)); 22 | assert(array_key_exists('c', $assoc)); 23 | assert(in_array('1', $assoc)); 24 | assert(in_array('2', $assoc)); 25 | assert(in_array('3', $assoc)); 26 | 27 | $arrayKeys = test_array_keys(); 28 | assert($arrayKeys[-42] === "foo"); 29 | assert($arrayKeys[0] === "bar"); 30 | assert($arrayKeys[5] === "baz"); 31 | assert($arrayKeys[10] === "qux"); 32 | assert($arrayKeys["10"] === "qux"); 33 | assert($arrayKeys["quux"] === "quuux"); 34 | 35 | $assoc_keys = test_array_assoc_array_keys([ 36 | 'a' => '1', 37 | 2 => '2', 38 | '3' => '3', 39 | ]); 40 | assert($assoc_keys === [ 41 | 'a' => '1', 42 | 2 => '2', 43 | '3' => '3', 44 | ]); 45 | $assoc_keys = test_btree_map([ 46 | 'a' => '1', 47 | 2 => '2', 48 | '3' => '3', 49 | ]); 50 | assert($assoc_keys === [ 51 | 2 => '2', 52 | '3' => '3', 53 | 'a' => '1', 54 | ]); 55 | 56 | $assoc_keys = test_array_assoc_array_keys(['foo', 'bar', 'baz']); 57 | assert($assoc_keys === [ 58 | 0 => 'foo', 59 | 1 => 'bar', 60 | 2 => 'baz', 61 | ]); 62 | assert(test_btree_map(['foo', 'bar', 'baz']) === [ 63 | 0 => 'foo', 64 | 1 => 'bar', 65 | 2 => 'baz', 66 | ]); 67 | 68 | $leading_zeros = test_array_assoc_array_keys([ 69 | '0' => 'zero', 70 | '00' => 'zerozero', 71 | '007' => 'bond', 72 | ]); 73 | 74 | assert(array_key_exists(0, $leading_zeros), '"0" should become integer key 0'); 75 | assert($leading_zeros[0] === 'zero', 'Value at key 0 should be "zero"'); 76 | 77 | assert(array_key_exists('007', $leading_zeros), '"007" should stay as string key'); 78 | assert($leading_zeros['007'] === 'bond', 'Value at key "007" should be "bond"'); 79 | 80 | assert(array_key_exists('00', $leading_zeros), '"00" should stay as string key'); 81 | assert($leading_zeros['00'] === 'zerozero', 'Value at key "00" should be "zerozero"'); 82 | -------------------------------------------------------------------------------- /.release-plz.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | release_always = false 3 | git_release_body = """ 4 | {{ changelog }} 5 | {% if remote.contributors %} 6 | ### Thanks to the contributors for this release: 7 | {% for contributor in remote.contributors %} 8 | * @{{ contributor.username }} 9 | {% endfor %} 10 | {% endif %} 11 | """ 12 | 13 | [[package]] 14 | name = "ext-php-rs" 15 | publish_features = ["enum", "runtime", "closure", "embed", "anyhow"] 16 | 17 | [changelog] 18 | header = "# Changelog" 19 | body = """ 20 | {%- macro username(commit) -%} 21 | {% if commit.remote.username %} (by @{{ commit.remote.username }}){% endif -%} 22 | {% endmacro -%} 23 | {% macro commit_message(commit) %} 24 | - {% if commit.scope %}*({{ commit.scope }})* {% endif %}\ 25 | {% if commit.breaking %}[**breaking**] {% endif %}\ 26 | {{ commit.message | upper_first }}\ 27 | {{ self::username(commit=commit) }} \ 28 | {%- if commit.links %} \ 29 | {% for link in commit.links | unique(attribute="href") %}\ 30 | [[{{link.text}}]({{link.href}})] \ 31 | {% endfor %}\ 32 | {% endif %} 33 | {%- if commit.breaking and commit.breaking_description and not commit.breaking_description == commit.message%} 34 | > {{ commit.breaking_description -}} 35 | {% endif -%} 36 | {% endmacro %} 37 | ## [{{ version | trim_start_matches(pat="v") }}]{%- if release_link -%}({{ release_link }}){% endif %} - {{ timestamp | date(format="%Y-%m-%d") -}} 38 | {%- set breaking_commits = commits | filter(attribute="breaking", value=true) %} 39 | {%- if breaking_commits | length > 0 %} 40 | 41 | ### BREAKING CHANGES 42 | {% for commit in breaking_commits -%} 43 | {{- self::commit_message(commit=commit) -}} 44 | {% endfor -%} 45 | {% endif %} 46 | {%- for group, commits in commits | filter(attribute="breaking", value=false) | group_by(attribute="group") %} 47 | 48 | ### {{ group | upper_first -}} 49 | {% for commit in commits 50 | | filter(attribute="scope") 51 | | sort(attribute="scope") -%} 52 | {{- self::commit_message(commit=commit) -}} 53 | {% endfor -%} 54 | {%- for commit in commits -%} 55 | {% if not commit.scope -%} 56 | {{- self::commit_message(commit=commit) -}} 57 | {% endif -%} 58 | {% endfor -%} 59 | {% endfor %} 60 | """ 61 | link_parsers = [ 62 | { pattern = "#(\\d+)", href = "https://github.com/davidcole1340/ext-php-rs/issues/$1" }, 63 | ] 64 | -------------------------------------------------------------------------------- /src/types/iterable.rs: -------------------------------------------------------------------------------- 1 | use super::array::Iter as ZendHashTableIter; 2 | use super::iterator::Iter as ZendIteratorIter; 3 | use crate::convert::FromZval; 4 | use crate::flags::DataType; 5 | use crate::types::{ZendHashTable, ZendIterator, Zval}; 6 | 7 | /// This type represents a PHP iterable, which can be either an array or an 8 | /// object implementing the Traversable interface. 9 | #[derive(Debug)] 10 | pub enum Iterable<'a> { 11 | /// Iterable is an Array 12 | Array(&'a ZendHashTable), 13 | /// Iterable is a Traversable 14 | Traversable(&'a mut ZendIterator), 15 | } 16 | 17 | impl Iterable<'_> { 18 | /// Creates a new rust iterator from a PHP iterable. 19 | /// May return None if a Traversable cannot be rewound. 20 | // TODO: Check iter not returning iterator 21 | #[allow(clippy::iter_not_returning_iterator)] 22 | pub fn iter(&mut self) -> Option> { 23 | match self { 24 | Iterable::Array(array) => Some(Iter::Array(array.iter())), 25 | Iterable::Traversable(traversable) => Some(Iter::Traversable(traversable.iter()?)), 26 | } 27 | } 28 | } 29 | 30 | // TODO: Implement `iter_mut` 31 | #[allow(clippy::into_iter_without_iter)] 32 | impl<'a> IntoIterator for &'a mut Iterable<'a> { 33 | type Item = (Zval, &'a Zval); 34 | type IntoIter = Iter<'a>; 35 | 36 | fn into_iter(self) -> Self::IntoIter { 37 | self.iter().expect("Could not rewind iterator!") 38 | } 39 | } 40 | 41 | impl<'a> FromZval<'a> for Iterable<'a> { 42 | const TYPE: DataType = DataType::Iterable; 43 | 44 | fn from_zval(zval: &'a Zval) -> Option { 45 | if let Some(array) = zval.array() { 46 | return Some(Iterable::Array(array)); 47 | } 48 | 49 | if let Some(traversable) = zval.traversable() { 50 | return Some(Iterable::Traversable(traversable)); 51 | } 52 | 53 | None 54 | } 55 | } 56 | 57 | /// Rust iterator over a PHP iterable. 58 | pub enum Iter<'a> { 59 | Array(ZendHashTableIter<'a>), 60 | Traversable(ZendIteratorIter<'a>), 61 | } 62 | 63 | impl<'a> Iterator for Iter<'a> { 64 | type Item = (Zval, &'a Zval); 65 | 66 | fn next(&mut self) -> Option { 67 | match self { 68 | Iter::Array(array) => array.next_zval(), 69 | Iter::Traversable(traversable) => traversable.next(), 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/internal/class.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, marker::PhantomData}; 2 | 3 | use crate::{ 4 | builders::FunctionBuilder, 5 | class::{ConstructorMeta, RegisteredClass}, 6 | convert::{IntoZval, IntoZvalDyn}, 7 | describe::DocComments, 8 | flags::MethodFlags, 9 | props::Property, 10 | }; 11 | 12 | /// Collector used to collect methods for PHP classes. 13 | pub struct PhpClassImplCollector(PhantomData); 14 | 15 | impl Default for PhpClassImplCollector { 16 | #[inline] 17 | fn default() -> Self { 18 | Self(PhantomData) 19 | } 20 | } 21 | 22 | pub trait PhpClassImpl { 23 | fn get_methods(self) -> Vec<(FunctionBuilder<'static>, MethodFlags)>; 24 | fn get_method_props<'a>(self) -> HashMap<&'static str, Property<'a, T>>; 25 | fn get_constructor(self) -> Option>; 26 | fn get_constants(self) -> &'static [(&'static str, &'static dyn IntoZvalDyn, DocComments)]; 27 | } 28 | 29 | /// Default implementation for classes without an `impl` block. Classes that do 30 | /// have an `impl` block will override this by implementing `PhpClassImpl` for 31 | /// `PhpClassImplCollector` (note the missing reference). This is 32 | /// `dtolnay` specialisation: 33 | impl PhpClassImpl for &'_ PhpClassImplCollector { 34 | #[inline] 35 | fn get_methods(self) -> Vec<(FunctionBuilder<'static>, MethodFlags)> { 36 | Vec::default() 37 | } 38 | 39 | #[inline] 40 | fn get_method_props<'a>(self) -> HashMap<&'static str, Property<'a, T>> { 41 | HashMap::default() 42 | } 43 | 44 | #[inline] 45 | fn get_constructor(self) -> Option> { 46 | Option::default() 47 | } 48 | 49 | #[inline] 50 | fn get_constants(self) -> &'static [(&'static str, &'static dyn IntoZvalDyn, DocComments)] { 51 | &[] 52 | } 53 | } 54 | 55 | // This implementation is only used for `TYPE` and `NULLABLE`. 56 | impl IntoZval for PhpClassImplCollector { 57 | const TYPE: crate::flags::DataType = T::TYPE; 58 | const NULLABLE: bool = T::NULLABLE; 59 | 60 | #[inline] 61 | fn set_zval(self, _: &mut crate::types::Zval, _: bool) -> crate::error::Result<()> { 62 | unreachable!(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | #![deny(clippy::unwrap_used)] 3 | #![allow(non_upper_case_globals)] 4 | #![allow(non_camel_case_types)] 5 | #![allow(non_snake_case)] 6 | #![allow(unsafe_attr_outside_unsafe)] 7 | #![warn(clippy::pedantic)] 8 | #![cfg_attr(docs, feature(doc_cfg))] 9 | #![cfg_attr(windows, feature(abi_vectorcall))] 10 | 11 | pub mod alloc; 12 | pub mod args; 13 | pub mod binary; 14 | pub mod binary_slice; 15 | pub mod builders; 16 | pub mod convert; 17 | pub mod error; 18 | pub mod exception; 19 | pub mod ffi; 20 | pub mod flags; 21 | #[macro_use] 22 | pub mod macros; 23 | pub mod boxed; 24 | pub mod class; 25 | #[cfg(any(docs, feature = "closure"))] 26 | #[cfg_attr(docs, doc(cfg(feature = "closure")))] 27 | pub mod closure; 28 | pub mod constant; 29 | pub mod describe; 30 | #[cfg(feature = "embed")] 31 | pub mod embed; 32 | #[cfg(feature = "enum")] 33 | pub mod enum_; 34 | #[doc(hidden)] 35 | pub mod internal; 36 | pub mod props; 37 | pub mod rc; 38 | #[cfg(test)] 39 | pub mod test; 40 | pub mod types; 41 | mod util; 42 | pub mod zend; 43 | 44 | /// A module typically glob-imported containing the typically required macros 45 | /// and imports. 46 | pub mod prelude { 47 | 48 | pub use crate::builders::ModuleBuilder; 49 | #[cfg(any(docs, feature = "closure"))] 50 | #[cfg_attr(docs, doc(cfg(feature = "closure")))] 51 | pub use crate::closure::Closure; 52 | pub use crate::exception::{PhpException, PhpResult}; 53 | #[cfg(feature = "enum")] 54 | pub use crate::php_enum; 55 | pub use crate::php_print; 56 | pub use crate::php_println; 57 | pub use crate::types::ZendCallable; 58 | pub use crate::{ 59 | ZvalConvert, php_class, php_const, php_extern, php_function, php_impl, php_interface, 60 | php_module, wrap_constant, wrap_function, zend_fastcall, 61 | }; 62 | } 63 | 64 | /// `ext-php-rs` version. 65 | pub const VERSION: &str = env!("CARGO_PKG_VERSION"); 66 | 67 | /// Whether the extension is compiled for PHP debug mode. 68 | pub const PHP_DEBUG: bool = cfg!(php_debug); 69 | 70 | /// Whether the extension is compiled for PHP thread-safe mode. 71 | pub const PHP_ZTS: bool = cfg!(php_zts); 72 | 73 | #[cfg(feature = "enum")] 74 | pub use ext_php_rs_derive::php_enum; 75 | pub use ext_php_rs_derive::{ 76 | ZvalConvert, php_class, php_const, php_extern, php_function, php_impl, php_interface, 77 | php_module, wrap_constant, wrap_function, zend_fastcall, 78 | }; 79 | -------------------------------------------------------------------------------- /unix_build.rs: -------------------------------------------------------------------------------- 1 | use std::{path::PathBuf, process::Command}; 2 | 3 | use anyhow::{Context, Result, bail}; 4 | 5 | use crate::{PHPInfo, PHPProvider, find_executable, path_from_env}; 6 | 7 | pub struct Provider<'a> { 8 | info: &'a PHPInfo, 9 | } 10 | 11 | impl Provider<'_> { 12 | /// Runs `php-config` with one argument, returning the stdout. 13 | fn php_config(arg: &str) -> Result { 14 | let cmd = Command::new(Self::find_bin()?) 15 | .arg(arg) 16 | .output() 17 | .context("Failed to run `php-config`")?; 18 | let stdout = String::from_utf8_lossy(&cmd.stdout); 19 | if !cmd.status.success() { 20 | let stderr = String::from_utf8_lossy(&cmd.stderr); 21 | bail!("Failed to run `php-config`: {stdout} {stderr}"); 22 | } 23 | Ok(stdout.to_string()) 24 | } 25 | 26 | fn find_bin() -> Result { 27 | // If path is given via env, it takes priority. 28 | if let Some(path) = path_from_env("PHP_CONFIG") { 29 | if !path.try_exists()? { 30 | // If path was explicitly given and it can't be found, this is a hard error 31 | bail!("php-config executable not found at {}", path.display()); 32 | } 33 | return Ok(path); 34 | } 35 | find_executable("php-config").with_context(|| { 36 | "Could not find `php-config` executable. \ 37 | Please ensure `php-config` is in your PATH or the \ 38 | `PHP_CONFIG` environment variable is set." 39 | }) 40 | } 41 | } 42 | 43 | impl<'a> PHPProvider<'a> for Provider<'a> { 44 | fn new(info: &'a PHPInfo) -> Result { 45 | Ok(Self { info }) 46 | } 47 | 48 | fn get_includes(&self) -> Result> { 49 | Ok(Self::php_config("--includes")? 50 | .split(' ') 51 | .map(|s| s.trim_start_matches("-I")) 52 | .map(PathBuf::from) 53 | .collect()) 54 | } 55 | 56 | fn get_defines(&self) -> Result> { 57 | let mut defines = vec![]; 58 | if self.info.thread_safety()? { 59 | defines.push(("ZTS", "1")); 60 | } 61 | Ok(defines) 62 | } 63 | 64 | fn print_extra_link_args(&self) -> Result<()> { 65 | #[cfg(feature = "embed")] 66 | println!("cargo:rustc-link-lib=php"); 67 | 68 | Ok(()) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: coverage 2 | on: 3 | push: 4 | branches-ignore: 5 | - "release-plz-*" 6 | workflow_call: 7 | workflow_dispatch: 8 | env: 9 | # increment this manually to force cache eviction 10 | RUST_CACHE_PREFIX: "v0-rust" 11 | 12 | jobs: 13 | test: 14 | name: coverage 15 | runs-on: ubuntu-latest 16 | env: 17 | clang: "17" 18 | php_version: "8.4" 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v5 22 | with: 23 | fetch-depth: 0 24 | - name: Setup PHP 25 | uses: shivammathur/setup-php@v2 26 | with: 27 | php-version: ${{ env.php_version }} 28 | env: 29 | debug: true 30 | - name: Setup Rust 31 | uses: dtolnay/rust-toolchain@master 32 | with: 33 | components: rustfmt, clippy 34 | toolchain: stable 35 | - name: Cache cargo dependencies 36 | uses: Swatinem/rust-cache@v2 37 | with: 38 | prefix-key: ${{ env.RUST_CACHE_PREFIX }} 39 | - name: Cache LLVM and Clang 40 | id: cache-llvm 41 | uses: actions/cache@v4 42 | with: 43 | path: ${{ runner.temp }}/llvm-${{ env.clang }} 44 | key: ubuntu-latest-llvm-${{ env.clang }} 45 | - name: Setup LLVM & Clang 46 | id: clang 47 | uses: KyleMayes/install-llvm-action@v2 48 | with: 49 | version: ${{ env.clang }} 50 | directory: ${{ runner.temp }}/llvm-${{ env.clang }} 51 | cached: ${{ steps.cache-llvm.outputs.cache-hit }} 52 | - name: Configure Clang 53 | run: | 54 | echo "LIBCLANG_PATH=${{ runner.temp }}/llvm-${{ env.clang }}/lib" >> $GITHUB_ENV 55 | echo "LLVM_VERSION=${{ steps.clang.outputs.version }}" >> $GITHUB_ENV 56 | echo "LLVM_CONFIG_PATH=${{ runner.temp }}/llvm-${{ env.clang }}/bin/llvm-config" >> $GITHUB_ENV 57 | - name: Install Cargo expand 58 | uses: dtolnay/install@cargo-expand 59 | - name: Install tarpaulin 60 | run: | 61 | cargo install cargo-tarpaulin --locked 62 | cargo tarpaulin --version 63 | - name: Run tests 64 | run: | 65 | cargo tarpaulin --engine llvm --workspace --features closure,embed,anyhow --tests --exclude tests --exclude-files docsrs_bindings.rs --exclude-files "crates/macros/tests/expand/*.expanded.rs" --timeout 120 --out Xml 66 | - name: Upload coverage 67 | uses: coverallsapp/github-action@v2 68 | -------------------------------------------------------------------------------- /crates/macros/tests/expand/class.expanded.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate ext_php_rs_derive; 3 | /// Doc comments for MyClass. 4 | /// This is a basic class example. 5 | pub struct MyClass {} 6 | impl ::ext_php_rs::class::RegisteredClass for MyClass { 7 | const CLASS_NAME: &'static str = "MyClass"; 8 | const BUILDER_MODIFIER: ::std::option::Option< 9 | fn(::ext_php_rs::builders::ClassBuilder) -> ::ext_php_rs::builders::ClassBuilder, 10 | > = ::std::option::Option::None; 11 | const EXTENDS: ::std::option::Option<::ext_php_rs::class::ClassEntryInfo> = None; 12 | const IMPLEMENTS: &'static [::ext_php_rs::class::ClassEntryInfo] = &[]; 13 | const FLAGS: ::ext_php_rs::flags::ClassFlags = ::ext_php_rs::flags::ClassFlags::empty(); 14 | const DOC_COMMENTS: &'static [&'static str] = &[ 15 | " Doc comments for MyClass.", 16 | " This is a basic class example.", 17 | ]; 18 | #[inline] 19 | fn get_metadata() -> &'static ::ext_php_rs::class::ClassMetadata { 20 | static METADATA: ::ext_php_rs::class::ClassMetadata = ::ext_php_rs::class::ClassMetadata::new(); 21 | &METADATA 22 | } 23 | fn get_properties<'a>() -> ::std::collections::HashMap< 24 | &'static str, 25 | ::ext_php_rs::internal::property::PropertyInfo<'a, Self>, 26 | > { 27 | use ::std::iter::FromIterator; 28 | ::std::collections::HashMap::from_iter([]) 29 | } 30 | #[inline] 31 | fn method_builders() -> ::std::vec::Vec< 32 | ( 33 | ::ext_php_rs::builders::FunctionBuilder<'static>, 34 | ::ext_php_rs::flags::MethodFlags, 35 | ), 36 | > { 37 | use ::ext_php_rs::internal::class::PhpClassImpl; 38 | ::ext_php_rs::internal::class::PhpClassImplCollector::::default() 39 | .get_methods() 40 | } 41 | #[inline] 42 | fn constructor() -> ::std::option::Option< 43 | ::ext_php_rs::class::ConstructorMeta, 44 | > { 45 | use ::ext_php_rs::internal::class::PhpClassImpl; 46 | ::ext_php_rs::internal::class::PhpClassImplCollector::::default() 47 | .get_constructor() 48 | } 49 | #[inline] 50 | fn constants() -> &'static [( 51 | &'static str, 52 | &'static dyn ::ext_php_rs::convert::IntoZvalDyn, 53 | &'static [&'static str], 54 | )] { 55 | use ::ext_php_rs::internal::class::PhpClassImpl; 56 | ::ext_php_rs::internal::class::PhpClassImplCollector::::default() 57 | .get_constants() 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /crates/macros/src/extern_.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::quote; 3 | use syn::{ 4 | ForeignItemFn, ItemForeignMod, ReturnType, Signature, Token, punctuated::Punctuated, 5 | spanned::Spanned as _, token::Unsafe, 6 | }; 7 | 8 | use crate::prelude::*; 9 | 10 | pub fn parser(input: ItemForeignMod) -> Result { 11 | input 12 | .items 13 | .into_iter() 14 | .map(|item| match item { 15 | syn::ForeignItem::Fn(func) => parse_function(func), 16 | _ => bail!(item => "Only `extern` functions are supported by PHP."), 17 | }) 18 | .collect::>>() 19 | .map(|vec| quote! { #(#vec)* }) 20 | } 21 | 22 | fn parse_function(mut func: ForeignItemFn) -> Result { 23 | let ForeignItemFn { 24 | attrs, vis, sig, .. 25 | } = &mut func; 26 | sig.unsafety = Some(Unsafe::default()); // Function must be unsafe. 27 | 28 | let Signature { ident, .. } = &sig; 29 | 30 | let name = ident.to_string(); 31 | let params = sig 32 | .inputs 33 | .iter() 34 | .map(|input| match input { 35 | syn::FnArg::Typed(arg) => { 36 | let pat = &arg.pat; 37 | Some(quote! { &#pat }) 38 | } 39 | syn::FnArg::Receiver(_) => None, 40 | }) 41 | .collect::>>() 42 | .ok_or_else(|| { 43 | err!(sig.span() => "`self` parameters are not permitted inside `#[php_extern]` blocks.") 44 | })?; 45 | let ret = build_return(&name, &sig.output, ¶ms); 46 | 47 | Ok(quote! { 48 | #(#attrs)* #vis #sig { 49 | use ::std::convert::TryInto; 50 | 51 | let callable = ::ext_php_rs::types::ZendCallable::try_from_name( 52 | #name 53 | ).expect(concat!("Unable to find callable function `", #name, "`.")); 54 | 55 | #ret 56 | } 57 | }) 58 | } 59 | 60 | fn build_return( 61 | name: &str, 62 | return_type: &ReturnType, 63 | params: &Punctuated, 64 | ) -> TokenStream { 65 | match return_type { 66 | ReturnType::Default => quote! { 67 | callable.try_call(vec![ #params ]); 68 | }, 69 | ReturnType::Type(_, _) => quote! { 70 | callable 71 | .try_call(vec![ #params ]) 72 | .ok() 73 | .and_then(|zv| zv.try_into().ok()) 74 | .expect(concat!("Failed to call function `", #name, "`.")) 75 | }, 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /guide/src/types/object.md: -------------------------------------------------------------------------------- 1 | # Object 2 | 3 | An object is any object type in PHP. This can include a PHP class and PHP 4 | `stdClass`. A Rust struct registered as a PHP class is a [class object], which 5 | contains an object. 6 | 7 | Objects are valid as parameters but only as an immutable or mutable reference. 8 | You cannot take ownership of an object as objects are reference counted, and 9 | multiple zvals can point to the same object. You can return a boxed owned 10 | object. 11 | 12 | | `T` parameter | `&T` parameter | `T` Return type | `&T` Return type | PHP representation | 13 | | ------------- | -------------- | ------------------ | ----------------- | ------------------ | 14 | | No | Yes | `ZBox` | Yes, mutable only | Zend object. | 15 | 16 | ## Examples 17 | 18 | ### Calling a method 19 | 20 | ```rust,no_run 21 | # #![cfg_attr(windows, feature(abi_vectorcall))] 22 | # extern crate ext_php_rs; 23 | use ext_php_rs::{prelude::*, types::ZendObject}; 24 | 25 | // Take an object reference and also return it. 26 | #[php_function] 27 | pub fn take_obj(obj: &mut ZendObject) -> () { 28 | let _ = obj.try_call_method("hello", vec![&"arg1", &"arg2"]); 29 | } 30 | 31 | #[php_module] 32 | pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { 33 | module.function(wrap_function!(take_obj)) 34 | } 35 | # fn main() {} 36 | ``` 37 | 38 | ### Taking an object reference 39 | 40 | ```rust,no_run 41 | # #![cfg_attr(windows, feature(abi_vectorcall))] 42 | # extern crate ext_php_rs; 43 | use ext_php_rs::{prelude::*, types::ZendObject}; 44 | 45 | // Take an object reference and also return it. 46 | #[php_function] 47 | pub fn take_obj(obj: &mut ZendObject) -> &mut ZendObject { 48 | let _ = obj.set_property("hello", 5); 49 | dbg!(obj) 50 | } 51 | 52 | #[php_module] 53 | pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { 54 | module.function(wrap_function!(take_obj)) 55 | } 56 | # fn main() {} 57 | ``` 58 | 59 | ### Creating a new object 60 | 61 | ```rust,no_run 62 | # #![cfg_attr(windows, feature(abi_vectorcall))] 63 | # extern crate ext_php_rs; 64 | use ext_php_rs::{prelude::*, types::ZendObject, boxed::ZBox}; 65 | 66 | // Create a new `stdClass` and return it. 67 | #[php_function] 68 | pub fn make_object() -> ZBox { 69 | let mut obj = ZendObject::new_stdclass(); 70 | let _ = obj.set_property("hello", 5); 71 | obj 72 | } 73 | 74 | #[php_module] 75 | pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { 76 | module.function(wrap_function!(make_object)) 77 | } 78 | # fn main() {} 79 | ``` 80 | 81 | [class object]: ./class_object.md 82 | -------------------------------------------------------------------------------- /src/wrapper.h: -------------------------------------------------------------------------------- 1 | // PHP for Windows uses the `vectorcall` calling convention on some functions. 2 | // This is guarded by the `ZEND_FASTCALL` macro, which is set to `__vectorcall` 3 | // on Windows and nothing on other systems. 4 | // 5 | // However, `ZEND_FASTCALL` is only set when compiling with MSVC and the PHP 6 | // source code checks for the __clang__ macro and will not define `__vectorcall` 7 | // if it is set (even on Windows). This is a problem as Bindgen uses libclang to 8 | // generate bindings. To work around this, we include the header file containing 9 | // the `ZEND_FASTCALL` macro but not before undefining `__clang__` to pretend we 10 | // are compiling on MSVC. 11 | #if defined(_MSC_VER) && defined(__clang__) 12 | // PHP 8.5+ uses safe integer arithmetic functions. For Clang, we define macros 13 | // to use Clang's built-in overflow detection instead of Windows' intsafe.h. 14 | // This matches PHP's upstream solution in https://github.com/php/php-src/pull/17472 15 | #if defined(EXT_PHP_RS_PHP_85) 16 | #define PHP_HAVE_BUILTIN_SADDL_OVERFLOW 1 17 | #define PHP_HAVE_BUILTIN_SADDLL_OVERFLOW 1 18 | #define PHP_HAVE_BUILTIN_SSUBL_OVERFLOW 1 19 | #define PHP_HAVE_BUILTIN_SSUBLL_OVERFLOW 1 20 | #define PHP_HAVE_BUILTIN_SMULL_OVERFLOW 1 21 | #define PHP_HAVE_BUILTIN_SMULLL_OVERFLOW 1 22 | #endif 23 | #undef __clang__ 24 | #include "zend_portability.h" 25 | #define __clang__ 26 | #endif 27 | 28 | #include "php.h" 29 | 30 | #include "ext/standard/info.h" 31 | #include "ext/standard/php_var.h" 32 | #include "ext/standard/file.h" 33 | #ifdef EXT_PHP_RS_PHP_81 34 | #include "zend_enum.h" 35 | #endif 36 | #include "zend_exceptions.h" 37 | #include "zend_inheritance.h" 38 | #include "zend_interfaces.h" 39 | #include "php_variables.h" 40 | #include "zend_ini.h" 41 | #include "main/SAPI.h" 42 | 43 | zend_string *ext_php_rs_zend_string_init(const char *str, size_t len, bool persistent); 44 | void ext_php_rs_zend_string_release(zend_string *zs); 45 | bool ext_php_rs_is_known_valid_utf8(const zend_string *zs); 46 | void ext_php_rs_set_known_valid_utf8(zend_string *zs); 47 | 48 | const char *ext_php_rs_php_build_id(); 49 | void *ext_php_rs_zend_object_alloc(size_t obj_size, zend_class_entry *ce); 50 | void ext_php_rs_zend_object_release(zend_object *obj); 51 | zend_executor_globals *ext_php_rs_executor_globals(); 52 | php_core_globals *ext_php_rs_process_globals(); 53 | sapi_globals_struct *ext_php_rs_sapi_globals(); 54 | php_file_globals *ext_php_rs_file_globals(); 55 | sapi_module_struct *ext_php_rs_sapi_module(); 56 | bool ext_php_rs_zend_try_catch(void* (*callback)(void *), void *ctx, void **result); 57 | bool ext_php_rs_zend_first_try_catch(void* (*callback)(void *), void *ctx, void **result); 58 | void ext_php_rs_zend_bailout(); 59 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/01_bug_report.yml: -------------------------------------------------------------------------------- 1 | name: 🐛 Bug report 2 | description: Something isn't working as expected? 3 | labels: ["bug"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thanks for taking the time to report a bug! Please fill out the following sections to help us understand the issue. 9 | - type: textarea 10 | attributes: 11 | label: Description 12 | description: "Please provide a minimal way to reproduce the problem and describe what the expected vs actual behavior is." 13 | value: | 14 | ## Description 15 | 16 | 17 | ## Steps to Reproduce 18 | 1. 19 | 2. 20 | 3. 21 | 22 | ## Example 23 | 24 | **Extension Code:** 25 | ```rs 26 | ``` 27 | 28 | **PHP Code:** 29 | ```php 30 | usize { 24 | let char = unsafe { std::slice::from_raw_parts(str.cast::(), str_length) }; 25 | let string = String::from_utf8_lossy(char); 26 | 27 | println!("{string}"); 28 | 29 | unsafe { 30 | LAST_OUTPUT = string.to_string(); 31 | }; 32 | 33 | str_length 34 | } 35 | 36 | #[test] 37 | fn test_sapi() { 38 | let mut builder = SapiBuilder::new("test", "Test"); 39 | builder = builder.ub_write_function(output_tester); 40 | 41 | let sapi = builder.build().unwrap().into_raw(); 42 | let module = get_module(); 43 | 44 | unsafe { 45 | ext_php_rs_sapi_startup(); 46 | } 47 | 48 | unsafe { 49 | sapi_startup(sapi); 50 | } 51 | 52 | unsafe { 53 | php_module_startup(sapi, module); 54 | } 55 | 56 | let result = unsafe { php_request_startup() }; 57 | 58 | assert_eq!(result, ZEND_RESULT_CODE_SUCCESS); 59 | 60 | let _ = try_catch_first(|| { 61 | let result = Embed::eval("$foo = hello_world('foo');"); 62 | 63 | assert!(result.is_ok()); 64 | 65 | let zval = result.unwrap(); 66 | 67 | assert!(zval.is_string()); 68 | 69 | let string = zval.string().unwrap(); 70 | 71 | assert_eq!(string.clone(), "Hello, foo!"); 72 | 73 | let result = Embed::eval("var_dump($foo);"); 74 | 75 | assert!(result.is_ok()); 76 | }); 77 | 78 | unsafe { 79 | php_request_shutdown(std::ptr::null_mut()); 80 | } 81 | 82 | unsafe { 83 | php_module_shutdown(); 84 | } 85 | 86 | unsafe { 87 | sapi_shutdown(); 88 | } 89 | } 90 | 91 | /// Gives you a nice greeting! 92 | /// 93 | /// @param string $name Your name. 94 | /// 95 | /// @return string Nice greeting! 96 | #[php_function] 97 | pub fn hello_world(name: String) -> String { 98 | format!("Hello, {name}!") 99 | } 100 | 101 | #[php_module] 102 | pub fn module(module: ModuleBuilder) -> ModuleBuilder { 103 | module.function(wrap_function!(hello_world)) 104 | } 105 | -------------------------------------------------------------------------------- /crates/macros/src/module.rs: -------------------------------------------------------------------------------- 1 | use darling::FromAttributes; 2 | use proc_macro2::{Ident, TokenStream}; 3 | use quote::quote; 4 | use syn::{ItemFn, Signature}; 5 | 6 | use crate::prelude::*; 7 | 8 | #[derive(FromAttributes, Default, Debug)] 9 | #[darling(default, attributes(php))] 10 | pub(crate) struct PhpModuleAttribute { 11 | startup: Option, 12 | } 13 | 14 | pub fn parser(input: ItemFn) -> Result { 15 | let ItemFn { sig, block, .. } = input; 16 | let Signature { output, inputs, .. } = sig; 17 | let stmts = &block.stmts; 18 | 19 | let attr = PhpModuleAttribute::from_attributes(&input.attrs)?; 20 | let startup = if let Some(startup) = attr.startup { 21 | quote! { #startup(ty, mod_num) } 22 | } else { 23 | quote! { 0i32 } 24 | }; 25 | 26 | Ok(quote! { 27 | #[doc(hidden)] 28 | #[unsafe(no_mangle)] 29 | extern "C" fn get_module() -> *mut ::ext_php_rs::zend::ModuleEntry { 30 | static __EXT_PHP_RS_MODULE_STARTUP: ::ext_php_rs::internal::ModuleStartupMutex = 31 | ::ext_php_rs::internal::MODULE_STARTUP_INIT; 32 | 33 | extern "C" fn ext_php_rs_startup(ty: i32, mod_num: i32) -> i32 { 34 | let a = unsafe { #startup }; 35 | let b = __EXT_PHP_RS_MODULE_STARTUP 36 | .lock() 37 | .take() 38 | .inspect(|_| ::ext_php_rs::internal::ext_php_rs_startup()) 39 | .expect("Module startup function has already been called.") 40 | .startup(ty, mod_num) 41 | .map(|_| 0) 42 | .unwrap_or(1); 43 | a | b 44 | } 45 | 46 | #[inline] 47 | fn internal(#inputs) #output { 48 | #(#stmts)* 49 | } 50 | 51 | let builder = internal(::ext_php_rs::builders::ModuleBuilder::new( 52 | env!("CARGO_PKG_NAME"), 53 | env!("CARGO_PKG_VERSION") 54 | )) 55 | .startup_function(ext_php_rs_startup); 56 | 57 | match builder.try_into() { 58 | Ok((entry, startup)) => { 59 | __EXT_PHP_RS_MODULE_STARTUP.lock().replace(startup); 60 | entry.into_raw() 61 | }, 62 | Err(e) => panic!("Failed to build PHP module: {:?}", e), 63 | } 64 | } 65 | 66 | #[cfg(debug_assertions)] 67 | #[unsafe(no_mangle)] 68 | pub extern "C" fn ext_php_rs_describe_module() -> ::ext_php_rs::describe::Description { 69 | use ::ext_php_rs::describe::*; 70 | 71 | #[inline] 72 | fn internal(#inputs) #output { 73 | #(#stmts)* 74 | } 75 | 76 | let builder = internal(::ext_php_rs::builders::ModuleBuilder::new( 77 | env!("CARGO_PKG_NAME"), 78 | env!("CARGO_PKG_VERSION") 79 | )); 80 | 81 | Description::new(builder.into()) 82 | } 83 | }) 84 | } 85 | -------------------------------------------------------------------------------- /tests/src/integration/magic_method/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::unused_self)] 2 | use ext_php_rs::{ 3 | prelude::*, 4 | types::{ArrayKey, Zval}, 5 | }; 6 | use std::collections::HashMap; 7 | 8 | #[php_class] 9 | pub struct MagicMethod(i64); 10 | 11 | #[php_impl] 12 | impl MagicMethod { 13 | pub fn __construct() -> Self { 14 | Self(0) 15 | } 16 | 17 | pub fn __destruct(&self) {} 18 | 19 | pub fn __call(&self, name: String, _arguments: HashMap) -> Zval { 20 | let mut z = Zval::new(); 21 | if name == "callMagicMethod" { 22 | let s = "Hello".to_string(); 23 | 24 | let _ = z.set_string(s.as_str(), false); 25 | z 26 | } else { 27 | z.set_null(); 28 | z 29 | } 30 | } 31 | 32 | pub fn __call_static(name: String, arguments: Vec<(ArrayKey<'_>, &Zval)>) -> Zval { 33 | let mut zval = Zval::new(); 34 | if name == "callStaticSomeMagic" { 35 | let concat_args = format!( 36 | "Hello from static call {}", 37 | arguments 38 | .iter() 39 | .filter(|(_, v)| v.is_long()) 40 | .map(|(_, s)| s.long().unwrap().to_string()) 41 | .collect::>() 42 | .join(", ") 43 | ); 44 | 45 | let _ = zval.set_string(&concat_args, false); 46 | zval 47 | } else { 48 | zval.set_null(); 49 | zval 50 | } 51 | } 52 | 53 | pub fn __get(&self, name: String) -> Zval { 54 | let mut v = Zval::new(); 55 | v.set_null(); 56 | if name == "count" { 57 | v.set_long(self.0); 58 | } 59 | 60 | v 61 | } 62 | 63 | pub fn __set(&mut self, prop_name: String, val: &Zval) { 64 | if val.is_long() && prop_name == "count" { 65 | self.0 = val.long().unwrap(); 66 | } 67 | } 68 | 69 | pub fn __isset(&self, prop_name: String) -> bool { 70 | "count" == prop_name 71 | } 72 | 73 | pub fn __unset(&mut self, prop_name: String) { 74 | if prop_name == "count" { 75 | self.0 = 0; 76 | } 77 | } 78 | 79 | pub fn __to_string(&self) -> String { 80 | self.0.to_string() 81 | } 82 | 83 | pub fn __invoke(&self, n: i64) -> i64 { 84 | self.0 + n 85 | } 86 | 87 | pub fn __debug_info(&self) -> HashMap { 88 | let mut h: HashMap = HashMap::new(); 89 | let mut z = Zval::new(); 90 | z.set_long(self.0); 91 | h.insert("count".to_string(), z); 92 | 93 | h 94 | } 95 | } 96 | 97 | pub fn build_module(builder: ModuleBuilder) -> ModuleBuilder { 98 | builder.class::() 99 | } 100 | 101 | #[cfg(test)] 102 | mod tests { 103 | #[test] 104 | fn magic_method() { 105 | assert!(crate::integration::test::run_php( 106 | "magic_method/magic_method.php" 107 | )); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/zend/ini_entry_def.rs: -------------------------------------------------------------------------------- 1 | //! Builder for creating inis and methods in PHP. 2 | //! See for details. 3 | 4 | use std::{ffi::CString, os::raw::c_char, ptr}; 5 | 6 | use crate::{ffi::zend_ini_entry_def, ffi::zend_register_ini_entries, flags::IniEntryPermission}; 7 | 8 | /// A Zend ini entry definition. 9 | /// 10 | /// To register ini definitions for extensions, the [`IniEntryDef`] builder 11 | /// should be used. Ini entries should be registered in your module's 12 | /// `startup_function` via `IniEntryDef::register(Vec)`. 13 | pub type IniEntryDef = zend_ini_entry_def; 14 | 15 | impl IniEntryDef { 16 | /// Creates a new ini entry definition. 17 | /// 18 | /// # Panics 19 | /// 20 | /// * If the name or value cannot be converted to a C string 21 | /// * If the name or value length is > `65_535` 22 | /// * If the permission bits are invalid 23 | #[must_use] 24 | pub fn new(name: String, default_value: String, permission: &IniEntryPermission) -> Self { 25 | let mut template = Self::end(); 26 | let name = CString::new(name).expect("Unable to create CString from name"); 27 | let value = CString::new(default_value).expect("Unable to create CString from value"); 28 | template.name_length = name 29 | .as_bytes() 30 | .len() 31 | .try_into() 32 | .expect("Invalid name length"); 33 | template.name = name.into_raw(); 34 | template.value_length = value 35 | .as_bytes() 36 | .len() 37 | .try_into() 38 | .expect("Invalid value length"); 39 | template.value = value.into_raw(); 40 | // FIXME: Double assignment of modifiable 41 | template.modifiable = IniEntryPermission::PerDir 42 | .bits() 43 | .try_into() 44 | .expect("Invalid permission bits"); 45 | template.modifiable = permission 46 | .bits() 47 | .try_into() 48 | .expect("Invalid permission bits"); 49 | template 50 | } 51 | 52 | /// Returns an empty ini entry def, signifying the end of a ini list. 53 | #[must_use] 54 | pub fn end() -> Self { 55 | Self { 56 | name: ptr::null::(), 57 | on_modify: None, 58 | mh_arg1: std::ptr::null_mut(), 59 | mh_arg2: std::ptr::null_mut(), 60 | mh_arg3: std::ptr::null_mut(), 61 | value: std::ptr::null_mut(), 62 | displayer: None, 63 | modifiable: 0, 64 | value_length: 0, 65 | name_length: 0, 66 | } 67 | } 68 | 69 | /// Converts the ini entry into a raw and pointer, releasing it to the 70 | /// C world. 71 | #[must_use] 72 | pub fn into_raw(self) -> *mut Self { 73 | Box::into_raw(Box::new(self)) 74 | } 75 | 76 | /// Registers a list of ini entries. 77 | pub fn register(mut entries: Vec, module_number: i32) { 78 | entries.push(Self::end()); 79 | let entries = Box::into_raw(entries.into_boxed_slice()) as *const Self; 80 | 81 | unsafe { zend_register_ini_entries(entries, module_number) }; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /.github/workflows/release-plz.yml: -------------------------------------------------------------------------------- 1 | name: Release-plz 2 | 3 | permissions: 4 | pull-requests: write 5 | contents: write 6 | 7 | on: 8 | workflow_call: 9 | secrets: 10 | RELEASE_PLZ_TOKEN: 11 | required: true 12 | outputs: 13 | release_created: 14 | description: "Indicates if a release was created" 15 | value: ${{ jobs.release-plz-release.outputs.release_created }} 16 | workflow_dispatch: 17 | 18 | jobs: 19 | release-plz-release: 20 | name: Release-plz release 21 | runs-on: ubuntu-latest 22 | if: github.ref == 'refs/heads/master' 23 | permissions: 24 | id-token: write 25 | outputs: 26 | release_created: ${{ steps.release-plz.outputs.release_created }} 27 | env: 28 | clang: "17" 29 | php_version: "8.5" 30 | steps: 31 | - name: Checkout repository 32 | uses: actions/checkout@v5 33 | with: 34 | fetch-depth: 0 35 | - name: Setup PHP 36 | uses: shivammathur/setup-php@v2 37 | with: 38 | php-version: ${{ env.php_version }} 39 | env: 40 | debug: true 41 | - name: Install Rust toolchain 42 | uses: dtolnay/rust-toolchain@stable 43 | - name: Cache LLVM and Clang 44 | id: cache-llvm 45 | uses: actions/cache@v4 46 | with: 47 | path: ${{ runner.temp }}/llvm-${{ env.clang }} 48 | key: ubuntu-latest-llvm-${{ env.clang }} 49 | - name: Setup LLVM & Clang 50 | id: clang 51 | uses: KyleMayes/install-llvm-action@v2 52 | with: 53 | version: ${{ env.clang }} 54 | directory: ${{ runner.temp }}/llvm-${{ env.clang }} 55 | cached: ${{ steps.cache-llvm.outputs.cache-hit }} 56 | - name: Configure Clang 57 | run: | 58 | echo "LIBCLANG_PATH=${{ runner.temp }}/llvm-${{ env.clang }}/lib" >> $GITHUB_ENV 59 | echo "LLVM_VERSION=${{ steps.clang.outputs.version }}" >> $GITHUB_ENV 60 | echo "LLVM_CONFIG_PATH=${{ runner.temp }}/llvm-${{ env.clang }}/bin/llvm-config" >> $GITHUB_ENV 61 | - name: Authenticate to crates.io 62 | uses: rust-lang/crates-io-auth-action@v1 63 | id: auth 64 | - name: Run release-plz 65 | id: release-plz 66 | uses: release-plz/action@v0.5 67 | with: 68 | command: release 69 | env: 70 | GITHUB_TOKEN: ${{ secrets.RELEASE_PLZ_TOKEN }} 71 | 72 | release-plz-pr: 73 | name: Release-plz PR 74 | runs-on: ubuntu-latest 75 | if: github.ref == 'refs/heads/master' 76 | permissions: 77 | id-token: write 78 | concurrency: 79 | group: release-plz-${{ github.ref }} 80 | cancel-in-progress: false 81 | steps: 82 | - name: Checkout repository 83 | uses: actions/checkout@v5 84 | with: 85 | fetch-depth: 0 86 | - name: Install Rust toolchain 87 | uses: dtolnay/rust-toolchain@stable 88 | - name: Authenticate to crates.io 89 | uses: rust-lang/crates-io-auth-action@v1 90 | id: auth 91 | - name: Run release-plz 92 | uses: release-plz/action@v0.5 93 | with: 94 | command: release-pr 95 | env: 96 | GITHUB_TOKEN: ${{ secrets.RELEASE_PLZ_TOKEN }} 97 | -------------------------------------------------------------------------------- /src/wrapper.c: -------------------------------------------------------------------------------- 1 | #include "wrapper.h" 2 | 3 | zend_string *ext_php_rs_zend_string_init(const char *str, size_t len, bool persistent) { 4 | return zend_string_init(str, len, persistent); 5 | } 6 | 7 | void ext_php_rs_zend_string_release(zend_string *zs) { 8 | zend_string_release(zs); 9 | } 10 | 11 | bool ext_php_rs_is_known_valid_utf8(const zend_string *zs) { 12 | return GC_FLAGS(zs) & IS_STR_VALID_UTF8; 13 | } 14 | 15 | void ext_php_rs_set_known_valid_utf8(zend_string *zs) { 16 | if (!ZSTR_IS_INTERNED(zs)) { 17 | GC_ADD_FLAGS(zs, IS_STR_VALID_UTF8); 18 | } 19 | } 20 | 21 | const char *ext_php_rs_php_build_id() { return ZEND_MODULE_BUILD_ID; } 22 | 23 | void *ext_php_rs_zend_object_alloc(size_t obj_size, zend_class_entry *ce) { 24 | return zend_object_alloc(obj_size, ce); 25 | } 26 | 27 | void ext_php_rs_zend_object_release(zend_object *obj) { 28 | zend_object_release(obj); 29 | } 30 | 31 | zend_executor_globals *ext_php_rs_executor_globals() { 32 | #ifdef ZTS 33 | #ifdef ZEND_ENABLE_STATIC_TSRMLS_CACHE 34 | return TSRMG_FAST_BULK_STATIC(executor_globals_offset, zend_executor_globals); 35 | #else 36 | return TSRMG_FAST_BULK(executor_globals_offset, zend_executor_globals *); 37 | #endif 38 | #else 39 | return &executor_globals; 40 | #endif 41 | } 42 | 43 | zend_compiler_globals *ext_php_rs_compiler_globals() { 44 | #ifdef ZTS 45 | #ifdef ZEND_ENABLE_STATIC_TSRMLS_CACHE 46 | return TSRMG_FAST_BULK_STATIC(compiler_globals_offset, zend_compiler_globals); 47 | #else 48 | return TSRMG_FAST_BULK(compiler_globals_offset, zend_compiler_globals *); 49 | #endif 50 | #else 51 | return &compiler_globals; 52 | #endif 53 | } 54 | 55 | php_core_globals *ext_php_rs_process_globals() { 56 | #ifdef ZTS 57 | #ifdef ZEND_ENABLE_STATIC_TSRMLS_CACHE 58 | return TSRMG_FAST_BULK_STATIC(core_globals_offset, php_core_globals); 59 | #else 60 | return TSRMG_FAST_BULK(core_globals_offset, php_core_globals *); 61 | #endif 62 | #else 63 | return &core_globals; 64 | #endif 65 | } 66 | 67 | sapi_globals_struct *ext_php_rs_sapi_globals() { 68 | #ifdef ZTS 69 | #ifdef ZEND_ENABLE_STATIC_TSRMLS_CACHE 70 | return TSRMG_FAST_BULK_STATIC(sapi_globals_offset, sapi_globals_struct); 71 | #else 72 | return TSRMG_FAST_BULK(sapi_globals_offset, sapi_globals_struct *); 73 | #endif 74 | #else 75 | return &sapi_globals; 76 | #endif 77 | } 78 | 79 | php_file_globals *ext_php_rs_file_globals() { 80 | #ifdef ZTS 81 | return TSRMG_FAST_BULK(file_globals_id, php_file_globals *); 82 | #else 83 | return &file_globals; 84 | #endif 85 | } 86 | 87 | sapi_module_struct *ext_php_rs_sapi_module() { 88 | return &sapi_module; 89 | } 90 | 91 | bool ext_php_rs_zend_try_catch(void* (*callback)(void *), void *ctx, void **result) { 92 | zend_try { 93 | *result = callback(ctx); 94 | } zend_catch { 95 | return true; 96 | } zend_end_try(); 97 | 98 | return false; 99 | } 100 | 101 | bool ext_php_rs_zend_first_try_catch(void* (*callback)(void *), void *ctx, void **result) { 102 | zend_first_try { 103 | *result = callback(ctx); 104 | } zend_catch { 105 | return true; 106 | } zend_end_try(); 107 | 108 | return false; 109 | } 110 | 111 | void ext_php_rs_zend_bailout() { 112 | zend_bailout(); 113 | } 114 | -------------------------------------------------------------------------------- /guide/src/macros/enum.md: -------------------------------------------------------------------------------- 1 | # `#[php_enum]` Attribute 2 | 3 | Enums can be exported to PHP as enums with the `#[php_enum]` attribute macro. 4 | This attribute derives the `RegisteredClass` and `PhpEnum` traits on your enum. 5 | To register the enum use the `enumeration::()` method on the `ModuleBuilder` 6 | in the `#[php_module]` macro. 7 | 8 | ## Options 9 | 10 | The `#[php_enum]` attribute can be configured with the following options: 11 | - `#[php(name = "EnumName")]` or `#[php(change_case = snake_case)]`: Sets the name of the enum in PHP. 12 | The default is the `PascalCase` name of the enum. 13 | - `#[php(allow_native_discriminants)]`: Allows the use of native Rust discriminants (e.g., `Hearts = 1`). 14 | 15 | The cases of the enum can be configured with the following options: 16 | - `#[php(name = "CaseName")]` or `#[php(change_case = snake_case)]`: Sets the name of the enum case in PHP. 17 | The default is the `PascalCase` name of the case. 18 | - `#[php(value = "value")]` or `#[php(value = 123)]`: Sets the discriminant value for the enum case. 19 | This can be a string or an integer. If not set, the case will be exported as a simple enum case without a discriminant. 20 | 21 | ### Example 22 | 23 | This example creates a PHP enum `Suit`. 24 | 25 | ```rust,no_run 26 | # #![cfg_attr(windows, feature(abi_vectorcall))] 27 | # extern crate ext_php_rs; 28 | use ext_php_rs::prelude::*; 29 | 30 | #[php_enum] 31 | pub enum Suit { 32 | Hearts, 33 | Diamonds, 34 | Clubs, 35 | Spades, 36 | } 37 | 38 | #[php_module] 39 | pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { 40 | module.enumeration::() 41 | } 42 | # fn main() {} 43 | ``` 44 | 45 | ## Backed Enums 46 | Enums can also be backed by either `i64` or `&'static str`. Those values can be set using the 47 | `#[php(value = "value")]` or `#[php(value = 123)]` attributes on the enum variants. 48 | 49 | All variants must have a value of the same type, either all `i64` or all `&'static str`. 50 | 51 | ```rust,no_run 52 | # #![cfg_attr(windows, feature(abi_vectorcall))] 53 | # extern crate ext_php_rs; 54 | use ext_php_rs::prelude::*; 55 | 56 | #[php_enum] 57 | pub enum Suit { 58 | #[php(value = "hearts")] 59 | Hearts, 60 | #[php(value = "diamonds")] 61 | Diamonds, 62 | #[php(value = "clubs")] 63 | Clubs, 64 | #[php(value = "spades")] 65 | Spades, 66 | } 67 | #[php_module] 68 | pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { 69 | module.enumeration::() 70 | } 71 | # fn main() {} 72 | ``` 73 | 74 | ### 'Native' Discriminators 75 | Native rust discriminants are currently not supported and will not be exported to PHP. 76 | 77 | To avoid confusion a compiler error will be raised if you try to use a native discriminant. 78 | You can ignore this error by adding the `#[php(allow_native_discriminants)]` attribute to your enum. 79 | 80 | ```rust,no_run 81 | # #![cfg_attr(windows, feature(abi_vectorcall))] 82 | # extern crate ext_php_rs; 83 | use ext_php_rs::prelude::*; 84 | 85 | #[php_enum] 86 | #[php(allow_native_discriminants)] 87 | pub enum Suit { 88 | Hearts = 1, 89 | Diamonds = 2, 90 | Clubs = 3, 91 | Spades = 4, 92 | } 93 | 94 | #[php_module] 95 | pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { 96 | module.enumeration::() 97 | } 98 | # fn main() {} 99 | ``` 100 | 101 | 102 | TODO: Add backed enums example 103 | -------------------------------------------------------------------------------- /crates/cli/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.1.14](https://github.com/extphprs/ext-php-rs/compare/cargo-php-v0.1.13...cargo-php-v0.1.14) - 2025-12-06 4 | 5 | ### Other 6 | - *(deps)* Update libloading requirement from 0.8 to 0.9 (by @dependabot[bot]) 7 | - *(rust)* Bump Rust edition to 2024 (by @ptondereau) 8 | ## [0.1.13](https://github.com/extphprs/ext-php-rs/compare/cargo-php-v0.1.12...cargo-php-v0.1.13) - 2025-10-29 9 | 10 | ### Other 11 | - Change links for org move (by @Xenira) [[#500](https://github.com/davidcole1340/ext-php-rs/issues/500)] 12 | ## [0.1.12](https://github.com/extphprs/ext-php-rs/compare/cargo-php-v0.1.11...cargo-php-v0.1.12) - 2025-10-28 13 | 14 | ### Added 15 | - *(enum)* Add basic enum support (by @Xenira, @joehoyle) [[#178](https://github.com/extphprs/ext-php-rs/issues/178)] [[#302](https://github.com/extphprs/ext-php-rs/issues/302)] 16 | 17 | ### Other 18 | - *(clippy)* Fix new clippy errors (by @Xenira) [[#558](https://github.com/extphprs/ext-php-rs/issues/558)] 19 | - *(deps)* Update cargo_metadata requirement from 0.22 to 0.23 (by @dependabot[bot]) 20 | - *(deps)* Update dialoguer requirement from 0.11 to 0.12 (by @dependabot[bot]) 21 | - *(deps)* Update cargo_metadata requirement from 0.21 to 0.22 (by @dependabot[bot]) 22 | - *(deps)* Update cargo_metadata requirement from 0.20 to 0.21 (by @dependabot[bot]) 23 | - Update guide url and authors (by @Xenira) [[#500](https://github.com/extphprs/ext-php-rs/issues/500)] 24 | ## [0.1.11](https://github.com/extphprs/ext-php-rs/compare/cargo-php-v0.1.10...cargo-php-v0.1.11) - 2025-07-04 25 | 26 | ### Added 27 | - *(cargo-php)* --features, --all-features, --no-default-features (by @kakserpom) 28 | 29 | ### Fixed 30 | - *(cargo-php)* `get_ext_dir()`/`get_php_ini()` stdout noise tolerance (by @kakserpom) [[#459](https://github.com/extphprs/ext-php-rs/issues/459)] 31 | - *(clippy)* Fix new clippy findings (by @Xenira) 32 | 33 | ### Other 34 | - *(cargo-php)* Add locked option to install guide ([#370](https://github.com/extphprs/ext-php-rs/pull/370)) (by @Xenira) [[#370](https://github.com/extphprs/ext-php-rs/issues/370)] [[#314](https://github.com/extphprs/ext-php-rs/issues/314)] 35 | - *(cli)* Enforce docs for cli (by @Xenira) [[#392](https://github.com/extphprs/ext-php-rs/issues/392)] 36 | - *(clippy)* Apply pedantic rules (by @Xenira) [[#418](https://github.com/extphprs/ext-php-rs/issues/418)] 37 | - *(deps)* Update cargo_metadata requirement from 0.19 to 0.20 ([#437](https://github.com/extphprs/ext-php-rs/pull/437)) (by @dependabot[bot]) [[#437](https://github.com/extphprs/ext-php-rs/issues/437)] 38 | - *(deps)* Update cargo_metadata requirement from 0.15 to 0.19 ([#404](https://github.com/extphprs/ext-php-rs/pull/404)) (by @dependabot[bot]) [[#404](https://github.com/extphprs/ext-php-rs/issues/404)] 39 | - *(deps)* Update libloading requirement from 0.7 to 0.8 ([#389](https://github.com/extphprs/ext-php-rs/pull/389)) (by @dependabot[bot]) [[#389](https://github.com/extphprs/ext-php-rs/issues/389)] 40 | - *(deps)* Update dialoguer requirement from 0.10 to 0.11 ([#387](https://github.com/extphprs/ext-php-rs/pull/387)) (by @dependabot[bot]) [[#387](https://github.com/extphprs/ext-php-rs/issues/387)] 41 | 42 | ## [0.1.10](https://github.com/extphprs/ext-php-rs/compare/cargo-php-v0.1.9...cargo-php-v0.1.10) - 2025-02-06 43 | 44 | ### Other 45 | - *(release)* Add release bot (#346) (by @Xenira) [[#346](https://github.com/extphprs/ext-php-rs/issues/346)] [[#340](https://github.com/extphprs/ext-php-rs/issues/340)] 46 | - Don't use symbolic links for git. (by @faassen) 47 | - Fix pipeline (#320) (by @Xenira) [[#320](https://github.com/extphprs/ext-php-rs/issues/320)] 48 | -------------------------------------------------------------------------------- /tests/src/integration/variadic_args/mod.rs: -------------------------------------------------------------------------------- 1 | //! Rust type &[&Zval] must be converted to Vec because of 2 | //! lifetime hell. 3 | 4 | use ext_php_rs::{prelude::*, types::Zval}; 5 | 6 | #[php_function] 7 | pub fn test_variadic_args(params: &[&Zval]) -> Vec { 8 | params.iter().map(|x| x.shallow_clone()).collect() 9 | } 10 | 11 | #[php_function] 12 | pub fn test_variadic_add_required(number: u32, numbers: &[&Zval]) -> u32 { 13 | number 14 | + numbers 15 | .iter() 16 | .map(|x| u32::try_from(x.long().unwrap()).unwrap()) 17 | .sum::() 18 | } 19 | 20 | #[php_function] 21 | pub fn test_variadic_count(items: &[&Zval]) -> usize { 22 | items.len() 23 | } 24 | 25 | #[php_function] 26 | pub fn test_variadic_types(values: &[&Zval]) -> Vec { 27 | values 28 | .iter() 29 | .map(|v| { 30 | if v.is_long() { 31 | "long".to_string() 32 | } else if v.is_string() { 33 | "string".to_string() 34 | } else if v.is_double() { 35 | "double".to_string() 36 | } else if v.is_true() || v.is_false() { 37 | "bool".to_string() 38 | } else if v.is_array() { 39 | "array".to_string() 40 | } else if v.is_object() { 41 | "object".to_string() 42 | } else if v.is_null() { 43 | "null".to_string() 44 | } else { 45 | "unknown".to_string() 46 | } 47 | }) 48 | .collect() 49 | } 50 | 51 | #[php_function] 52 | pub fn test_variadic_strings(prefix: String, suffixes: &[&Zval]) -> Vec { 53 | suffixes 54 | .iter() 55 | .filter_map(|v| v.str()) 56 | .map(|s| format!("{prefix}{s}")) 57 | .collect() 58 | } 59 | 60 | #[php_function] 61 | pub fn test_variadic_sum_all(nums: &[&Zval]) -> i64 { 62 | nums.iter().filter_map(|v| v.long()).sum() 63 | } 64 | 65 | #[php_function] 66 | pub fn test_variadic_optional(required: String, optional: Option, extras: &[&Zval]) -> String { 67 | let opt_str = optional.map_or_else(|| "none".to_string(), |v| v.to_string()); 68 | format!("{required}-{opt_str}-{}", extras.len()) 69 | } 70 | 71 | #[php_function] 72 | pub fn test_variadic_empty_check(items: &[&Zval]) -> bool { 73 | items.is_empty() 74 | } 75 | 76 | #[php_function] 77 | pub fn test_variadic_first_last(items: &[&Zval]) -> Vec { 78 | let mut result = Vec::new(); 79 | if let Some(first) = items.first() { 80 | result.push(first.shallow_clone()); 81 | } 82 | if let Some(last) = items.last() 83 | && items.len() > 1 84 | { 85 | result.push(last.shallow_clone()); 86 | } 87 | result 88 | } 89 | 90 | pub fn build_module(builder: ModuleBuilder) -> ModuleBuilder { 91 | builder 92 | .function(wrap_function!(test_variadic_args)) 93 | .function(wrap_function!(test_variadic_add_required)) 94 | .function(wrap_function!(test_variadic_count)) 95 | .function(wrap_function!(test_variadic_types)) 96 | .function(wrap_function!(test_variadic_strings)) 97 | .function(wrap_function!(test_variadic_sum_all)) 98 | .function(wrap_function!(test_variadic_optional)) 99 | .function(wrap_function!(test_variadic_empty_check)) 100 | .function(wrap_function!(test_variadic_first_last)) 101 | } 102 | 103 | #[cfg(test)] 104 | mod tests { 105 | #[test] 106 | fn test_variadic_args() { 107 | assert!(crate::integration::test::run_php( 108 | "variadic_args/variadic_args.php" 109 | )); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /guide/src/macros/php.md: -------------------------------------------------------------------------------- 1 | # `#[php]` Attributes 2 | 3 | There are a number of attributes that can be used to annotate elements in your 4 | extension. 5 | 6 | Multiple `#[php]` attributes will be combined. For example, the following will 7 | be identical: 8 | 9 | ```rust,no_run 10 | # #![cfg_attr(windows, feature(abi_vectorcall))] 11 | # extern crate ext_php_rs; 12 | # use ext_php_rs::prelude::*; 13 | #[php_function] 14 | #[php(name = "hi_world")] 15 | #[php(defaults(a = 1, b = 2))] 16 | fn hello_world(a: i32, b: i32) -> i32 { 17 | a + b 18 | } 19 | # fn main() {} 20 | ``` 21 | 22 | ```rust,no_run 23 | # #![cfg_attr(windows, feature(abi_vectorcall))] 24 | # extern crate ext_php_rs; 25 | # use ext_php_rs::prelude::*; 26 | #[php_function] 27 | #[php(name = "hi_world", defaults(a = 1, b = 2))] 28 | fn hello_world(a: i32, b: i32) -> i32 { 29 | a + b 30 | } 31 | # fn main() {} 32 | ``` 33 | 34 | Which attributes are available depends on the element you are annotating: 35 | 36 | | Attribute | `const` | `fn` | `struct` | `struct` field | `impl` | `impl` `const` | `impl` `fn` | `enum` | `enum` case | 37 | | -------------------------- | ------- | ---- | -------- | -------------- | ------ | -------------- | ----------- | ------ | ----------- | 38 | | name | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | 39 | | change_case | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | 40 | | change_method_case | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | 41 | | change_constant_case | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | 42 | | flags | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | 43 | | prop | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | 44 | | extends | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | 45 | | implements | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | 46 | | modifier | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | 47 | | defaults | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | 48 | | optional | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | 49 | | vis | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | 50 | | getter | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | 51 | | setter | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | 52 | | constructor | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | 53 | | abstract_method | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | 54 | | allow_native_discriminants | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | 55 | | discriminant | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | 56 | 57 | ## `name` and `change_case` 58 | 59 | `name` and `change_case` are mutually exclusive. The `name` attribute is used to set the name of 60 | an item to a string literal. The `change_case` attribute is used to change the case of the name. 61 | 62 | ```rs 63 | #[php(name = "NEW_NAME")] 64 | #[php(change_case = snake_case)]] 65 | ``` 66 | 67 | Available cases are: 68 | - `snake_case` 69 | - `PascalCase` 70 | - `camelCase` 71 | - `UPPER_CASE` 72 | - `none` - No change 73 | -------------------------------------------------------------------------------- /src/alloc.rs: -------------------------------------------------------------------------------- 1 | //! Functions relating to the Zend Memory Manager, used to allocate 2 | //! request-bound memory. 3 | 4 | use cfg_if::cfg_if; 5 | 6 | use crate::ffi::{_efree, _emalloc, _estrdup}; 7 | use std::{ 8 | alloc::Layout, 9 | ffi::{CString, c_char, c_void}, 10 | }; 11 | 12 | /// Uses the PHP memory allocator to allocate request-bound memory. 13 | /// 14 | /// # Parameters 15 | /// 16 | /// * `layout` - The layout of the requested memory. 17 | /// 18 | /// # Returns 19 | /// 20 | /// A pointer to the memory allocated. 21 | #[must_use] 22 | pub fn emalloc(layout: Layout) -> *mut u8 { 23 | // TODO account for alignment 24 | let size = layout.size(); 25 | 26 | (unsafe { 27 | cfg_if! { 28 | if #[cfg(php_debug)] { 29 | #[allow(clippy::used_underscore_items)] 30 | _emalloc(size as _, std::ptr::null_mut(), 0, std::ptr::null_mut(), 0) 31 | } else { 32 | #[allow(clippy::used_underscore_items)] 33 | _emalloc(size as _) 34 | } 35 | } 36 | }) 37 | .cast::() 38 | } 39 | 40 | /// Frees a given memory pointer which was allocated through the PHP memory 41 | /// manager. 42 | /// 43 | /// # Parameters 44 | /// 45 | /// * `ptr` - The pointer to the memory to free. 46 | /// 47 | /// # Safety 48 | /// 49 | /// Caller must guarantee that the given pointer is valid (aligned and non-null) 50 | /// and was originally allocated through the Zend memory manager. 51 | pub unsafe fn efree(ptr: *mut u8) { 52 | cfg_if! { 53 | if #[cfg(php_debug)] { 54 | #[allow(clippy::used_underscore_items)] 55 | unsafe { 56 | _efree( 57 | ptr.cast::(), 58 | std::ptr::null_mut(), 59 | 0, 60 | std::ptr::null_mut(), 61 | 0, 62 | ); 63 | } 64 | } else { 65 | #[allow(clippy::used_underscore_items)] 66 | unsafe { 67 | _efree(ptr.cast::()); 68 | } 69 | } 70 | } 71 | } 72 | 73 | /// Duplicates a string using the PHP memory manager. 74 | /// 75 | /// # Parameters 76 | /// 77 | /// * `string` - The string to duplicate, which can be any type that can be 78 | /// converted into a `Vec`. 79 | /// 80 | /// # Returns 81 | /// 82 | /// A pointer to the duplicated string in the PHP memory manager. 83 | pub fn estrdup(string: impl Into>) -> *mut c_char { 84 | let string = unsafe { CString::from_vec_unchecked(string.into()) }.into_raw(); 85 | 86 | let result = unsafe { 87 | cfg_if! { 88 | if #[cfg(php_debug)] { 89 | #[allow(clippy::used_underscore_items)] 90 | _estrdup(string, std::ptr::null_mut(), 0, std::ptr::null_mut(), 0) 91 | } else { 92 | #[allow(clippy::used_underscore_items)] 93 | _estrdup(string) 94 | } 95 | } 96 | }; 97 | 98 | drop(unsafe { CString::from_raw(string) }); 99 | result 100 | } 101 | 102 | #[cfg(test)] 103 | #[cfg(feature = "embed")] 104 | mod test { 105 | use super::*; 106 | use crate::embed::Embed; 107 | use std::ffi::CStr; 108 | 109 | #[test] 110 | fn test_emalloc() { 111 | Embed::run(|| { 112 | let layout = Layout::from_size_align(16, 8).expect("should create layout"); 113 | let ptr = emalloc(layout); 114 | assert!(!ptr.is_null()); 115 | unsafe { efree(ptr) }; 116 | }); 117 | } 118 | 119 | #[test] 120 | fn test_estrdup() { 121 | Embed::run(|| { 122 | let original = "Hello, world!"; 123 | let duplicated = estrdup(original); 124 | assert!(!duplicated.is_null()); 125 | 126 | let duplicated_str = unsafe { CStr::from_ptr(duplicated) }; 127 | assert_eq!( 128 | duplicated_str.to_str().expect("should convert to str"), 129 | original 130 | ); 131 | 132 | unsafe { efree(duplicated.cast::()) } 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /guide/src/types/hashmap.md: -------------------------------------------------------------------------------- 1 | # `HashMap` 2 | 3 | `HashMap`s are represented as associative arrays in PHP. 4 | 5 | | `T` parameter | `&T` parameter | `T` Return type | `&T` Return type | PHP representation | 6 | | ------------- | -------------- | --------------- | ---------------- | ------------------ | 7 | | Yes | No | Yes | No | `ZendHashTable` | 8 | 9 | Converting from a zval to a `HashMap` is valid when the key is a `String`, and 10 | the value implements `FromZval`. The key and values are copied into Rust types 11 | before being inserted into the `HashMap`. If one of the key-value pairs has a 12 | numeric key, the key is represented as a string before being inserted. 13 | 14 | Converting from a `HashMap` to a zval is valid when the key implements 15 | `AsRef`, and the value implements `IntoZval`. 16 | 17 |
18 | 19 | When using `HashMap` the order of the elements it not preserved. 20 | 21 | HashMaps are unordered collections, so the order of elements may not be the same 22 | when converting from PHP to Rust and back. 23 | 24 | If you need to preserve the order of elements, consider using `Vec<(K, V)>` or 25 | `Vec` instead. 26 |
27 | 28 | ## Rust example 29 | 30 | ```rust,no_run 31 | # #![cfg_attr(windows, feature(abi_vectorcall))] 32 | # extern crate ext_php_rs; 33 | # use ext_php_rs::prelude::*; 34 | # use std::collections::HashMap; 35 | #[php_function] 36 | pub fn test_hashmap(hm: HashMap) -> Vec { 37 | for (k, v) in hm.iter() { 38 | println!("k: {} v: {}", k, v); 39 | } 40 | 41 | hm.into_iter() 42 | .map(|(_, v)| v) 43 | .collect::>() 44 | } 45 | # fn main() {} 46 | ``` 47 | 48 | ## PHP example 49 | 50 | ```php 51 | 'world', 55 | 'rust' => 'php', 56 | 'okk', 57 | ])); 58 | ``` 59 | 60 | Output: 61 | 62 | ```text 63 | k: rust v: php 64 | k: hello v: world 65 | k: 0 v: okk 66 | array(3) { 67 | [0] => string(3) "php", 68 | [1] => string(5) "world", 69 | [2] => string(3) "okk" 70 | } 71 | ``` 72 | 73 | ## `Vec<(K, V)>` and `Vec` 74 | 75 | `Vec<(K, V)>` and `Vec` are used to represent associative arrays in PHP 76 | where the keys can be strings or integers. 77 | 78 | If using `String` or `&str` as the key type, only string keys will be accepted. 79 | 80 | For `i64` keys, string keys that can be parsed as integers will be accepted, and 81 | converted to `i64`. 82 | 83 | If you need to accept both string and integer keys, use `ArrayKey` as the key type. 84 | 85 | ### Rust example 86 | 87 | ```rust,no_run 88 | # #![cfg_attr(windows, feature(abi_vectorcall))] 89 | # extern crate ext_php_rs; 90 | # use ext_php_rs::prelude::*; 91 | # use ext_php_rs::types::ArrayKey; 92 | #[php_function] 93 | pub fn test_vec_kv(vec: Vec<(String, String)>) -> Vec { 94 | for (k, v) in vec.iter() { 95 | println!("k: {} v: {}", k, v); 96 | } 97 | 98 | vec.into_iter() 99 | .map(|(_, v)| v) 100 | .collect::>() 101 | } 102 | 103 | #[php_function] 104 | pub fn test_vec_arraykey(vec: Vec<(ArrayKey, String)>) -> Vec { 105 | for (k, v) in vec.iter() { 106 | println!("k: {} v: {}", k, v); 107 | } 108 | 109 | vec.into_iter() 110 | .map(|(_, v)| v) 111 | .collect::>() 112 | } 113 | # fn main() {} 114 | ``` 115 | 116 | ## PHP example 117 | 118 | ```php 119 | string(5) "world", 144 | [1] => string(3) "php", 145 | [2] => string(3) "okk" 146 | } 147 | k: hello v: world 148 | k: 1 v: php 149 | k: 2 v: okk 150 | array(3) { 151 | [0] => string(5) "world", 152 | [1] => string(3) "php", 153 | [2] => string(3) "okk" 154 | } 155 | ``` 156 | -------------------------------------------------------------------------------- /tests/src/integration/variadic_args/variadic_args.php: -------------------------------------------------------------------------------- 1 | .expanded.rs` 46 | file will be created in the same directory. This file contains the expanded macro code. Verify that the 47 | expanded code is correct and that it matches the expected output. Commit the expanded file as well. 48 | 49 | If creating a new macro it needs to be added to the test contained at the bottom of the `crates/macros/src/lib.rs` file. 50 | 51 | ### State of unit tests 52 | There are still large parts of the library that are not covered by unit tests. We strive to cover 53 | as much as possible, but this is a work in progress. If you make changes to untested code, we would 54 | appreciate it if you could add tests for the code you changed. 55 | 56 | If this is not possible, or requires a lot of unrelated changes, you don't have to add tests. However, 57 | we would appreciate it if you are able to add those tests in a follow-up PR. 58 | 59 | ## Documentation 60 | 61 | Our documentation is located in the `guide` directory. 62 | If you update functionality, please ensure that the documentation is updated accordingly. 63 | 64 | ### Breaking Changes 65 | 66 | If you make a breaking change, please 67 | If your change is a [breaking change](https://semver.org) a migration guide MUST be included. This 68 | MUST be placed in the `guide/src/migration-guides` directory and named `v.md` (e.g. `v0.14.md`). 69 | This guide MUST also be linked in the `guide/src/SUMMARY.md` file under the `Migration Guides` section. 70 | 71 | ## Commit Messages 72 | 73 | We are using [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) to generate our changelogs. 74 | For that reason our tooling ensures that all commit messages adhere to that standard. 75 | 76 | To make this easier you can use [convco](https://convco.github.io) to generate commit messages. 77 | 78 | ## Use of AI tools 79 | 80 | - Using AI tools to generate Issues is NOT allowed. AI issues will be closed without comment. 81 | - Using AI tools to generate entire PRs is NOT allowed. 82 | - Using AI tools to generate short code snippets is allowed, but the contributor must review and understand 83 | the generated code. Think of it as a code completion tool. 84 | 85 | This is to ensure that the contributor has a good understanding of the code and its purpose. 86 | 87 | ## License 88 | 89 | Unless you explicitly state otherwise, any contribution intentionally submitted 90 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be 91 | dual licensed as described in the [README](README.md#license), without any additional terms or conditions. 92 | --------------------------------------------------------------------------------