ext-php-rs 0.15.11

Bindings for the Zend API to build PHP extensions natively in Rust.
Documentation
# Bailout Guard

When PHP triggers a "bailout" (via `exit()`, `die()`, or a fatal error), it uses
`longjmp` to unwind the stack. This bypasses Rust's normal drop semantics,
meaning destructors for stack-allocated values won't run. This can lead to
resource leaks for things like file handles, network connections, or locks.

## The Problem

Consider this code:

```rust,ignore
#[php_function]
pub fn process_file(callback: ZendCallable) {
    let file = File::open("data.txt").unwrap();

    // If callback calls exit(), the file handle leaks!
    callback.try_call(vec![]);

    // file.drop() never runs
}
```

If the PHP callback triggers `exit()`, the `File` handle is never closed because
`longjmp` skips Rust's destructor calls.

## Solution 1: Using `try_call`

The simplest solution is to use `try_call` for PHP callbacks. It catches bailouts
internally and returns normally, allowing Rust destructors to run:

```rust,ignore
#[php_function]
pub fn process_file(callback: ZendCallable) {
    let file = File::open("data.txt").unwrap();

    // try_call catches bailout, function returns, file is dropped
    let result = callback.try_call(vec![]);

    if result.is_err() {
        // Bailout occurred, but file will still be closed
        // when this function returns
    }
}
```

## Solution 2: Using `BailoutGuard`

For cases where you need guaranteed cleanup even if bailout occurs directly
(not through `try_call`), use `BailoutGuard`:

```rust,ignore
use ext_php_rs::prelude::*;
use std::fs::File;

#[php_function]
pub fn process_file(callback: ZendCallable) {
    // Wrap the file handle in BailoutGuard
    let file = BailoutGuard::new(File::open("data.txt").unwrap());

    // Even if bailout occurs, the file will be closed
    callback.try_call(vec![]);

    // Use the file via Deref
    // file.read_to_string(...);
}
```

### How `BailoutGuard` Works

1. **Heap allocation**: The wrapped value is heap-allocated so it survives
   the `longjmp` stack unwinding.

2. **Cleanup registration**: A cleanup callback is registered in thread-local
   storage when the guard is created.

3. **On normal drop**: The cleanup is cancelled and the value is dropped normally.

4. **On bailout**: Before re-triggering the bailout, all registered cleanup
   callbacks are executed, dropping the guarded values.

### API

```rust,ignore
// Create a guard
let guard = BailoutGuard::new(value);

// Access the value (implements Deref and DerefMut)
guard.do_something();
let inner: &T = &*guard;
let inner_mut: &mut T = &mut *guard;

// Explicitly get references
let inner: &T = guard.get();
let inner_mut: &mut T = guard.get_mut();

// Extract the value, cancelling cleanup
let value: T = guard.into_inner();
```

### Performance Note

`BailoutGuard` incurs a heap allocation. Only use it for values that absolutely
must be cleaned up, such as:

- File handles
- Network connections
- Database connections
- Locks and mutexes
- Other system resources

For simple values without cleanup requirements, the overhead isn't worth it.

## Nested Calls

`BailoutGuard` works correctly with nested function calls. Guards at all
nesting levels are cleaned up when bailout occurs:

```rust,ignore
#[php_function]
pub fn outer_function(callback: ZendCallable) {
    let _outer_resource = BailoutGuard::new(Resource::new());

    inner_function(&callback);
}

fn inner_function(callback: &ZendCallable) {
    let _inner_resource = BailoutGuard::new(Resource::new());

    // If bailout occurs here, both inner and outer resources are cleaned up
    callback.try_call(vec![]);
}
```

## Best Practices

1. **Prefer `try_call`**: For most cases, using `try_call` and handling the
   error result is simpler and doesn't require heap allocation.

2. **Use `BailoutGuard` for critical resources**: Only wrap values that
   absolutely must be cleaned up (connections, locks, etc.).

3. **Don't overuse**: Not every value needs to be wrapped. Simple data
   structures without cleanup requirements don't need `BailoutGuard`.

4. **Combine approaches**: Use `try_call` where possible and `BailoutGuard`
   for critical resources that must be cleaned up regardless.