Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

#[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:

#[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:

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

// 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:

#[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.