Skip to content

Commit 1f5ed19

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

File tree

6 files changed

+347
-0
lines changed

6 files changed

+347
-0
lines changed

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

4041
# Advanced Topics

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,no_run
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,no_run
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_write!` instead.
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_write!`
49+
50+
Writes binary data to PHP's standard output. This macro is binary-safe and can
51+
handle data containing NUL bytes. It uses the SAPI module's `ub_write` function.
52+
53+
```rust,no_run
54+
use ext_php_rs::prelude::*;
55+
56+
#[php_function]
57+
pub fn output_binary() -> i64 {
58+
// Write a byte literal
59+
php_write!(b"Hello World");
60+
61+
// Write binary data with NUL bytes (would panic with php_print!)
62+
let bytes_written = php_write!(b"Hello\x00World");
63+
64+
// Write a byte slice
65+
let data: &[u8] = &[0x48, 0x65, 0x6c, 0x6c, 0x6f]; // "Hello"
66+
php_write!(data);
67+
68+
bytes_written as i64
69+
}
70+
```
71+
72+
The macro returns the number of bytes written, which can be useful for verifying
73+
that all data was output successfully.
74+
75+
### `php_write_err!`
76+
77+
Writes binary data to standard error (stderr). This macro is also binary-safe
78+
and writes directly to file descriptor 2, bypassing PHP's error handling.
79+
80+
```rust,no_run
81+
use ext_php_rs::prelude::*;
82+
83+
#[php_function]
84+
pub fn log_error() {
85+
// Write error message to stderr
86+
php_write_err!(b"Error: something went wrong\n");
87+
88+
// Binary data with NUL bytes also works
89+
php_write_err!(b"Error code: \x00\x01\x02\x03\n");
90+
}
91+
```
92+
93+
## Function API
94+
95+
In addition to macros, you can use the underlying functions directly:
96+
97+
| Function | Target | Binary-Safe | Description |
98+
|----------|--------|-------------|-------------|
99+
| `zend::printf()` | stdout | No | Printf-style output (used by `php_print!`) |
100+
| `zend::write()` | stdout | Yes | Binary-safe stdout output |
101+
| `zend::write_err()` | stderr | Yes | Binary-safe stderr output |
102+
103+
### Example using functions directly
104+
105+
```rust,no_run
106+
use ext_php_rs::zend::{write, write_err};
107+
108+
fn output_data(data: &[u8]) {
109+
let bytes_written = write(data);
110+
if bytes_written != data.len() {
111+
write_err(b"Warning: incomplete write\n");
112+
}
113+
}
114+
```
115+
116+
## Comparison
117+
118+
| Macro/Function | Binary-Safe | Supports Formatting | Target |
119+
|----------------|-------------|---------------------|--------|
120+
| `php_print!` | No | Yes | stdout |
121+
| `php_println!` | No | Yes | stdout |
122+
| `php_write!` | Yes | No | stdout |
123+
| `php_write_err!` | Yes | No | stderr |
124+
125+
## When to Use Each
126+
127+
- **`php_print!` / `php_println!`**: Use for text output with format strings,
128+
similar to Rust's `print!` and `println!`. Best for human-readable messages.
129+
130+
- **`php_write!`**: Use when outputting binary data, file contents, or any data
131+
that might contain NUL bytes. Also useful when you need to know exactly how
132+
many bytes were written.
133+
134+
- **`php_write_err!`**: Use for error messages or diagnostic output that should
135+
go to stderr rather than stdout. Essential for CLI tools where stdout might be
136+
piped to another program.

