Skip to content

Commit e8c42e1

Browse files
committed
feat(php_write): A binary-safe way to write to PHP's stdout/stderr #508
1 parent 6523879 commit e8c42e1

File tree

8 files changed

+397
-2
lines changed

8 files changed

+397
-2
lines changed

allowed_bindings.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -339,5 +339,6 @@ bind! {
339339
php_ini_builder_prepend,
340340
php_ini_builder_unquoted,
341341
php_ini_builder_quoted,
342-
php_ini_builder_define
342+
php_ini_builder_define,
343+
php_output_write
343344
}

guide/src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
- [`ZvalConvert`](./macros/zval_convert.md)
3636
- [`Attributes`](./macros/php.md)
3737
- [Exceptions](./exceptions.md)
38+
- [Output](./output.md)
3839
- [INI Settings](./ini-settings.md)
3940
- [Superglobals](./superglobals.md)
4041

guide/src/output.md

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
# Output
2+
3+
`ext-php-rs` provides several macros and functions for writing output to PHP's
4+
stdout and stderr streams. These are essential when your extension needs to
5+
produce output that integrates with PHP's output buffering system.
6+
7+
## Text Output
8+
9+
For regular text output (strings without NUL bytes), use the `php_print!` and
10+
`php_println!` macros. These work similarly to Rust's `print!` and `println!`
11+
macros.
12+
13+
### `php_print!`
14+
15+
Prints to PHP's standard output without a trailing newline.
16+
17+
```rust,ignore
18+
use ext_php_rs::prelude::*;
19+
20+
#[php_function]
21+
pub fn greet(name: &str) {
22+
php_print!("Hello, {}!", name);
23+
}
24+
```
25+
26+
### `php_println!`
27+
28+
Prints to PHP's standard output with a trailing newline.
29+
30+
```rust,ignore
31+
use ext_php_rs::prelude::*;
32+
33+
#[php_function]
34+
pub fn greet(name: &str) {
35+
php_println!("Hello, {}!", name);
36+
}
37+
```
38+
39+
> **Note:** `php_print!` and `php_println!` will panic if the string contains
40+
> NUL bytes (`\0`). For binary-safe output, use `php_output!` or `php_write!`.
41+
42+
## Binary-Safe Output
43+
44+
When working with binary data that may contain NUL bytes, use the binary-safe
45+
output functions. These are essential for outputting raw bytes, binary file
46+
contents, or any data that might contain `\0` characters.
47+
48+
### `php_output!`
49+
50+
Writes binary data to PHP's output stream. This macro is **both binary-safe AND
51+
respects PHP's output buffering** (`ob_start()`). This is usually what you want
52+
for binary output.
53+
54+
```rust,ignore
55+
use ext_php_rs::prelude::*;
56+
57+
#[php_function]
58+
pub fn output_binary() -> i64 {
59+
// Write binary data with NUL bytes - will be captured by ob_start()
60+
let bytes_written = php_output!(b"Hello\x00World");
61+
bytes_written as i64
62+
}
63+
```
64+
65+
### `php_write!`
66+
67+
Writes binary data directly to the SAPI output, **bypassing PHP's output
68+
buffering**. This macro is binary-safe but output will NOT be captured by
69+
`ob_start()`. The "ub" in `ub_write` stands for "unbuffered".
70+
71+
```rust,ignore
72+
use ext_php_rs::prelude::*;
73+
74+
#[php_function]
75+
pub fn output_binary() -> i64 {
76+
// Write a byte literal
77+
php_write!(b"Hello World").expect("write failed");
78+
79+
// Write binary data with NUL bytes (would panic with php_print!)
80+
let bytes_written = php_write!(b"Hello\x00World").expect("write failed");
81+
82+
// Write a byte slice
83+
let data: &[u8] = &[0x48, 0x65, 0x6c, 0x6c, 0x6f]; // "Hello"
84+
php_write!(data).expect("write failed");
85+
86+
bytes_written as i64
87+
}
88+
```
89+
90+
The macro returns a `Result<usize>` with the number of bytes written, which can
91+
be useful for verifying that all data was output successfully. The error case
92+
occurs when the SAPI's `ub_write` function is not available.
93+
94+
## Function API
95+
96+
In addition to macros, you can use the underlying functions directly:
97+
98+
| Function | Binary-Safe | Output Buffering | Description |
99+
|----------|-------------|------------------|-------------|
100+
| `zend::printf()` | No | Yes | Printf-style output (used by `php_print!`) |
101+
| `zend::output_write()` | Yes | Yes | Binary-safe buffered output |
102+
| `zend::write()` | Yes | No | Binary-safe unbuffered output |
103+
104+
### Example using functions directly
105+
106+
```rust,ignore
107+
use ext_php_rs::zend::output_write;
108+
109+
fn output_data(data: &[u8]) {
110+
let bytes_written = output_write(data);
111+
if bytes_written != data.len() {
112+
eprintln!("Warning: incomplete write");
113+
}
114+
}
115+
```
116+
117+
## Comparison
118+
119+
| Macro | Binary-Safe | Output Buffering | Supports Formatting |
120+
|-------|-------------|------------------|---------------------|
121+
| `php_print!` | No | Yes | Yes |
122+
| `php_println!` | No | Yes | Yes |
123+
| `php_output!` | Yes | Yes | No |
124+
| `php_write!` | Yes | No | No |
125+
126+
## When to Use Each
127+
128+
- **`php_print!` / `php_println!`**: Use for text output with format strings,
129+
similar to Rust's `print!` and `println!`. Best for human-readable messages.
130+
131+
- **`php_output!`**: Use for binary data that needs to work with PHP's output
132+
buffering. This is the recommended choice for most binary output needs.
133+
134+
- **`php_write!`**: Use when you need direct, unbuffered output that bypasses
135+
PHP's output layer. Useful for low-level SAPI interaction or when output
136+
buffering must be avoided.

