Skip to main content

reifydb_sub_flow/operator/
ffi.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use 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			// SAFETY: single-threaded actor; no aliasing with guest (vtable not
107
108			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
117// SAFETY: FFIOperator is only accessed from a single actor at a time.
118unsafe 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		// SAFETY: single-threaded per operator; no live pointers from a prior
227
228		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}