From e8c42e1e4d60ac35a421273162c4044df7f3d7f7 Mon Sep 17 00:00:00 2001 From: Vasily Zorin Date: Mon, 22 Dec 2025 21:31:50 +0700 Subject: [PATCH] feat(php_write): A binary-safe way to write to PHP's stdout/stderr #508 --- allowed_bindings.rs | 3 +- guide/src/SUMMARY.md | 1 + guide/src/output.md | 136 +++++++++++++++++++++++++++++++++++++++++++ src/embed/mod.rs | 94 ++++++++++++++++++++++++++++++ src/error.rs | 5 ++ src/lib.rs | 1 + src/macros.rs | 79 +++++++++++++++++++++++++ src/zend/mod.rs | 80 ++++++++++++++++++++++++- 8 files changed, 397 insertions(+), 2 deletions(-) create mode 100644 guide/src/output.md diff --git a/allowed_bindings.rs b/allowed_bindings.rs index 42dbb87501..b1596ad115 100644 --- a/allowed_bindings.rs +++ b/allowed_bindings.rs @@ -339,5 +339,6 @@ bind! { php_ini_builder_prepend, php_ini_builder_unquoted, php_ini_builder_quoted, - php_ini_builder_define + php_ini_builder_define, + php_output_write } diff --git a/guide/src/SUMMARY.md b/guide/src/SUMMARY.md index 626cad84c4..914918618e 100644 --- a/guide/src/SUMMARY.md +++ b/guide/src/SUMMARY.md @@ -35,6 +35,7 @@ - [`ZvalConvert`](./macros/zval_convert.md) - [`Attributes`](./macros/php.md) - [Exceptions](./exceptions.md) +- [Output](./output.md) - [INI Settings](./ini-settings.md) - [Superglobals](./superglobals.md) diff --git a/guide/src/output.md b/guide/src/output.md new file mode 100644 index 0000000000..23bb8181f2 --- /dev/null +++ b/guide/src/output.md @@ -0,0 +1,136 @@ +# Output + +`ext-php-rs` provides several macros and functions for writing output to PHP's +stdout and stderr streams. These are essential when your extension needs to +produce output that integrates with PHP's output buffering system. + +## Text Output + +For regular text output (strings without NUL bytes), use the `php_print!` and +`php_println!` macros. These work similarly to Rust's `print!` and `println!` +macros. + +### `php_print!` + +Prints to PHP's standard output without a trailing newline. + +```rust,ignore +use ext_php_rs::prelude::*; + +#[php_function] +pub fn greet(name: &str) { + php_print!("Hello, {}!", name); +} +``` + +### `php_println!` + +Prints to PHP's standard output with a trailing newline. + +```rust,ignore +use ext_php_rs::prelude::*; + +#[php_function] +pub fn greet(name: &str) { + php_println!("Hello, {}!", name); +} +``` + +> **Note:** `php_print!` and `php_println!` will panic if the string contains +> NUL bytes (`\0`). For binary-safe output, use `php_output!` or `php_write!`. + +## Binary-Safe Output + +When working with binary data that may contain NUL bytes, use the binary-safe +output functions. These are essential for outputting raw bytes, binary file +contents, or any data that might contain `\0` characters. + +### `php_output!` + +Writes binary data to PHP's output stream. This macro is **both binary-safe AND +respects PHP's output buffering** (`ob_start()`). This is usually what you want +for binary output. + +```rust,ignore +use ext_php_rs::prelude::*; + +#[php_function] +pub fn output_binary() -> i64 { + // Write binary data with NUL bytes - will be captured by ob_start() + let bytes_written = php_output!(b"Hello\x00World"); + bytes_written as i64 +} +``` + +### `php_write!` + +Writes binary data directly to the SAPI output, **bypassing PHP's output +buffering**. This macro is binary-safe but output will NOT be captured by +`ob_start()`. The "ub" in `ub_write` stands for "unbuffered". + +```rust,ignore +use ext_php_rs::prelude::*; + +#[php_function] +pub fn output_binary() -> i64 { + // Write a byte literal + php_write!(b"Hello World").expect("write failed"); + + // Write binary data with NUL bytes (would panic with php_print!) + let bytes_written = php_write!(b"Hello\x00World").expect("write failed"); + + // Write a byte slice + let data: &[u8] = &[0x48, 0x65, 0x6c, 0x6c, 0x6f]; // "Hello" + php_write!(data).expect("write failed"); + + bytes_written as i64 +} +``` + +The macro returns a `Result` with the number of bytes written, which can +be useful for verifying that all data was output successfully. The error case +occurs when the SAPI's `ub_write` function is not available. + +## Function API + +In addition to macros, you can use the underlying functions directly: + +| Function | Binary-Safe | Output Buffering | Description | +|----------|-------------|------------------|-------------| +| `zend::printf()` | No | Yes | Printf-style output (used by `php_print!`) | +| `zend::output_write()` | Yes | Yes | Binary-safe buffered output | +| `zend::write()` | Yes | No | Binary-safe unbuffered output | + +### Example using functions directly + +```rust,ignore +use ext_php_rs::zend::output_write; + +fn output_data(data: &[u8]) { + let bytes_written = output_write(data); + if bytes_written != data.len() { + eprintln!("Warning: incomplete write"); + } +} +``` + +## Comparison + +| Macro | Binary-Safe | Output Buffering | Supports Formatting | +|-------|-------------|------------------|---------------------| +| `php_print!` | No | Yes | Yes | +| `php_println!` | No | Yes | Yes | +| `php_output!` | Yes | Yes | No | +| `php_write!` | Yes | No | No | + +## When to Use Each + +- **`php_print!` / `php_println!`**: Use for text output with format strings, + similar to Rust's `print!` and `println!`. Best for human-readable messages. + +- **`php_output!`**: Use for binary data that needs to work with PHP's output + buffering. This is the recommended choice for most binary output needs. + +- **`php_write!`**: Use when you need direct, unbuffered output that bypasses + PHP's output layer. Useful for low-level SAPI interaction or when output + buffering must be avoided. diff --git a/src/embed/mod.rs b/src/embed/mod.rs index 97df7e0aa0..6dadb37d17 100644 --- a/src/embed/mod.rs +++ b/src/embed/mod.rs @@ -297,4 +297,98 @@ mod tests { assert!(result.unwrap_err().is_bailout()); }); } + + #[test] + fn test_php_write() { + use crate::zend::write; + + Embed::run(|| { + // Test write function with regular data + let bytes_written = write(b"Hello").expect("write failed"); + assert_eq!(bytes_written, 5); + + // Test write function with binary data containing NUL bytes + let bytes_written = write(b"Hello\x00World").expect("write failed"); + assert_eq!(bytes_written, 11); + + // Test php_write! macro with byte literal + let bytes_written = php_write!(b"Test").expect("php_write failed"); + assert_eq!(bytes_written, 4); + + // Test php_write! macro with binary data containing NUL bytes + let bytes_written = php_write!(b"Binary\x00Data\x00Here").expect("php_write failed"); + assert_eq!(bytes_written, 16); + + // Test php_write! macro with byte slice variable + let data: &[u8] = &[0x48, 0x65, 0x6c, 0x6c, 0x6f]; // "Hello" + let bytes_written = php_write!(data).expect("php_write failed"); + assert_eq!(bytes_written, 5); + + // Test empty data + let bytes_written = write(b"").expect("write failed"); + assert_eq!(bytes_written, 0); + }); + } + + #[test] + fn test_php_write_bypasses_output_buffering() { + use crate::zend::write; + + Embed::run(|| { + // Start PHP output buffering + Embed::eval("ob_start();").expect("ob_start failed"); + + // Write data using ub_write - this bypasses output buffering + // ("ub" = unbuffered) and goes directly to SAPI output + write(b"Direct output").expect("write failed"); + + // Get the buffered output - should be empty since ub_write bypasses buffering + let result = Embed::eval("ob_get_clean();").expect("ob_get_clean failed"); + let output = result.string().expect("expected string result"); + + // Verify that ub_write bypasses output buffering + assert_eq!(output, "", "ub_write should bypass output buffering"); + }); + } + + #[test] + fn test_php_print_respects_output_buffering() { + use crate::zend::printf; + + Embed::run(|| { + // Start PHP output buffering + Embed::eval("ob_start();").expect("ob_start failed"); + + // Write data using php_printf - this goes through output buffering + printf("Hello from Rust").expect("printf failed"); + + // Get the buffered output + let result = Embed::eval("ob_get_clean();").expect("ob_get_clean failed"); + let output = result.string().expect("expected string result"); + + // Verify that printf output is captured by output buffering + assert_eq!(output, "Hello from Rust"); + }); + } + + #[test] + fn test_php_output_write_binary_safe_with_buffering() { + use crate::zend::output_write; + + Embed::run(|| { + // Start PHP output buffering + Embed::eval("ob_start();").expect("ob_start failed"); + + // Write binary data with NUL bytes - should be captured by buffer + let bytes_written = output_write(b"Hello\x00World"); + assert_eq!(bytes_written, 11); + + // Get the buffered output + let result = Embed::eval("ob_get_clean();").expect("ob_get_clean failed"); + let output = result.string().expect("expected string result"); + + // Verify binary data was captured correctly (including NUL byte) + assert_eq!(output, "Hello\x00World"); + }); + } } diff --git a/src/error.rs b/src/error.rs index 9bd2e178ef..66bc9257ac 100644 --- a/src/error.rs +++ b/src/error.rs @@ -70,6 +70,8 @@ pub enum Error { StreamWrapperRegistrationFailure, /// A failure occurred while unregistering the stream wrapper StreamWrapperUnregistrationFailure, + /// The SAPI write function is not available + SapiWriteUnavailable, } impl Display for Error { @@ -113,6 +115,9 @@ impl Display for Error { "A failure occurred while unregistering the stream wrapper" ) } + Error::SapiWriteUnavailable => { + write!(f, "The SAPI write function is not available") + } } } } diff --git a/src/lib.rs b/src/lib.rs index 3da2710b05..1f9c53a53e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -54,6 +54,7 @@ pub mod prelude { pub use crate::php_enum; pub use crate::php_print; pub use crate::php_println; + pub use crate::php_write; pub use crate::types::ZendCallable; pub use crate::{ ZvalConvert, php_class, php_const, php_extern, php_function, php_impl, php_interface, diff --git a/src/macros.rs b/src/macros.rs index 88d6bf8254..1482de8ce9 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -425,3 +425,82 @@ macro_rules! php_println { $crate::php_print!(concat!($fmt, "\n"), $($arg)*); }; } + +/// Writes binary data to the PHP standard output. +/// +/// Unlike [`php_print!`], this macro is binary-safe and can handle data +/// containing `NUL` bytes. It uses the SAPI module's `ub_write` function. +/// +/// # Arguments +/// +/// * `$data` - A byte slice (`&[u8]`) or byte literal (`b"..."`) to write. +/// +/// # Returns +/// +/// A `Result` containing the number of bytes written. +/// +/// # Errors +/// +/// Returns [`Error::SapiWriteUnavailable`] if the SAPI's `ub_write` function +/// is not available. +/// +/// [`Error::SapiWriteUnavailable`]: crate::error::Error::SapiWriteUnavailable +/// +/// # Examples +/// +/// ```ignore +/// use ext_php_rs::php_write; +/// +/// // Write a byte literal +/// php_write!(b"Hello World").expect("write failed"); +/// +/// // Write binary data with NUL bytes (would panic with php_print!) +/// php_write!(b"Hello\x00World").expect("write failed"); +/// +/// // Write a byte slice +/// let data: &[u8] = &[0x48, 0x65, 0x6c, 0x6c, 0x6f]; +/// php_write!(data).expect("write failed"); +/// ``` +#[macro_export] +macro_rules! php_write { + ($data: expr) => {{ $crate::zend::write($data) }}; +} + +/// Writes binary data to PHP's output stream with output buffering support. +/// +/// This macro is both binary-safe (can handle `NUL` bytes) AND respects PHP's +/// output buffering (`ob_start()`). Use this when you need both capabilities. +/// +/// # Arguments +/// +/// * `$data` - A byte slice (`&[u8]`) or byte literal (`b"..."`) to write. +/// +/// # Returns +/// +/// The number of bytes written. +/// +/// # Comparison +/// +/// | Macro | Binary-safe | Output Buffering | +/// |-------|-------------|------------------| +/// | [`php_print!`] | No | Yes | +/// | [`php_write!`] | Yes | No (unbuffered) | +/// | [`php_output!`] | Yes | Yes | +/// +/// # Examples +/// +/// ```ignore +/// use ext_php_rs::php_output; +/// +/// // Write binary data that will be captured by ob_start() +/// php_output!(b"Hello\x00World"); +/// +/// // Use with output buffering +/// // ob_start(); +/// // php_output!(b"captured"); +/// // $data = ob_get_clean(); // Contains "captured" +/// ``` +#[macro_export] +macro_rules! php_output { + ($data: expr) => {{ $crate::zend::output_write($data) }}; +} diff --git a/src/zend/mod.rs b/src/zend/mod.rs index 02d44b67ef..107e14c881 100644 --- a/src/zend/mod.rs +++ b/src/zend/mod.rs @@ -15,9 +15,10 @@ mod try_catch; use crate::{ error::Result, - ffi::{php_printf, sapi_module}, + ffi::{php_output_write, php_printf, sapi_module}, }; use std::ffi::CString; +use std::os::raw::c_char; pub use _type::ZendType; pub use class::ClassEntry; @@ -62,6 +63,83 @@ pub fn printf(message: &str) -> Result<()> { Ok(()) } +/// Writes binary data to PHP's output stream (stdout). +/// +/// Unlike [`printf`], this function is binary-safe and can handle data +/// containing NUL bytes. It uses the SAPI module's `ub_write` function +/// which accepts a pointer and length, allowing arbitrary binary data. +/// +/// Also see the [`php_write!`] macro. +/// +/// # Arguments +/// +/// * `data` - The binary data to write to stdout. +/// +/// # Returns +/// +/// The number of bytes written. +/// +/// # Errors +/// +/// Returns [`Error::SapiWriteUnavailable`] if the SAPI's `ub_write` function +/// is not available. +/// +/// # Example +/// +/// ```ignore +/// use ext_php_rs::zend::write; +/// +/// // Write binary data including NUL bytes +/// let data = b"Hello\x00World"; +/// write(data).expect("Failed to write data"); +/// ``` +pub fn write(data: &[u8]) -> Result { + unsafe { + if let Some(ub_write) = sapi_module.ub_write { + Ok(ub_write(data.as_ptr().cast::(), data.len())) + } else { + Err(crate::error::Error::SapiWriteUnavailable) + } + } +} + +/// Writes binary data to PHP's output stream with output buffering support. +/// +/// This function is binary-safe (can handle NUL bytes) AND respects PHP's +/// output buffering (`ob_start()`). Use this when you need both binary-safe +/// output and output buffering compatibility. +/// +/// # Arguments +/// +/// * `data` - The binary data to write. +/// +/// # Returns +/// +/// The number of bytes written. +/// +/// # Comparison +/// +/// | Function | Binary-safe | Output Buffering | +/// |----------|-------------|------------------| +/// | [`printf`] | No | Yes | +/// | [`write`] | Yes | No (unbuffered) | +/// | [`output_write`] | Yes | Yes | +/// +/// # Example +/// +/// ```ignore +/// use ext_php_rs::zend::output_write; +/// +/// // Binary data that will be captured by ob_start() +/// let data = b"Hello\x00World"; +/// output_write(data); +/// ``` +#[inline] +#[must_use] +pub fn output_write(data: &[u8]) -> usize { + unsafe { php_output_write(data.as_ptr().cast::(), data.len()) } +} + /// Get the name of the SAPI module. /// /// # Panics