src/embed/mod.rs

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,4 +297,98 @@ mod tests {
297297
assert!(result.unwrap_err().is_bailout());
298298
});
299299
}
300+
301+
#[test]
302+
fn test_php_write() {
303+
use crate::zend::write;
304+
305+
Embed::run(|| {
306+
// Test write function with regular data
307+
let bytes_written = write(b"Hello").expect("write failed");
308+
assert_eq!(bytes_written, 5);
309+
310+
// Test write function with binary data containing NUL bytes
311+
let bytes_written = write(b"Hello\x00World").expect("write failed");
312+
assert_eq!(bytes_written, 11);
313+
314+
// Test php_write! macro with byte literal
315+
let bytes_written = php_write!(b"Test").expect("php_write failed");
316+
assert_eq!(bytes_written, 4);
317+
318+
// Test php_write! macro with binary data containing NUL bytes
319+
let bytes_written = php_write!(b"Binary\x00Data\x00Here").expect("php_write failed");
320+
assert_eq!(bytes_written, 16);
321+
322+
// Test php_write! macro with byte slice variable
323+
let data: &[u8] = &[0x48, 0x65, 0x6c, 0x6c, 0x6f]; // "Hello"
324+
let bytes_written = php_write!(data).expect("php_write failed");
325+
assert_eq!(bytes_written, 5);
326+
327+
// Test empty data
328+
let bytes_written = write(b"").expect("write failed");
329+
assert_eq!(bytes_written, 0);
330+
});
331+
}
332+
333+
#[test]
334+
fn test_php_write_bypasses_output_buffering() {
335+
use crate::zend::write;
336+
337+
Embed::run(|| {
338+
// Start PHP output buffering
339+
Embed::eval("ob_start();").expect("ob_start failed");
340+
341+
// Write data using ub_write - this bypasses output buffering
342+
// ("ub" = unbuffered) and goes directly to SAPI output
343+
write(b"Direct output").expect("write failed");
344+
345+
// Get the buffered output - should be empty since ub_write bypasses buffering
346+
let result = Embed::eval("ob_get_clean();").expect("ob_get_clean failed");
347+
let output = result.string().expect("expected string result");
348+
349+
// Verify that ub_write bypasses output buffering
350+
assert_eq!(output, "", "ub_write should bypass output buffering");
351+
});
352+
}
353+
354+
#[test]
355+
fn test_php_print_respects_output_buffering() {
356+
use crate::zend::printf;
357+
358+
Embed::run(|| {
359+
// Start PHP output buffering
360+
Embed::eval("ob_start();").expect("ob_start failed");
361+
362+
// Write data using php_printf - this goes through output buffering
363+
printf("Hello from Rust").expect("printf failed");
364+
365+
// Get the buffered output
366+
let result = Embed::eval("ob_get_clean();").expect("ob_get_clean failed");
367+
let output = result.string().expect("expected string result");
368+
369+
// Verify that printf output is captured by output buffering
370+
assert_eq!(output, "Hello from Rust");
371+
});
372+
}
373+
374+
#[test]
375+
fn test_php_output_write_binary_safe_with_buffering() {
376+
use crate::zend::output_write;
377+
378+
Embed::run(|| {
379+
// Start PHP output buffering
380+
Embed::eval("ob_start();").expect("ob_start failed");
381+
382+
// Write binary data with NUL bytes - should be captured by buffer
383+
let bytes_written = output_write(b"Hello\x00World");
384+
assert_eq!(bytes_written, 11);
385+
386+
// Get the buffered output
387+
let result = Embed::eval("ob_get_clean();").expect("ob_get_clean failed");
388+
let output = result.string().expect("expected string result");
389+
390+
// Verify binary data was captured correctly (including NUL byte)
391+
assert_eq!(output, "Hello\x00World");
392+
});
393+
}
300394
}

