Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Embedded PHP Execution

Extensions sometimes need to execute PHP code at runtime for setup tasks like registering autoloaders, defining helper classes, or configuring error handlers. The php_eval module lets you embed .php files into your extension binary at compile time and execute them from Rust.

Why Not eval?

This module uses zend_compile_string + zend_execute instead of zend_eval_string because:

  • zend_eval_string triggers security scanner false positives (eval-like semantics)
  • Some hardened PHP builds disable eval-like functionality
  • The embedded code is static bytes in the binary – there is no injection risk

Basic Usage

1. Write your PHP file

Create a normal .php file with full IDE support (syntax highlighting, linting, static analysis):

<?php
// php/setup.php

spl_autoload_register(function (string $class): void {
    $prefix = 'Acme\\Encryption\\';
    if (str_starts_with($class, $prefix)) {
        $relative = substr($class, strlen($prefix));
        $file = __DIR__ . '/src/' . str_replace('\\', '/', $relative) . '.php';
        if (file_exists($file)) {
            require $file;
        }
    }
});

function acme_encrypt_version(): string {
    return '1.0.0';
}

2. Embed and execute from Rust

Use include_bytes! or include_str! to embed the file at compile time, then call php_eval::execute() from whatever lifecycle hook fits your target SAPI. The function accepts any type that implements AsRef<[u8]>:

use ext_php_rs::prelude::*;
use ext_php_rs::php_eval;

// Both forms work:
const SETUP: &[u8] = include_bytes!("../php/setup.php");
// const SETUP: &str = include_str!("../php/setup.php");

unsafe extern "C" fn on_request_start(
    _type: i32,
    _module_number: i32,
) -> i32 {
    if let Err(e) = php_eval::execute(SETUP) {
        eprintln!("Failed to run embedded PHP setup: {:?}", e);
    }
    0
}

#[php_module]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
    module.request_startup_function(on_request_start)
}

API Reference

php_eval::execute(code: impl AsRef<[u8]>) -> Result<(), PhpEvalError>

Compiles and executes the given PHP source within the running PHP engine.

Arguments:

  • code – Raw PHP source, typically from include_bytes! or include_str!. Any type implementing AsRef<[u8]> is accepted (&[u8], &str, String, Vec<u8>, etc.).

Returns:

  • Ok(()) on success.
  • Err(PhpEvalError::MissingOpenTag) if the code does not start with <?php (case-insensitive).
  • Err(PhpEvalError::CompilationFailed) if PHP cannot compile the code (syntax error).
  • Err(PhpEvalError::ExecutionFailed) if the code throws an unhandled exception.
  • Err(PhpEvalError::Bailout) if a PHP fatal error occurs.

Input handling

The code must start with a <?php opening tag (case-insensitive). The tag is stripped before compilation.

InputHandling
<?php opening tagRequired (case-insensitive), stripped before compilation
UTF-8 BOM (0xEF 0xBB 0xBF)Stripped before compilation
Empty after tag (e.g. <?php)Returns Ok(()) immediately
No <?php tag (including empty input)Returns Err(MissingOpenTag)

Lifecycle Hooks

The module does not prescribe when to run embedded PHP. The SAPI landscape is fragmented – FrankenPHP in worker mode does not trigger RINIT per request, for example. Choose the hook that fits your target:

HookUse case
RINIT (request_startup_function)Per-request setup (classic php-fpm / mod_php)
MINIT (startup_function)One-time global setup
Custom SAPI callbackWorker-mode runtimes (FrankenPHP, RoadRunner)

Error Handling

Errors during embedded PHP execution should not crash the host process. The recommended pattern is to log and continue:

if let Err(e) = php_eval::execute(SETUP_CODE) {
    match e {
        PhpEvalError::MissingOpenTag => {
            eprintln!("embedded PHP missing <?php open tag");
        }
        PhpEvalError::CompilationFailed => {
            eprintln!("embedded PHP syntax error");
        }
        PhpEvalError::ExecutionFailed => {
            eprintln!("embedded PHP threw an exception");
        }
        PhpEvalError::Bailout => {
            eprintln!("embedded PHP fatal error");
        }
    }
}

How It Works

  1. Build time: include_bytes! embeds the .php file contents into the extension binary as a &[u8] constant.

  2. Runtime: php_eval::execute() strips the <?php tag and BOM, then calls two C wrapper functions in wrapper.c:

    • ext_php_rs_zend_compile_string – compiles the source into an op_array. On PHP 8.2+ it passes ZEND_COMPILE_POSITION_AFTER_OPEN_TAG so the scanner starts directly in PHP mode. On PHP 8.1 the two-argument form is used.
    • ext_php_rs_zend_execute – executes the op_array, sets the execution scope, then cleans up static vars and frees the op_array.
  3. Safety: The entire execution is wrapped in try_catch to catch PHP bailouts (longjmp) without unwinding the Rust stack. Error reporting is suppressed during execution and restored afterward.