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}