Skip to content

Commit e352796

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

File tree

6 files changed

+217
-0
lines changed

6 files changed

+217
-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: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
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_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,ignore
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+
## Function API
76+
77+
In addition to macros, you can use the underlying functions directly:
78+
79+
| Function | Target | Binary-Safe | Description |
80+
|----------|--------|-------------|-------------|
81+
| `zend::printf()` | stdout | No | Printf-style output (used by `php_print!`) |
82+
| `zend::write()` | stdout | Yes | Binary-safe stdout output |
83+
84+
### Example using functions directly
85+
86+
```rust,ignore
87+
use ext_php_rs::zend::write;
88+
89+
fn output_data(data: &[u8]) {
90+
let bytes_written = write(data);
91+
if bytes_written != data.len() {
92+
eprintln!("Warning: incomplete write");
93+
}
94+
}
95+
```
96+
97+
## Comparison
98+
99+
| Macro/Function | Binary-Safe | Supports Formatting | Target |
100+
|----------------|-------------|---------------------|--------|
101+
| `php_print!` | No | Yes | stdout |
102+
| `php_println!` | No | Yes | stdout |
103+
| `php_write!` | Yes | No | stdout |
104+
105+
## When to Use Each
106+
107+
- **`php_print!` / `php_println!`**: Use for text output with format strings,
108+
similar to Rust's `print!` and `println!`. Best for human-readable messages.
109+
110+
- **`php_write!`**: Use when outputting binary data, file contents, or any data
111+
that might contain NUL bytes. Also useful when you need to know exactly how
112+
many bytes were written.

src/embed/mod.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,4 +297,36 @@ 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+
}
300332
}

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: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,3 +425,36 @@ 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+
}

src/zend/mod.rs

Lines changed: 38 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,43 @@ 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+
65103
/// Get the name of the SAPI module.
66104
///
67105
/// # Panics

0 commit comments

Comments
 (0)