src/embed/mod.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,4 +297,71 @@ 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");
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");
312+
assert_eq!(bytes_written, 11);
313+
314+
// Test php_write! macro with byte literal
315+
let bytes_written = php_write!(b"Test");
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");
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);
325+
assert_eq!(bytes_written, 5);
326+
327+
// Test empty data
328+
let bytes_written = write(b"");
329+
assert_eq!(bytes_written, 0);
330+
});
331+
}
332+
333+
#[test]
334+
fn test_php_write_err() {
335+
use crate::zend::write_err;
336+
337+
// Note: write_err doesn't require PHP embed as it writes directly to fd 2
338+
// But we test it within Embed::run for consistency
339+
340+
Embed::run(|| {
341+
// Test write_err function with regular data
342+
let bytes_written = write_err(b"Error");
343+
assert_eq!(bytes_written, 5);
344+
345+
// Test write_err function with binary data containing NUL bytes
346+
let bytes_written = write_err(b"Error\x00Data");
347+
assert_eq!(bytes_written, 10);
348+
349+
// Test php_write_err! macro with byte literal
350+
let bytes_written = php_write_err!(b"Test");
351+
assert_eq!(bytes_written, 4);
352+
353+
// Test php_write_err! macro with binary data containing NUL bytes
354+
let bytes_written = php_write_err!(b"Binary\x00Error\x00Here");
355+
assert_eq!(bytes_written, 17);
356+
357+
// Test php_write_err! macro with byte slice variable
358+
let data: &[u8] = &[0x45, 0x72, 0x72, 0x6f, 0x72]; // "Error"
359+
let bytes_written = php_write_err!(data);
360+
assert_eq!(bytes_written, 5);
361+
362+
// Test empty data
363+
let bytes_written = write_err(b"");
364+
assert_eq!(bytes_written, 0);
365+
});
366+
}
300367
}

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ 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;
58+
pub use crate::php_write_err;
5759
pub use crate::types::ZendCallable;
5860
pub use crate::{
5961
ZvalConvert, php_class, php_const, php_extern, php_function, php_impl, php_interface,

src/macros.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,3 +425,70 @@ 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+
/// The number of bytes written.
441+
///
442+
/// # Examples
443+
///
444+
/// ```ignore
445+
/// use ext_php_rs::php_write;
446+
///
447+
/// // Write a byte literal
448+
/// php_write!(b"Hello World");
449+
///
450+
/// // Write binary data with NUL bytes (would panic with php_print!)
451+
/// php_write!(b"Hello\x00World");
452+
///
453+
/// // Write a byte slice
454+
/// let data: &[u8] = &[0x48, 0x65, 0x6c, 0x6c, 0x6f];
455+
/// php_write!(data);
456+
/// ```
457+
#[macro_export]
458+
macro_rules! php_write {
459+
($data: expr) => {{ $crate::zend::write($data) }};
460+
}
461+
462+
/// Writes binary data to standard error (stderr).
463+
///
464+
/// This macro is binary-safe and can handle data containing `NUL` bytes.
465+
/// It writes directly to file descriptor 2 (stderr), bypassing PHP's
466+
/// error handling which uses null-terminated strings.
467+
///
468+
/// # Arguments
469+
///
470+
/// * `$data` - A byte slice (`&[u8]`) or byte literal (`b"..."`) to write.
471+
///
472+
/// # Returns
473+
///
474+
/// The number of bytes written.
475+
///
476+
/// # Examples
477+
///
478+
/// ```ignore
479+
/// use ext_php_rs::php_write_err;
480+
///
481+
/// // Write a byte literal to stderr
482+
/// php_write_err!(b"Error occurred");
483+
///
484+
/// // Write binary data with NUL bytes to stderr
485+
/// php_write_err!(b"Error:\x00binary\x00data");
486+
///
487+
/// // Write a byte slice to stderr
488+
/// let data: &[u8] = &[0x45, 0x72, 0x72, 0x6f, 0x72]; // "Error"
489+
/// php_write_err!(data);
490+
/// ```
491+
#[macro_export]
492+
macro_rules! php_write_err {
493+
($data: expr) => {{ $crate::zend::write_err($data) }};
494+
}

src/zend/mod.rs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use crate::{
1818
ffi::{php_printf, sapi_module},
1919
};
2020
use std::ffi::CString;
21+
use std::os::raw::c_char;
2122

2223
pub use _type::ZendType;
2324
pub use class::ClassEntry;
@@ -62,6 +63,79 @@ pub fn printf(message: &str) -> Result<()> {
6263
Ok(())
6364
}
6465

66+
/// Writes binary data to PHP's output stream (stdout).
67+
///
68+
/// Unlike [`printf`], this function is binary-safe and can handle data
69+
/// containing NUL bytes. It uses the SAPI module's `ub_write` function
70+
/// which accepts a pointer and length, allowing arbitrary binary data.
71+
///
72+
/// Also see the [`php_write!`] macro.
73+
///
74+
/// # Arguments
75+
///
76+
/// * `data` - The binary data to write to stdout.
77+
///
78+
/// # Returns
79+
///
80+
/// The number of bytes written, or 0 if the SAPI's `ub_write` function
81+
/// is not available.
82+
///
83+
/// # Example
84+
///
85+
/// ```ignore
86+
/// use ext_php_rs::zend::write;
87+
///
88+
/// // Write binary data including NUL bytes
89+
/// let data = b"Hello\x00World";
90+
/// write(data);
91+
/// ```
92+
#[must_use]
93+
pub fn write(data: &[u8]) -> usize {
94+
unsafe {
95+
if let Some(ub_write) = sapi_module.ub_write {
96+
ub_write(data.as_ptr().cast::<c_char>(), data.len())
97+
} else {
98+
0
99+
}
100+
}
101+
}
102+
103+
/// Writes binary data to stderr.
104+
///
105+
/// This function is binary-safe and can handle data containing NUL bytes.
106+
/// It writes directly to stderr, bypassing PHP's error handling which
107+
/// uses null-terminated strings.
108+
///
109+
/// Also see the [`php_write_err!`] macro.
110+
///
111+
/// # Arguments
112+
///
113+
/// * `data` - The binary data to write to stderr.
114+
///
115+
/// # Returns
116+
///
117+
/// The number of bytes written, or 0 if writing failed.
118+
///
119+
/// # Example
120+
///
121+
/// ```ignore
122+
/// use ext_php_rs::zend::write_err;
123+
///
124+
/// // Write binary data including NUL bytes to stderr
125+
/// let data = b"Error: \x00binary\x00data";
126+
/// write_err(data);
127+
/// ```
128+
#[must_use]
129+
pub fn write_err(data: &[u8]) -> usize {
130+
use std::io::Write;
131+
132+
if data.is_empty() {
133+
return 0;
134+
}
135+
136+
std::io::stderr().write(data).unwrap_or(0)
137+
}
138+
65139
/// Get the name of the SAPI module.
66140
///
67141
/// # Panics

0 commit comments

Comments
 (0)