quack-rs 0.12.0

Production-grade Rust SDK for building DuckDB loadable extensions
Documentation
// SPDX-License-Identifier: MIT
// Copyright 2026 Tom F. <https://github.com/tomtom215/>
// My way of giving something small back to the open source community
// and encouraging more Rust development!

//! Safe callback wrapper macros for `DuckDB` extension callbacks.
//!
//! Every `DuckDB` callback is `unsafe extern "C" fn`. If a Rust panic unwinds
//! across the FFI boundary, it is **undefined behaviour**. These macros wrap
//! user-provided closures with `std::panic::catch_unwind`, converting panics
//! into error reporting via `duckdb_scalar_function_set_error` or by setting
//! the output chunk size to 0.
//!
//! # Macros
//!
//! - `scalar_callback!` — wraps a scalar function callback
//! - `table_scan_callback!` — wraps a table function scan callback
//!
//! # Estimated impact
//!
//! Eliminates ~60 `unsafe extern "C" fn` declarations across typical extensions.
//!
//! # Example: Scalar callback
//!
//! ```rust,no_run
//! use quack_rs::data_chunk::DataChunk;
//! use quack_rs::vector::VectorWriter;
//!
//! quack_rs::scalar_callback!(my_func, |info, input, output| {
//!     let chunk = unsafe { DataChunk::from_raw(input) };
//!     let mut writer = unsafe { VectorWriter::from_vector(output) };
//!     for row in 0..chunk.size() {
//!         let val = unsafe { chunk.reader(0).read_i64(row) };
//!         unsafe { writer.write_i64(row, val * 2) };
//!     }
//! });
//! ```
//!
//! # Example: Table scan callback
//!
//! ```rust,no_run
//! use quack_rs::data_chunk::DataChunk;
//!
//! quack_rs::table_scan_callback!(my_scan, |info, output| {
//!     let chunk = unsafe { DataChunk::from_raw(output) };
//!     let mut writer = unsafe { chunk.writer(0) };
//!     unsafe { writer.write_i64(0, 42) };
//!     unsafe { chunk.set_size(1) };
//! });
//! ```

/// Generates a panic-safe `unsafe extern "C"` scalar function callback.
///
/// The macro emits a function with signature:
/// ```text
/// unsafe extern "C" fn $name(
///     info: duckdb_function_info,
///     input: duckdb_data_chunk,
///     output: duckdb_vector,
/// )
/// ```
///
/// The body is wrapped in `std::panic::catch_unwind`. If the closure panics,
/// the error is reported via `duckdb_scalar_function_set_error` and the
/// function returns without unwinding across the FFI boundary.
///
/// # Parameters
///
/// - `$name` — the name of the generated function
/// - `$body` — a closure with signature `|info: duckdb_function_info, input: duckdb_data_chunk, output: duckdb_vector|`
///
/// # Example
///
/// ```rust,no_run
/// quack_rs::scalar_callback!(double_it, |info, input, output| {
///     let chunk = unsafe { quack_rs::data_chunk::DataChunk::from_raw(input) };
///     let mut writer = unsafe { quack_rs::vector::VectorWriter::from_vector(output) };
///     for row in 0..chunk.size() {
///         let val = unsafe { chunk.reader(0).read_i64(row) };
///         unsafe { writer.write_i64(row, val * 2) };
///     }
/// });
/// ```
#[macro_export]
macro_rules! scalar_callback {
    ($name:ident, |$info:ident, $input:ident, $output:ident| $body:block) => {
        /// Scalar function callback (generated by `scalar_callback!`).
        ///
        /// # Safety
        ///
        /// Called by DuckDB. All parameters are provided by the DuckDB runtime.
        /// Panics are caught and reported via `duckdb_scalar_function_set_error`.
        #[allow(unused_unsafe)]
        pub unsafe extern "C" fn $name(
            $info: ::libduckdb_sys::duckdb_function_info,
            $input: ::libduckdb_sys::duckdb_data_chunk,
            $output: ::libduckdb_sys::duckdb_vector,
        ) {
            let result = ::std::panic::catch_unwind(::std::panic::AssertUnwindSafe(|| $body));
            if let Err(panic) = result {
                // Extract a message from the panic payload.
                let msg = if let Some(s) = panic.downcast_ref::<&str>() {
                    s.to_string()
                } else if let Some(s) = panic.downcast_ref::<String>() {
                    s.clone()
                } else {
                    "scalar callback panicked".to_string()
                };
                // Report the error to DuckDB.
                if let Ok(c_msg) = ::std::ffi::CString::new(msg) {
                    unsafe {
                        ::libduckdb_sys::duckdb_scalar_function_set_error($info, c_msg.as_ptr());
                    }
                }
            }
        }
    };
}

/// Generates a panic-safe `unsafe extern "C"` table function scan callback.
///
/// The macro emits a function with signature:
/// ```text
/// unsafe extern "C" fn $name(
///     info: duckdb_function_info,
///     output: duckdb_data_chunk,
/// )
/// ```
///
/// The body is wrapped in `std::panic::catch_unwind`. If the closure panics,
/// the output chunk size is set to 0 (signaling end of stream) to prevent
/// undefined behaviour from unwinding across the FFI boundary.
///
/// # Parameters
///
/// - `$name` — the name of the generated function
/// - `$body` — a closure with signature `|info: duckdb_function_info, output: duckdb_data_chunk|`
///
/// # Example
///
/// ```rust,no_run
/// quack_rs::table_scan_callback!(my_scan, |info, output| {
///     let chunk = unsafe { quack_rs::data_chunk::DataChunk::from_raw(output) };
///     let mut writer = unsafe { chunk.writer(0) };
///     unsafe { writer.write_i64(0, 42) };
///     unsafe { chunk.set_size(1) };
/// });
/// ```
#[macro_export]
macro_rules! table_scan_callback {
    ($name:ident, |$info:ident, $output:ident| $body:block) => {
        /// Table function scan callback (generated by `table_scan_callback!`).
        ///
        /// # Safety
        ///
        /// Called by DuckDB. All parameters are provided by the DuckDB runtime.
        /// Panics are caught; on panic the output chunk size is set to 0.
        #[allow(unused_unsafe)]
        pub unsafe extern "C" fn $name(
            $info: ::libduckdb_sys::duckdb_function_info,
            $output: ::libduckdb_sys::duckdb_data_chunk,
        ) {
            let result = ::std::panic::catch_unwind(::std::panic::AssertUnwindSafe(|| $body));
            if let Err(panic) = result {
                // Extract a message from the panic payload.
                let msg = if let Some(s) = panic.downcast_ref::<&str>() {
                    s.to_string()
                } else if let Some(s) = panic.downcast_ref::<String>() {
                    s.clone()
                } else {
                    "table scan callback panicked".to_string()
                };
                // Report the error to DuckDB so users see a meaningful message.
                if let Ok(c_msg) = ::std::ffi::CString::new(msg) {
                    unsafe {
                        ::libduckdb_sys::duckdb_function_set_error($info, c_msg.as_ptr());
                    }
                }
                // Signal end-of-stream by setting size to 0.
                // SAFETY: output is a valid data chunk provided by DuckDB.
                unsafe {
                    ::libduckdb_sys::duckdb_data_chunk_set_size($output, 0);
                }
            }
        }
    };
}