reifydb_sub_flow/operator/
ffi.rs1use std::{
5 any::Any,
6 cell::{Cell, UnsafeCell},
7 ffi::c_void,
8 panic::{AssertUnwindSafe, catch_unwind},
9 process::abort,
10 ptr,
11};
12
13use reifydb_abi::{
14 callbacks::builder::EmitDiffKind,
15 context::context::ContextFFI,
16 flow::change::ChangeFFI,
17 operator::{
18 capabilities::{OperatorCapability, from_bitmask},
19 descriptor::OperatorDescriptorFFI,
20 vtable::OperatorVTableFFI,
21 },
22};
23use reifydb_core::{
24 common::CommitVersion,
25 interface::{
26 catalog::flow::FlowNodeId,
27 change::{Change, Diff, Diffs},
28 },
29 value::column::columns::Columns,
30};
31use reifydb_engine::vm::executor::Executor;
32use reifydb_extension::ffi_callbacks::builder::{BuilderRegistry, with_registry};
33use reifydb_sdk::{error::SdkError, ffi::arena::Arena, operator::Tick};
34use reifydb_value::{
35 Result,
36 value::{datetime::DateTime, duration::Duration},
37};
38use tracing::{Span, error, field, instrument};
39
40use crate::{
41 ffi::{callbacks::create_host_callbacks, context::new_ffi_context},
42 operator::Operator,
43 transaction::{FlowTransaction, slot::PersistFn},
44};
45
46thread_local! {
47 static FFI_MARSHAL_ARENA: UnsafeCell<Arena> = UnsafeCell::new(Arena::new());
48}
49
50#[derive(Clone, Copy)]
51struct SendableInstance(*mut c_void);
52unsafe impl Send for SendableInstance {}
53unsafe impl Sync for SendableInstance {}
54
55pub struct FFIOperator {
56 capabilities: Box<[OperatorCapability]>,
57
58 vtable: OperatorVTableFFI,
59
60 instance: *mut c_void,
61
62 operator_id: FlowNodeId,
63
64 executor: Executor,
65
66 builder_registry: BuilderRegistry,
67
68 last_registered_txn: Cell<u64>,
69
70 cached_ctx: UnsafeCell<ContextFFI>,
71}
72
73impl FFIOperator {
74 pub fn new(
75 descriptor: OperatorDescriptorFFI,
76 instance: *mut c_void,
77 operator_id: FlowNodeId,
78 executor: Executor,
79 ) -> Self {
80 let vtable = descriptor.vtable;
81 let capabilities = from_bitmask(descriptor.capabilities).into_boxed_slice();
82
83 Self {
84 capabilities,
85 vtable,
86 instance,
87 operator_id,
88 executor,
89 builder_registry: BuilderRegistry::new(),
90 last_registered_txn: Cell::new(u64::MAX),
91 cached_ctx: UnsafeCell::new(ContextFFI {
92 txn_ptr: ptr::null_mut(),
93 executor_ptr: ptr::null(),
94 operator_id: operator_id.0,
95 clock_now_nanos: 0,
96 callbacks: create_host_callbacks(),
97 }),
98 }
99 }
100
101 fn ensure_txn_setup(&self, txn: &mut FlowTransaction) -> Result<()> {
102 let txn_version = txn.version().0;
103 if self.last_registered_txn.get() != txn_version {
104 ensure_flush_slot(txn, self.operator_id, self.vtable, self.instance, self.executor.clone())?;
105 self.last_registered_txn.set(txn_version);
106 let ctx = unsafe { &mut *self.cached_ctx.get() };
109 ctx.txn_ptr = txn as *mut _ as *mut c_void;
110 ctx.executor_ptr = &self.executor as *const _ as *const c_void;
111 ctx.clock_now_nanos = txn.clock().now_nanos();
112 }
113 Ok(())
114 }
115}
116
117unsafe impl Send for FFIOperator {}
119
120impl Drop for FFIOperator {
121 fn drop(&mut self) {
122 if !self.instance.is_null() {
123 unsafe { (self.vtable.destroy)(self.instance) };
124 }
125 }
126}
127
128#[inline]
129#[instrument(name = "flow::ffi::marshal", level = "trace", skip_all)]
130fn marshal_input(arena: &mut Arena, change: &Change) -> ChangeFFI {
131 arena.marshal_change(change)
132}
133
134#[inline]
135#[instrument(name = "flow::ffi::vtable_call", level = "trace", skip_all, fields(operator_id = operator_id.0))]
136fn call_vtable(
137 vtable: &OperatorVTableFFI,
138 instance: *mut c_void,
139 ffi_ctx_ptr: *mut ContextFFI,
140 ffi_input: &ChangeFFI,
141 operator_id: FlowNodeId,
142) -> i32 {
143 let result = catch_unwind(AssertUnwindSafe(|| unsafe { (vtable.apply)(instance, ffi_ctx_ptr, ffi_input) }));
144
145 match result {
146 Ok(code) => code,
147 Err(panic_info) => {
148 let msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
149 s.to_string()
150 } else if let Some(s) = panic_info.downcast_ref::<String>() {
151 s.clone()
152 } else {
153 "Unknown panic".to_string()
154 };
155 error!(operator_id = operator_id.0, "FFI operator panicked during apply: {}", msg);
156 abort();
157 }
158 }
159}
160
161fn ensure_flush_slot(
162 txn: &mut FlowTransaction,
163 operator_id: FlowNodeId,
164 vtable: OperatorVTableFFI,
165 instance: *mut c_void,
166 executor: Executor,
167) -> Result<()> {
168 let send_instance = SendableInstance(instance);
169 let _ = txn.operator_state(operator_id, move |_txn| {
170 let captured_instance = send_instance;
171 let captured_vtable = vtable;
172 let captured_executor = executor;
173 let captured_id = operator_id;
174 let persist: PersistFn = Box::new(move |txn, _value: Box<dyn Any>| {
175 let ffi_ctx = new_ffi_context(txn, &captured_executor, captured_id, create_host_callbacks());
176 let ffi_ctx_ptr = &ffi_ctx as *const _ as *mut ContextFFI;
177 let inst = captured_instance;
178 let result = catch_unwind(AssertUnwindSafe(|| unsafe {
179 (captured_vtable.flush_state)(inst.0, ffi_ctx_ptr)
180 }));
181 match result {
182 Ok(0) => Ok(()),
183 Ok(code) => Err(SdkError::Other(format!(
184 "FFI operator flush_state failed with code: {}",
185 code
186 ))
187 .into()),
188 Err(_) => {
189 error!(operator_id = captured_id.0, "FFI operator panicked during flush_state");
190 abort();
191 }
192 }
193 });
194
195 Ok(((), persist))
196 })?;
197 txn.mark_state_dirty(operator_id);
198 Ok(())
199}
200
201impl Operator for FFIOperator {
202 fn id(&self) -> FlowNodeId {
203 self.operator_id
204 }
205
206 fn capabilities(&self) -> &[OperatorCapability] {
207 &self.capabilities
208 }
209
210 fn ticks(&self) -> Option<Duration> {
211 if !self.capabilities.contains(&OperatorCapability::Tick) {
212 return None;
213 }
214 let nanos = unsafe { (self.vtable.tick_interval)(self.instance) };
215 Some(Duration::from_nanoseconds(nanos as i64).unwrap())
216 }
217
218 #[instrument(name = "flow::ffi::apply", level = "trace", skip_all, fields(
219 operator_id = self.operator_id.0,
220 input_diff_count = change.diffs.len(),
221 output_diff_count = field::Empty
222 ))]
223 fn apply(&self, txn: &mut FlowTransaction, change: Change) -> Result<Change> {
224 self.ensure_txn_setup(txn)?;
225
226 FFI_MARSHAL_ARENA.with(|cell| unsafe { (*cell.get()).clear() });
229 let ffi_input = FFI_MARSHAL_ARENA.with(|cell| marshal_input(unsafe { &mut *cell.get() }, &change));
230
231 let version = change.version;
232 let changed_at = change.changed_at;
233
234 let ffi_ctx_ptr = self.cached_ctx.get();
235
236 let result_code = with_registry(&self.builder_registry, || {
237 call_vtable(&self.vtable, self.instance, ffi_ctx_ptr, &ffi_input, self.operator_id)
238 });
239
240 if result_code != 0 {
241 let _ = self.builder_registry.drain();
242 return Err(
243 SdkError::Other(format!("FFI operator apply failed with code: {}", result_code)).into()
244 );
245 }
246
247 let output_change = drain_emitted_diffs(&self.builder_registry, self.operator_id, version, changed_at);
248
249 Span::current().record("output_diff_count", output_change.diffs.len());
250
251 Ok(output_change)
252 }
253
254 #[instrument(name = "flow::ffi::tick", level = "trace", skip_all, fields(
255 operator_id = self.operator_id.0,
256 output_diff_count = field::Empty
257 ))]
258 fn tick(&self, txn: &mut FlowTransaction, tick: Tick) -> Result<Option<Change>> {
259 self.ensure_txn_setup(txn)?;
260
261 let timestamp_nanos = tick.now.to_nanos();
262 let ffi_ctx_ptr = self.cached_ctx.get();
263
264 let result_code = self.invoke_under_panic_guard("tick", || unsafe {
265 (self.vtable.tick)(self.instance, ffi_ctx_ptr, timestamp_nanos)
266 });
267
268 if result_code < 0 {
269 let _ = self.builder_registry.drain();
270 return Err(
271 SdkError::Other(format!("FFI operator tick failed with code: {}", result_code)).into()
272 );
273 }
274
275 let version = CommitVersion(timestamp_nanos);
276 let output_change = drain_emitted_diffs(&self.builder_registry, self.operator_id, version, tick.now);
277 Span::current().record("output_diff_count", output_change.diffs.len());
278 if output_change.diffs.is_empty() {
279 return Ok(None);
280 }
281 Ok(Some(output_change))
282 }
283}
284
285impl FFIOperator {
286 #[inline]
287 fn invoke_under_panic_guard<F>(&self, op: &'static str, call: F) -> i32
288 where
289 F: FnOnce() -> i32,
290 {
291 with_registry(&self.builder_registry, || {
292 let result = catch_unwind(AssertUnwindSafe(call));
293 match result {
294 Ok(code) => code,
295 Err(panic_info) => {
296 let msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
297 s.to_string()
298 } else if let Some(s) = panic_info.downcast_ref::<String>() {
299 s.clone()
300 } else {
301 "Unknown panic".to_string()
302 };
303 error!(
304 operator_id = self.operator_id.0,
305 "FFI operator panicked during {}: {}", op, msg
306 );
307 abort();
308 }
309 }
310 })
311 }
312}
313
314fn drain_emitted_diffs(
315 registry: &BuilderRegistry,
316 operator_id: FlowNodeId,
317 version: CommitVersion,
318 changed_at: DateTime,
319) -> Change {
320 let emitted = registry.drain();
321 let diffs: Diffs = emitted
322 .into_iter()
323 .map(|d| match d.kind {
324 EmitDiffKind::Insert => Diff::insert(d.post.unwrap_or_else(Columns::empty)),
325 EmitDiffKind::Update => Diff::update(
326 d.pre.unwrap_or_else(Columns::empty),
327 d.post.unwrap_or_else(Columns::empty),
328 ),
329 EmitDiffKind::Remove => Diff::remove(d.pre.unwrap_or_else(Columns::empty)),
330 })
331 .collect();
332 Change::from_flow(operator_id, version, diffs, changed_at)
333}