ext-php-rs

repository·master·Indexed 21 days ago

https://github.com/extphprs/ext-php-rs

Bindings for the Zend API to build high-performance PHP extensions natively in Rust. It provides abstractions over the Zend API, allowing the use of standard Rust types and custom types via IntoZval and FromZval traits. The library includes the cargo-php CLI tool for managing the extension lifecycle, including installation, removal, and the generation of PHP stub files for IDE typehinting. Supports PHP 8.1+ on Linux, macOS, and Windows.

Tokens
71.1K
Snippets
218
Records
270
Agent score
73%

What's inside ext-php-rs

  1. Use cargo-php subcommands

    master

    The cargo-php CLI provides three main subcommands for managing your PHP extensions:

    • install: Copies the extension to your PHP installation and enables it in a php.ini file.
    • remove: Deletes the extension from the PHP installation and removes it from the configuration file.
    • stubs: Generates PHP stub files to provide typehinting for extension classes, functions, and constants in IDEs.
    $ cargo php <SUBCOMMAND>
  2. Use ext-php-rs macros to export Rust to PHP

    master

    The ext-php-rs crate provides a suite of procedural macros used to annotate Rust types and functions so they can be exported to PHP. These macros allow you to define PHP modules, startup functions, classes, and functions without manually manipulating PHP zvals.

    Key macros include:

    • #[php_module]: Defines the entry point function PHP uses to retrieve your extension.
    • #[php_startup]: Defines the initialization function called when the extension starts.
    • #[php_function]: Exports a Rust function as a PHP function.
    • #[php_class]: Exports a Rust struct or enum as a PHP class.
    • #[php_impl]: Exports a Rust impl block, including its methods and constants, to PHP.
    • #[php_const]: Exports a Rust constant as a global PHP constant.
    • #[php_interface]: Exports a Rust trait as a PHP interface.
    • #[php_extern]: Annotates extern blocks containing PHP functions.
    • #[php]: A generic attribute used to modify the default behavior of most other macros.
  3. What is `ZendHashTable` and when to use it

    master

    ZendHashTable is the internal representation of PHP arrays. While Vec and HashMap are automatically converted to/from ZendHashTable, you should use ZendHashTable directly when:

    • You need to modify a PHP array in place without copying data.
    • You are working with arrays passed by reference.
    • You require fine-grained control over array operations.
    • You are implementing custom iterators or data structures.
  4. Use BinarySlice to read PHP strings as Rust slices

    master

    In ext-php-rs, binary data (typically generated via PHP's pack or unpack functions) is represented as a string in PHP. You can use BinarySlice<T> in Rust to treat these PHP strings as read-only slices of type T without copying the underlying data.

    Requirements

    To use BinarySlice<T>, the type T must implement the PackSlice trait. This is currently implemented for most primitive numbers:

    • Signed integers: i8, i16, i32, i64, isize
    • Unsigned integers: u8, u16, u32, u64, usize
    • Floats: f32, f64

    Mapping Table

    T parameter&T parameterT Return type&T Return typePHP representation
    YesNoNoNozend_string

    Implementation Detail

    The data is converted into a slice, and the pointer to the data is set as the string pointer, with the length of the array being the length of the string.

    #![cfg_attr(windows, feature(abi_vectorcall))]
    extern crate ext_php_rs;
    use ext_php_rs::prelude::*;
    use ext_php_rs::binary_slice::BinarySlice;
    
    #[php_function]
    pub fn test_binary_slice(input: BinarySlice<u8>) -> u8 {
        let mut sum = 0;
        for i in input.iter() {
            sum += i;
        }
    
        sum
    }
    
    fn main() {}
  5. Configure Naming and Case Styles in v0.14

    master

    In v0.14, default casing follows PSR standards:

    • Classes: PascalCase
    • Properties: camelCase
    • Methods: camelCase
    • Constants: UPPER_CASE
    • Functions: snake_case

    You can override these using the change_case attribute.

    Important distinction:

    • #[php(name = "NEW_NAME")]: Sets the literal name of the item.
    • #[php(change_case = case)]: Transforms the existing name into the specified case.

    Available cases:

    • snake_case
    • PascalCase
    • camelCase
    • UPPER_CASE
    • none (no change)
  6. Choose between `Separated` and `PhpRef` for Zval ownership

    master

    When writing PHP functions in Rust, you must decide how to handle variable ownership and mutation. Using &mut Zval directly is considered legacy and forces PHP callers to use pass-by-reference syntax (&$x), which prevents passing literals (e.g., foo([1,2,3])).

    To decouple Rust mutability from PHP's pass-by-reference semantics, use Separated or PhpRef:

    TypePHP syntaxModifies caller?Use case
    Separatedfoo($x) or foo([1,2])NoMutate a local copy using Copy-on-Write (COW)
    PhpReffoo(&$x)YesModify the original variable in the caller's scope
    &Zvalfoo($x)NoRead-only access
    &mut Zvalfoo(&$x)YesLegacy (avoid)
    | Type | PHP syntax | Modifies caller? | Use case |
    |---|---|---|---|
    | `Separated` | `foo($x)` or `foo([1,2])` | No | Mutate a local copy (COW) |
    | `PhpRef` | `foo(&$x)` | Yes | Modify the caller's variable |
    | `&Zval` | `foo($x)` | No | Read-only access |
    | `&mut Zval` | `foo(&$x)` | Yes | Legacy — prefer `PhpRef` |
  7. Thread Safety requirements for Observers

    master

    Observers are created once during MINIT and stored as global singletons. Because they are global, they must implement Send + Sync to support both Non-Thread Safe (NTS) and Zend Thread Safe (ZTS) environments.

    • NTS: A single instance handles all requests.
    • ZTS: The same instance may be called from different threads.

    Requirement: Use thread-safe primitives such as AtomicU64, Mutex, or RwLock if your observer needs to maintain mutable state.

  8. Versioning and stability considerations

    master

    ext-php-rs follows semantic versioning. However, because the project is currently in major version 0, no backwards compatibility is guaranteed.

    Recommendation: Lock your dependency to the patch level to ensure stability. When breaking changes are introduced, migration guides will be provided within the documentation.

  9. Prevent resource leaks during PHP bailouts

    master

    When PHP triggers a 'bailout' (via exit(), die(), or a fatal error), it uses longjmp to unwind the stack. This bypasses Rust's normal drop semantics, meaning destructors for stack-allocated values will not run, potentially causing resource leaks (e.g., file handles, network connections, or locks).

    You can prevent these leaks using two primary methods:

    1. try_call: Use this for PHP callbacks. It catches bailouts internally and returns a Result, allowing the Rust function to return normally and trigger standard destructors.
    2. BailoutGuard: Use this for critical resources that must be cleaned up even if a bailout occurs directly (not through a try_call catch).
    #[php_function]
    pub fn process_file(callback: ZendCallable) {
        let file = File::open("data.txt").unwrap();
    
        // If callback calls exit(), the file handle leaks!
        callback.try_call(vec![]);
    
        // file.drop() never runs
    }
  10. How `FnOnce` closures work in ext-php-rs

    master

    Closures that implement FnOnce can only be called once because they consume values from their environment.

    Usage:

    • You must use Closure::wrap_once instead of Closure::wrap.
    • If a user attempts to call an FnOnce closure more than once in PHP, an exception will be thrown.

    Internally, ext-php-rs wraps the FnOnce closure in an FnMut closure that owns it until the first call.

    use ext_php_rs::prelude::*;
    
    #[php_function]
    pub fn closure_return_string() -> Closure {
        let example: String = "Hello, world!".into();
    
        // This closure consumes `example` and therefore cannot be called more than once.
        Closure::wrap_once(Box::new(move || {
            example
        }) as Box<dyn FnOnce() -> String>)
    }