Skip to content

Latest commit

 

History

History
278 lines (206 loc) · 7.69 KB

File metadata and controls

278 lines (206 loc) · 7.69 KB

Error Handling in nih_plug_vstgui

This document describes the error handling strategy implemented in the nih_plug_vstgui crate.

Overview

The nih_plug_vstgui crate provides comprehensive error handling to ensure safe interaction between Rust and VSTGUI's C++ API. The error handling system addresses several key concerns:

  1. Unified Error Type: A single VSTGUIError enum covers all possible errors
  2. Panic Safety: FFI callbacks are protected from unwinding into C++
  3. Thread Safety: Debug builds enforce main-thread access to VSTGUI
  4. Null Pointer Checks: All FFI boundaries validate pointers
  5. Error Logging: Errors are logged for debugging

Error Types

VSTGUIError

The main error type is VSTGUIError, which implements std::error::Error. It includes variants for:

  • NullPointer: Null pointer encountered at FFI boundary
  • InvalidHandle: Invalid handle provided to an operation
  • CreationFailed: Failed to create a VSTGUI object
  • OperationFailed(String): A VSTGUI operation failed with details
  • PlatformError(String): Platform-specific error occurred
  • WindowOpenFailed: Failed to open a window
  • WindowCloseFailed: Failed to close a window
  • InvalidView: Invalid view provided
  • InvalidControl: Invalid control provided
  • ResourceLoadFailed(String): Failed to load a resource
  • InvalidParameter(String): Invalid parameter provided
  • ThreadSafetyViolation: Operation attempted from wrong thread

Module-Specific Errors

Individual modules (frame, view, control) have their own error types that can be converted to VSTGUIError:

  • FrameError: Errors specific to frame operations
  • ViewError: Errors specific to view operations
  • ControlError: Errors specific to control operations

These provide more specific error information while maintaining compatibility with the unified error type.

Panic Safety

catch_unwind_ffi

All FFI callbacks use the catch_unwind_ffi helper to prevent Rust panics from unwinding into C++ code:

pub fn catch_unwind_ffi<F, R>(f: F) -> Option<R>
where
    F: FnOnce() -> R + std::panic::UnwindSafe,
{
    match std::panic::catch_unwind(f) {
        Ok(result) => Some(result),
        Err(panic_info) => {
            log_error(&format!("Panic in FFI callback: {:?}", panic_info));
            None
        }
    }
}

This ensures that:

  • Panics are caught at the FFI boundary
  • Panic information is logged for debugging
  • C++ code receives a safe error indication (None)
  • Undefined behavior from unwinding into C++ is prevented

Usage in Callbacks

All C callbacks use catch_unwind_ffi:

unsafe extern "C" fn control_value_changed_callback(
    user_data: *mut c_void,
    control: *mut ffi::Control,
) {
    crate::error::catch_unwind_ffi(|| {
        // Rust code that might panic
        // ...
    });
}

Thread Safety

Thread Checking

VSTGUI is not thread-safe and must be accessed from the main/GUI thread. The thread_check module enforces this in debug builds:

pub fn assert_main_thread() -> Result<()> {
    #[cfg(debug_assertions)]
    {
        let main_thread_id = MAIN_THREAD_ID.get_or_init(|| std::thread::current().id());
        let current_thread_id = std::thread::current().id();

        if *main_thread_id != current_thread_id {
            return Err(VSTGUIError::ThreadSafetyViolation);
        }
    }
    Ok(())
}

Usage

All VSTGUI operations check thread safety in debug builds:

pub fn new(size: Rect, system_window: *mut c_void) -> Result<Self, FrameError> {
    #[cfg(debug_assertions)]
    crate::thread_check::assert_main_thread()
        .map_err(|_| FrameError::OperationFailed("thread safety violation".to_string()))?;
    
    // ... rest of the function
}

Performance

Thread safety checks are only enabled in debug builds. In release builds, they compile to no-ops for zero runtime overhead.

Null Pointer Checks

Helper Functions

The error module provides helpers for null pointer checking:

pub fn check_null_ptr<T>(ptr: *const T, context: &str) -> Result<()> {
    if ptr.is_null() {
        log_error(&format!("Null pointer encountered: {}", context));
        Err(VSTGUIError::NullPointer)
    } else {
        Ok(())
    }
}

pub fn check_null_ptr_mut<T>(ptr: *mut T, context: &str) -> Result<()> {
    if ptr.is_null() {
        log_error(&format!("Null pointer encountered: {}", context));
        Err(VSTGUIError::NullPointer)
    } else {
        Ok(())
    }
}

Usage

All FFI boundaries check for null pointers:

pub fn add_view(&mut self, view: *mut ffi::View) -> Result<(), FrameError> {
    if view.is_null() {
        crate::error::log_error("Attempted to add null view to frame");
        return Err(FrameError::NullPointer);
    }
    
    // ... rest of the function
}

Error Logging

log_error Function

The log_error function provides consistent error logging:

pub fn log_error(message: &str) {
    #[cfg(debug_assertions)]
    {
        eprintln!("[VSTGUI Error] {}", message);
    }

    #[cfg(not(debug_assertions))]
    {
        // In release builds, use nih_plug's logging if available
        eprintln!("[VSTGUI Error] {}", message);
    }
}

Usage

Errors are logged at key points:

if ptr.is_null() {
    crate::error::log_error("Failed to create frame: vstgui_frame_new returned null");
    return Err(FrameError::CreationFailed);
}

Best Practices

1. Always Use Result Types

All fallible operations should return Result:

pub fn create_something() -> Result<Something, VSTGUIError> {
    // ...
}

2. Check Thread Safety

Add thread safety checks to all VSTGUI operations:

#[cfg(debug_assertions)]
crate::thread_check::assert_main_thread()
    .map_err(|_| MyError::OperationFailed("thread safety violation".to_string()))?;

3. Validate Pointers

Always check pointers before dereferencing:

if ptr.is_null() {
    crate::error::log_error("Null pointer encountered");
    return Err(VSTGUIError::NullPointer);
}

4. Use catch_unwind_ffi in Callbacks

All FFI callbacks must use catch_unwind_ffi:

unsafe extern "C" fn my_callback(user_data: *mut c_void) {
    crate::error::catch_unwind_ffi(|| {
        // Rust code here
    });
}

5. Log Errors

Log errors to aid debugging:

if operation_failed {
    crate::error::log_error("Operation failed: reason");
    return Err(VSTGUIError::OperationFailed("reason".to_string()));
}

Testing

The error handling system is thoroughly tested:

  • Unit Tests: Each error module has unit tests
  • Integration Tests: error_handling_tests.rs tests the error system
  • Thread Safety Tests: thread_safety_tests.rs tests thread checking
  • Property Tests: Future property-based tests will verify error handling properties

Future Improvements

Potential enhancements to the error handling system:

  1. Integration with nih_plug logging: Use nih_plug's logging system in release builds
  2. Error recovery: Add recovery strategies for certain error types
  3. Error metrics: Track error frequencies for debugging
  4. Platform-specific errors: Better platform error reporting
  5. Error context: Add more context to errors using error chains

References