Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion allowed_bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
3 changes: 3 additions & 0 deletions docsrs_bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2201,6 +2201,9 @@ unsafe extern "C" {
...
);
}
unsafe extern "C" {
pub fn php_output_write(str_: *const ::std::os::raw::c_char, len: usize) -> usize;
}
pub type php_stream = _php_stream;
pub type php_stream_wrapper = _php_stream_wrapper;
pub type php_stream_context = _php_stream_context;
Expand Down
1 change: 1 addition & 0 deletions guide/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
136 changes: 136 additions & 0 deletions guide/src/output.md
Original file line number Diff line number Diff line change
@@ -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<usize>` 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.
94 changes: 94 additions & 0 deletions src/embed/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
}
}
5 changes: 5 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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")
}
}
}
}
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
79 changes: 79 additions & 0 deletions src/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<usize>` 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) }};
}
Loading