daft_ext/abi/mod.rs
1//! Stable C ABI contract between Daft and extension cdylibs.
2//!
3//! This module defines the `repr(C)` types that Daft and extension shared
4//! libraries use to communicate. It has zero Daft internal dependencies
5//! and zero Arrow implementation dependencies (unless a feature flag is enabled).
6//!
7//! Naming follows Postgres conventions:
8//! - "module" = the shared library at the ABI boundary
9//! - "extension" = the higher-level Python package wrapping a module
10
11pub mod arrow;
12pub mod compat;
13pub mod ffi;
14
15use std::ffi::{c_char, c_int, c_void};
16
17pub use arrow::{ArrowArray, ArrowArrayStream, ArrowData, ArrowSchema};
18
19/// Modules built against a different ABI version are rejected at load time.
20pub const DAFT_ABI_VERSION: u32 = 1;
21
22/// Symbol that every Daft module cdylib must export.
23///
24/// ```ignore
25/// #[no_mangle]
26/// pub extern "C" fn daft_module_magic() -> FFI_Module { ... }
27/// ```
28pub const DAFT_MODULE_MAGIC_SYMBOL: &str = "daft_module_magic";
29
30/// Module definition returned by the entry point symbol.
31///
32/// Analogous to Postgres's `Pg_magic_struct` + `_PG_init` combined into
33/// a single struct.
34#[derive(Copy, Clone)]
35#[repr(C)]
36pub struct FFI_Module {
37 /// Must equal [`DAFT_ABI_VERSION`] or the loader rejects the module.
38 pub daft_abi_version: u32,
39
40 /// Module name as a null-terminated UTF-8 string.
41 ///
42 /// Must remain valid for the lifetime of the process (typically a
43 /// `&'static CStr` cast to `*const c_char`).
44 pub name: *const c_char,
45
46 /// Called by the host to let the module register its functions.
47 ///
48 /// Returns 0 on success, non-zero on error.
49 pub init: unsafe extern "C" fn(session: *mut FFI_SessionContext) -> c_int,
50
51 /// Free a string previously allocated by this module
52 /// (e.g. from `FFI_ScalarFunction::get_return_field` or error messages).
53 pub free_string: unsafe extern "C" fn(s: *mut c_char),
54}
55
56// SAFETY: Function pointers plus a static string pointer.
57unsafe impl Send for FFI_Module {}
58unsafe impl Sync for FFI_Module {}
59
60/// Virtual function table for a scalar function.
61///
62/// The host calls methods through these function pointers. `ctx` is an opaque
63/// pointer owned by the module; the host never dereferences it directly.
64#[repr(C)]
65pub struct FFI_ScalarFunction {
66 /// Opaque module-side context pointer.
67 pub ctx: *const c_void,
68
69 /// Return the function name as a null-terminated UTF-8 string.
70 ///
71 /// The returned pointer borrows from `ctx` and is valid until `fini`.
72 pub name: unsafe extern "C" fn(ctx: *const c_void) -> *const c_char,
73
74 /// Compute the output field given input fields.
75 ///
76 /// `args` points to `args_count` Arrow field schemas (C Data Interface).
77 /// On success, writes the result schema to `*ret`.
78 /// On error, writes a null-terminated message to `*errmsg`
79 /// (freed by `FFI_Module::free_string`).
80 ///
81 /// Returns 0 on success, non-zero on error.
82 pub get_return_field: unsafe extern "C" fn(
83 ctx: *const c_void,
84 args: *const ArrowSchema,
85 args_count: usize,
86 ret: *mut ArrowSchema,
87 errmsg: *mut *mut c_char,
88 ) -> c_int,
89
90 /// Evaluate the function on Arrow arrays via the C Data Interface.
91 ///
92 /// On error, writes a null-terminated message to `*errmsg`
93 /// (freed by `FFI_Module::free_string`).
94 ///
95 /// Returns 0 on success, non-zero on error.
96 pub call: unsafe extern "C" fn(
97 ctx: *const c_void,
98 args: *const ArrowArray,
99 args_schemas: *const ArrowSchema,
100 args_count: usize,
101 ret_array: *mut ArrowArray,
102 ret_schema: *mut ArrowSchema,
103 errmsg: *mut *mut c_char,
104 ) -> c_int,
105
106 /// Finalize the function, freeing all owned resources.
107 pub fini: unsafe extern "C" fn(ctx: *mut c_void),
108}
109
110// SAFETY: The vtable is function pointers plus an opaque ctx pointer.
111// The module is responsible for thread-safety of ctx.
112unsafe impl Send for FFI_ScalarFunction {}
113unsafe impl Sync for FFI_ScalarFunction {}
114
115/// Host-side session context passed to a module's `init` function.
116///
117/// The module calls `define_function` to register extensions.
118#[repr(C)]
119pub struct FFI_SessionContext {
120 /// Opaque host-side context pointer.
121 pub ctx: *mut c_void,
122
123 /// Register a scalar function with the host session.
124 ///
125 /// The host takes ownership of `function` on success.
126 /// Returns 0 on success, non-zero on error.
127 pub define_function:
128 unsafe extern "C" fn(ctx: *mut c_void, function: FFI_ScalarFunction) -> c_int,
129}
130
131// SAFETY: Function pointer plus opaque host pointer.
132unsafe impl Send for FFI_SessionContext {}
133unsafe impl Sync for FFI_SessionContext {}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 #[test]
140 fn struct_sizes() {
141 let ptr = std::mem::size_of::<usize>();
142
143 // FFI_ScalarFunction: ctx + name + get_return_field + call + fini = 5 pointers
144 assert_eq!(std::mem::size_of::<FFI_ScalarFunction>(), 5 * ptr);
145
146 // FFI_SessionContext: ctx + define_function = 2 pointers
147 assert_eq!(std::mem::size_of::<FFI_SessionContext>(), 2 * ptr);
148
149 // FFI_Module: u32 (padded) + name + init + free_string
150 // 64-bit: 4 + 4 pad + 8 + 8 + 8 = 32
151 // 32-bit: 4 + 4 + 4 + 4 = 16
152 assert_eq!(
153 std::mem::size_of::<FFI_Module>(),
154 if ptr == 8 { 32 } else { 16 }
155 );
156 }
157
158 #[test]
159 fn send_and_sync() {
160 fn assert_send_sync<T: Send + Sync>() {}
161 assert_send_sync::<FFI_ScalarFunction>();
162 assert_send_sync::<FFI_SessionContext>();
163 assert_send_sync::<FFI_Module>();
164 }
165
166 #[test]
167 fn constants() {
168 // !! THIS TEST EXISTS SO THAT THESE ARE NOT CHANGED BY ACCIDENT
169 // IT MEANS WE HAVE TO MANUALLY UPDATE IN TWO PLACES !!
170 assert_eq!(DAFT_ABI_VERSION, 1);
171 assert_eq!(DAFT_MODULE_MAGIC_SYMBOL, "daft_module_magic");
172 }
173}