Skip to main content

quack_rs/
callback.rs

1// SPDX-License-Identifier: MIT
2// Copyright 2026 Tom F. <https://github.com/tomtom215/>
3// My way of giving something small back to the open source community
4// and encouraging more Rust development!
5
6//! Safe callback wrapper macros for `DuckDB` extension callbacks.
7//!
8//! Every `DuckDB` callback is `unsafe extern "C" fn`. If a Rust panic unwinds
9//! across the FFI boundary, it is **undefined behaviour**. These macros wrap
10//! user-provided closures with `std::panic::catch_unwind`, converting panics
11//! into error reporting via `duckdb_scalar_function_set_error` or by setting
12//! the output chunk size to 0.
13//!
14//! # Macros
15//!
16//! - `scalar_callback!` — wraps a scalar function callback
17//! - `table_scan_callback!` — wraps a table function scan callback
18//!
19//! # Estimated impact
20//!
21//! Eliminates ~60 `unsafe extern "C" fn` declarations across typical extensions.
22//!
23//! # Example: Scalar callback
24//!
25//! ```rust,no_run
26//! use quack_rs::data_chunk::DataChunk;
27//! use quack_rs::vector::VectorWriter;
28//!
29//! quack_rs::scalar_callback!(my_func, |info, input, output| {
30//!     let chunk = unsafe { DataChunk::from_raw(input) };
31//!     let mut writer = unsafe { VectorWriter::from_vector(output) };
32//!     for row in 0..chunk.size() {
33//!         let val = unsafe { chunk.reader(0).read_i64(row) };
34//!         unsafe { writer.write_i64(row, val * 2) };
35//!     }
36//! });
37//! ```
38//!
39//! # Example: Table scan callback
40//!
41//! ```rust,no_run
42//! use quack_rs::data_chunk::DataChunk;
43//!
44//! quack_rs::table_scan_callback!(my_scan, |info, output| {
45//!     let chunk = unsafe { DataChunk::from_raw(output) };
46//!     let mut writer = unsafe { chunk.writer(0) };
47//!     unsafe { writer.write_i64(0, 42) };
48//!     unsafe { chunk.set_size(1) };
49//! });
50//! ```
51
52/// Generates a panic-safe `unsafe extern "C"` scalar function callback.
53///
54/// The macro emits a function with signature:
55/// ```text
56/// unsafe extern "C" fn $name(
57///     info: duckdb_function_info,
58///     input: duckdb_data_chunk,
59///     output: duckdb_vector,
60/// )
61/// ```
62///
63/// The body is wrapped in `std::panic::catch_unwind`. If the closure panics,
64/// the error is reported via `duckdb_scalar_function_set_error` and the
65/// function returns without unwinding across the FFI boundary.
66///
67/// # Parameters
68///
69/// - `$name` — the name of the generated function
70/// - `$body` — a closure with signature `|info: duckdb_function_info, input: duckdb_data_chunk, output: duckdb_vector|`
71///
72/// # Example
73///
74/// ```rust,no_run
75/// quack_rs::scalar_callback!(double_it, |info, input, output| {
76///     let chunk = unsafe { quack_rs::data_chunk::DataChunk::from_raw(input) };
77///     let mut writer = unsafe { quack_rs::vector::VectorWriter::from_vector(output) };
78///     for row in 0..chunk.size() {
79///         let val = unsafe { chunk.reader(0).read_i64(row) };
80///         unsafe { writer.write_i64(row, val * 2) };
81///     }
82/// });
83/// ```
84#[macro_export]
85macro_rules! scalar_callback {
86    ($name:ident, |$info:ident, $input:ident, $output:ident| $body:block) => {
87        /// Scalar function callback (generated by `scalar_callback!`).
88        ///
89        /// # Safety
90        ///
91        /// Called by DuckDB. All parameters are provided by the DuckDB runtime.
92        /// Panics are caught and reported via `duckdb_scalar_function_set_error`.
93        #[allow(unused_unsafe)]
94        pub unsafe extern "C" fn $name(
95            $info: ::libduckdb_sys::duckdb_function_info,
96            $input: ::libduckdb_sys::duckdb_data_chunk,
97            $output: ::libduckdb_sys::duckdb_vector,
98        ) {
99            let result = ::std::panic::catch_unwind(::std::panic::AssertUnwindSafe(|| $body));
100            if let Err(panic) = result {
101                // Extract a message from the panic payload.
102                let msg = if let Some(s) = panic.downcast_ref::<&str>() {
103                    s.to_string()
104                } else if let Some(s) = panic.downcast_ref::<String>() {
105                    s.clone()
106                } else {
107                    "scalar callback panicked".to_string()
108                };
109                // Report the error to DuckDB.
110                if let Ok(c_msg) = ::std::ffi::CString::new(msg) {
111                    unsafe {
112                        ::libduckdb_sys::duckdb_scalar_function_set_error($info, c_msg.as_ptr());
113                    }
114                }
115            }
116        }
117    };
118}
119
120/// Generates a panic-safe `unsafe extern "C"` table function scan callback.
121///
122/// The macro emits a function with signature:
123/// ```text
124/// unsafe extern "C" fn $name(
125///     info: duckdb_function_info,
126///     output: duckdb_data_chunk,
127/// )
128/// ```
129///
130/// The body is wrapped in `std::panic::catch_unwind`. If the closure panics,
131/// the output chunk size is set to 0 (signaling end of stream) to prevent
132/// undefined behaviour from unwinding across the FFI boundary.
133///
134/// # Parameters
135///
136/// - `$name` — the name of the generated function
137/// - `$body` — a closure with signature `|info: duckdb_function_info, output: duckdb_data_chunk|`
138///
139/// # Example
140///
141/// ```rust,no_run
142/// quack_rs::table_scan_callback!(my_scan, |info, output| {
143///     let chunk = unsafe { quack_rs::data_chunk::DataChunk::from_raw(output) };
144///     let mut writer = unsafe { chunk.writer(0) };
145///     unsafe { writer.write_i64(0, 42) };
146///     unsafe { chunk.set_size(1) };
147/// });
148/// ```
149#[macro_export]
150macro_rules! table_scan_callback {
151    ($name:ident, |$info:ident, $output:ident| $body:block) => {
152        /// Table function scan callback (generated by `table_scan_callback!`).
153        ///
154        /// # Safety
155        ///
156        /// Called by DuckDB. All parameters are provided by the DuckDB runtime.
157        /// Panics are caught; on panic the output chunk size is set to 0.
158        #[allow(unused_unsafe)]
159        pub unsafe extern "C" fn $name(
160            $info: ::libduckdb_sys::duckdb_function_info,
161            $output: ::libduckdb_sys::duckdb_data_chunk,
162        ) {
163            let result = ::std::panic::catch_unwind(::std::panic::AssertUnwindSafe(|| $body));
164            if let Err(panic) = result {
165                // Extract a message from the panic payload.
166                let msg = if let Some(s) = panic.downcast_ref::<&str>() {
167                    s.to_string()
168                } else if let Some(s) = panic.downcast_ref::<String>() {
169                    s.clone()
170                } else {
171                    "table scan callback panicked".to_string()
172                };
173                // Report the error to DuckDB so users see a meaningful message.
174                if let Ok(c_msg) = ::std::ffi::CString::new(msg) {
175                    unsafe {
176                        ::libduckdb_sys::duckdb_function_set_error($info, c_msg.as_ptr());
177                    }
178                }
179                // Signal end-of-stream by setting size to 0.
180                // SAFETY: output is a valid data chunk provided by DuckDB.
181                unsafe {
182                    ::libduckdb_sys::duckdb_data_chunk_set_size($output, 0);
183                }
184            }
185        }
186    };
187}