This document describes the error handling strategy implemented in the nih_plug_vstgui crate.
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:
- Unified Error Type: A single
VSTGUIErrorenum covers all possible errors - Panic Safety: FFI callbacks are protected from unwinding into C++
- Thread Safety: Debug builds enforce main-thread access to VSTGUI
- Null Pointer Checks: All FFI boundaries validate pointers
- Error Logging: Errors are logged for debugging
The main error type is VSTGUIError, which implements std::error::Error. It includes variants for:
NullPointer: Null pointer encountered at FFI boundaryInvalidHandle: Invalid handle provided to an operationCreationFailed: Failed to create a VSTGUI objectOperationFailed(String): A VSTGUI operation failed with detailsPlatformError(String): Platform-specific error occurredWindowOpenFailed: Failed to open a windowWindowCloseFailed: Failed to close a windowInvalidView: Invalid view providedInvalidControl: Invalid control providedResourceLoadFailed(String): Failed to load a resourceInvalidParameter(String): Invalid parameter providedThreadSafetyViolation: Operation attempted from wrong thread
Individual modules (frame, view, control) have their own error types that can be converted to VSTGUIError:
FrameError: Errors specific to frame operationsViewError: Errors specific to view operationsControlError: Errors specific to control operations
These provide more specific error information while maintaining compatibility with the unified error type.
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
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
// ...
});
}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(())
}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
}Thread safety checks are only enabled in debug builds. In release builds, they compile to no-ops for zero runtime overhead.
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(())
}
}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
}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);
}
}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);
}All fallible operations should return Result:
pub fn create_something() -> Result<Something, VSTGUIError> {
// ...
}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()))?;Always check pointers before dereferencing:
if ptr.is_null() {
crate::error::log_error("Null pointer encountered");
return Err(VSTGUIError::NullPointer);
}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
});
}Log errors to aid debugging:
if operation_failed {
crate::error::log_error("Operation failed: reason");
return Err(VSTGUIError::OperationFailed("reason".to_string()));
}The error handling system is thoroughly tested:
- Unit Tests: Each error module has unit tests
- Integration Tests:
error_handling_tests.rstests the error system - Thread Safety Tests:
thread_safety_tests.rstests thread checking - Property Tests: Future property-based tests will verify error handling properties
Potential enhancements to the error handling system:
- Integration with nih_plug logging: Use nih_plug's logging system in release builds
- Error recovery: Add recovery strategies for certain error types
- Error metrics: Track error frequencies for debugging
- Platform-specific errors: Better platform error reporting
- Error context: Add more context to errors using error chains