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_stringtriggers 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 frominclude_bytes!orinclude_str!. Any type implementingAsRef<[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.
| Input | Handling |
|---|---|
<?php opening tag | Required (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:
| Hook | Use case |
|---|---|
RINIT (request_startup_function) | Per-request setup (classic php-fpm / mod_php) |
MINIT (startup_function) | One-time global setup |
| Custom SAPI callback | Worker-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
-
Build time:
include_bytes!embeds the.phpfile contents into the extension binary as a&[u8]constant. -
Runtime:
php_eval::execute()strips the<?phptag and BOM, then calls two C wrapper functions inwrapper.c:ext_php_rs_zend_compile_string– compiles the source into an op_array. On PHP 8.2+ it passesZEND_COMPILE_POSITION_AFTER_OPEN_TAGso 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.
-
Safety: The entire execution is wrapped in
try_catchto catch PHP bailouts (longjmp) without unwinding the Rust stack. Error reporting is suppressed during execution and restored afterward.