src/error.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ pub enum Error {
7070
StreamWrapperRegistrationFailure,
7171
/// A failure occurred while unregistering the stream wrapper
7272
StreamWrapperUnregistrationFailure,
73+
/// The SAPI write function is not available
74+
SapiWriteUnavailable,
7375
}
7476

7577
impl Display for Error {
@@ -113,6 +115,9 @@ impl Display for Error {
113115
"A failure occurred while unregistering the stream wrapper"
114116
)
115117
}
118+
Error::SapiWriteUnavailable => {
119+
write!(f, "The SAPI write function is not available")
120+
}
116121
}
117122
}
118123
}

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ pub mod prelude {
5454
pub use crate::php_enum;
5555
pub use crate::php_print;
5656
pub use crate::php_println;
57+
pub use crate::php_write;
5758
pub use crate::types::ZendCallable;
5859
pub use crate::{
5960
ZvalConvert, php_class, php_const, php_extern, php_function, php_impl, php_interface,

src/macros.rs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,3 +425,82 @@ macro_rules! php_println {
425425
$crate::php_print!(concat!($fmt, "\n"), $($arg)*);
426426
};
427427
}
428+
429+
/// Writes binary data to the PHP standard output.
430+
///
431+
/// Unlike [`php_print!`], this macro is binary-safe and can handle data
432+
/// containing `NUL` bytes. It uses the SAPI module's `ub_write` function.
433+
///
434+
/// # Arguments
435+
///
436+
/// * `$data` - A byte slice (`&[u8]`) or byte literal (`b"..."`) to write.
437+
///
438+
/// # Returns
439+
///
440+
/// A `Result<usize>` containing the number of bytes written.
441+
///
442+
/// # Errors
443+
///
444+
/// Returns [`Error::SapiWriteUnavailable`] if the SAPI's `ub_write` function
445+
/// is not available.
446+
///
447+
/// [`Error::SapiWriteUnavailable`]: crate::error::Error::SapiWriteUnavailable
448+
///
449+
/// # Examples
450+
///
451+
/// ```ignore
452+
/// use ext_php_rs::php_write;
453+
///
454+
/// // Write a byte literal
455+
/// php_write!(b"Hello World").expect("write failed");
456+
///
457+
/// // Write binary data with NUL bytes (would panic with php_print!)
458+
/// php_write!(b"Hello\x00World").expect("write failed");
459+
///
460+
/// // Write a byte slice
461+
/// let data: &[u8] = &[0x48, 0x65, 0x6c, 0x6c, 0x6f];
462+
/// php_write!(data).expect("write failed");
463+
/// ```
464+
#[macro_export]
465+
macro_rules! php_write {
466+
($data: expr) => {{ $crate::zend::write($data) }};
467+
}
468+
469+
/// Writes binary data to PHP's output stream with output buffering support.
470+
///
471+
/// This macro is both binary-safe (can handle `NUL` bytes) AND respects PHP's
472+
/// output buffering (`ob_start()`). Use this when you need both capabilities.
473+
///
474+
/// # Arguments
475+
///
476+
/// * `$data` - A byte slice (`&[u8]`) or byte literal (`b"..."`) to write.
477+
///
478+
/// # Returns
479+
///
480+
/// The number of bytes written.
481+
///
482+
/// # Comparison
483+
///
484+
/// | Macro | Binary-safe | Output Buffering |
485+
/// |-------|-------------|------------------|
486+
/// | [`php_print!`] | No | Yes |
487+
/// | [`php_write!`] | Yes | No (unbuffered) |
488+
/// | [`php_output!`] | Yes | Yes |
489+
///
490+
/// # Examples
491+
///
492+
/// ```ignore
493+
/// use ext_php_rs::php_output;
494+
///
495+
/// // Write binary data that will be captured by ob_start()
496+
/// php_output!(b"Hello\x00World");
497+
///
498+
/// // Use with output buffering
499+
/// // ob_start();
500+
/// // php_output!(b"captured");
501+
/// // $data = ob_get_clean(); // Contains "captured"
502+
/// ```
503+
#[macro_export]
504+
macro_rules! php_output {
505+
($data: expr) => {{ $crate::zend::output_write($data) }};
506+
}

0 commit comments

Comments
 (0)