Skip to main content

reifydb_sub_flow/operator/
native.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use std::{
5	any::Any,
6	cell::{Cell, UnsafeCell},
7	collections::HashMap,
8	panic::{AssertUnwindSafe, catch_unwind},
9	path::{Path, PathBuf},
10	process::abort,
11	sync::OnceLock,
12};
13
14use libloading::Symbol;
15use reifydb_abi::operator::capabilities::OperatorCapability;
16use reifydb_codec::{
17	encoded::{
18		row::EncodedRow,
19		shape::{RowShape, fingerprint::RowShapeFingerprint},
20	},
21	key::encoded::{EncodedKey, EncodedKeyRange},
22};
23use reifydb_core::{
24	common::CommitVersion,
25	interface::{
26		catalog::{
27			flow::FlowNodeId,
28			id::{NamespaceId, TableId},
29			namespace::Namespace,
30			table::Table,
31		},
32		change::Change,
33	},
34};
35use reifydb_extension::loader::ffi::LibraryCache;
36use reifydb_runtime::sync::rwlock::RwLock;
37use reifydb_sdk::{
38	config::Config,
39	error::{Result as SdkResult, SdkError},
40	operator::{OperatorLogic, Tick, view::native::NativeChangeView},
41};
42use reifydb_transaction::multi::RangeScope;
43use reifydb_value::{
44	Result,
45	error::Error,
46	value::{
47		Value,
48		constraint::TypeConstraint,
49		dictionary::{DictionaryEntryId, DictionaryId},
50		duration::Duration,
51		row_number::RowNumber,
52	},
53};
54use tracing::error;
55
56use crate::{
57	error::NativeOperatorError,
58	operator::{
59		BoxedOperator, Operator,
60		context::native::{NativeBridge, NativeOperatorContext},
61		stateful::row::allocate_row_numbers,
62	},
63	transaction::{FlowTransaction, slot::PersistFn},
64};
65
66fn run_or_abort<R>(node: FlowNodeId, stage: &'static str, f: impl FnOnce() -> SdkResult<R>) -> R {
67	match catch_unwind(AssertUnwindSafe(f)) {
68		Ok(Ok(value)) => value,
69		Ok(Err(e)) => {
70			error!(
71				operator_id = node.0,
72				stage, "native operator returned an error; operators must not fail - aborting: {:?}", e
73			);
74			abort();
75		}
76		Err(_) => {
77			error!(operator_id = node.0, stage, "native operator panicked - aborting");
78			abort();
79		}
80	}
81}
82
83pub const NATIVE_OPERATOR_MAGIC: u32 = 0x5244_424E;
84
85pub const NATIVE_ABI_TAG: u32 = 0x0308;
86
87pub type NativeOperatorCreateFn = fn(FlowNodeId, &Config) -> Result<BoxedBridgedOperator>;
88
89pub struct NativeOperatorColumn {
90	pub name: String,
91	pub field_type: TypeConstraint,
92	pub description: String,
93}
94
95pub struct NativeOperatorDescriptor {
96	pub abi_tag: u32,
97	pub name: String,
98	pub version: String,
99	pub description: String,
100	pub capabilities: u32,
101	pub input_columns: Vec<NativeOperatorColumn>,
102	pub output_columns: Vec<NativeOperatorColumn>,
103}
104
105pub fn native_operator_magic() -> u32 {
106	NATIVE_OPERATOR_MAGIC
107}
108
109pub fn check_native_abi_tag(abi_tag: u32) -> Result<()> {
110	if abi_tag != NATIVE_ABI_TAG {
111		return Err(Error::from(NativeOperatorError::AbiTagMismatch {
112			plugin: abi_tag,
113			host: NATIVE_ABI_TAG,
114		}));
115	}
116	Ok(())
117}
118
119pub trait BridgedOperator: Send {
120	fn id(&self) -> FlowNodeId;
121
122	fn capabilities(&self) -> &'static [OperatorCapability];
123
124	fn apply(&self, bridge: &mut dyn NativeBridge, change: Change) -> Result<Change>;
125
126	fn tick(&self, _bridge: &mut dyn NativeBridge, _tick: Tick) -> Result<Option<Change>> {
127		Ok(None)
128	}
129
130	fn ticks(&self) -> Option<Duration> {
131		None
132	}
133
134	fn flush_state(&self, _bridge: &mut dyn NativeBridge) -> Result<()> {
135		Ok(())
136	}
137}
138
139pub type BoxedBridgedOperator = Box<dyn BridgedOperator>;
140
141pub struct FlowNativeBridge<'a> {
142	txn: &'a mut FlowTransaction,
143	node: FlowNodeId,
144	now_nanos: u64,
145}
146
147impl<'a> FlowNativeBridge<'a> {
148	pub fn new(txn: &'a mut FlowTransaction, node: FlowNodeId) -> Self {
149		let now_nanos = txn.clock().now_nanos();
150		Self {
151			txn,
152			node,
153			now_nanos,
154		}
155	}
156}
157
158impl NativeBridge for FlowNativeBridge<'_> {
159	fn clock_now_nanos(&self) -> u64 {
160		self.now_nanos
161	}
162	fn state_get(&mut self, key: &EncodedKey) -> Result<Option<EncodedRow>> {
163		self.txn.state_get(self.node, key)
164	}
165	fn state_get_many(&mut self, keys: &[EncodedKey]) -> Result<Vec<(EncodedKey, EncodedRow)>> {
166		Ok(self.txn.state_get_many(self.node, keys)?.items.into_iter().map(|r| (r.key, r.row)).collect())
167	}
168	fn state_set(&mut self, key: &EncodedKey, value: EncodedRow) -> Result<()> {
169		self.txn.state_set(self.node, key, value)
170	}
171	fn state_remove(&mut self, key: &EncodedKey) -> Result<()> {
172		self.txn.state_remove(self.node, key)
173	}
174	fn state_drop(&mut self, key: &EncodedKey) -> Result<()> {
175		self.txn.state_drop(self.node, key)
176	}
177	fn state_clear(&mut self) -> Result<()> {
178		self.txn.state_clear(self.node)
179	}
180	fn state_range(&mut self, range: EncodedKeyRange) -> Result<Vec<(EncodedKey, EncodedRow)>> {
181		Ok(self.txn.state_range_all(self.node, range)?.items.into_iter().map(|r| (r.key, r.row)).collect())
182	}
183	fn internal_state_get(&mut self, key: &EncodedKey) -> Result<Option<EncodedRow>> {
184		self.txn.internal_state_get(self.node, key)
185	}
186	fn internal_state_get_many(&mut self, keys: &[EncodedKey]) -> Result<Vec<(EncodedKey, EncodedRow)>> {
187		Ok(self.txn
188			.internal_state_get_many(self.node, keys)?
189			.items
190			.into_iter()
191			.map(|r| (r.key, r.row))
192			.collect())
193	}
194	fn internal_state_set(&mut self, key: &EncodedKey, value: EncodedRow) -> Result<()> {
195		self.txn.internal_state_set(self.node, key, value)
196	}
197	fn internal_state_remove(&mut self, key: &EncodedKey) -> Result<()> {
198		self.txn.internal_state_remove(self.node, key)
199	}
200	fn internal_state_drop(&mut self, key: &EncodedKey) -> Result<()> {
201		self.txn.internal_state_drop(self.node, key)
202	}
203	fn internal_state_range(&mut self, range: EncodedKeyRange) -> Result<Vec<(EncodedKey, EncodedRow)>> {
204		Ok(self.txn
205			.internal_state_range_all(self.node, range)?
206			.items
207			.into_iter()
208			.map(|r| (r.key, r.row))
209			.collect())
210	}
211	fn allocate_row_numbers(&mut self, count: u64) -> Result<RowNumber> {
212		allocate_row_numbers(self.txn, self.node, count).map(RowNumber)
213	}
214	fn store_get(&mut self, key: &EncodedKey) -> Result<Option<EncodedRow>> {
215		self.txn.get(key)
216	}
217	fn store_contains(&mut self, key: &EncodedKey) -> Result<bool> {
218		self.txn.contains_key(key)
219	}
220	fn store_prefix(&mut self, prefix: &EncodedKey) -> Result<Vec<(EncodedKey, EncodedRow)>> {
221		Ok(self.txn.prefix(prefix)?.items.into_iter().map(|r| (r.key, r.row)).collect())
222	}
223	fn store_range(&mut self, range: EncodedKeyRange) -> Result<Vec<(EncodedKey, EncodedRow)>> {
224		let rows = self.txn.range(range, RangeScope::All, 1024).collect::<Result<Vec<_>>>()?;
225		Ok(rows.into_iter().map(|r| (r.key, r.row)).collect())
226	}
227	fn catalog_find_namespace(
228		&mut self,
229		namespace: NamespaceId,
230		version: CommitVersion,
231	) -> Result<Option<Namespace>> {
232		Ok(self.txn.host_catalog().find_namespace(namespace, version))
233	}
234	fn catalog_find_namespace_by_name(
235		&mut self,
236		namespace: &str,
237		version: CommitVersion,
238	) -> Result<Option<Namespace>> {
239		Ok(self.txn.host_catalog().find_namespace_by_name(namespace, version))
240	}
241	fn catalog_find_table(&mut self, table: TableId, version: CommitVersion) -> Result<Option<Table>> {
242		Ok(self.txn.host_catalog().find_table(table, version))
243	}
244	fn catalog_find_table_by_name(
245		&mut self,
246		namespace: NamespaceId,
247		name: &str,
248		version: CommitVersion,
249	) -> Result<Option<Table>> {
250		Ok(self.txn.host_catalog().find_table_by_name(namespace, name, version))
251	}
252	fn catalog_find_row_shape(&mut self, fingerprint: RowShapeFingerprint) -> Result<Option<RowShape>> {
253		Ok(self.txn.host_catalog().find_row_shape(fingerprint))
254	}
255	fn dictionary_id_by_name(&mut self, name: &str) -> Result<Option<DictionaryId>> {
256		Ok(self.txn.find_dictionary_by_name(name).map(|d| d.id))
257	}
258	fn dictionary_find(&mut self, dictionary: DictionaryId, value: &Value) -> Result<Option<DictionaryEntryId>> {
259		match self.txn.find_dictionary(dictionary) {
260			Some(dict) => self.txn.find_in_dictionary(&dict, value),
261			None => Ok(None),
262		}
263	}
264	fn dictionary_get(&mut self, dictionary: DictionaryId, id: DictionaryEntryId) -> Result<Option<Value>> {
265		match self.txn.find_dictionary(dictionary) {
266			Some(dict) => self.txn.get_from_dictionary(&dict, id),
267			None => Ok(None),
268		}
269	}
270	fn state_get_many_visit(
271		&mut self,
272		keys: &[EncodedKey],
273		visit: &mut dyn FnMut(&EncodedKey, &EncodedRow) -> SdkResult<()>,
274	) -> SdkResult<()> {
275		let batch = self.txn.state_get_many(self.node, keys).map_err(|e| SdkError::Other(e.to_string()))?;
276		for r in &batch.items {
277			visit(&r.key, &r.row)?;
278		}
279		Ok(())
280	}
281	fn internal_state_get_many_visit(
282		&mut self,
283		keys: &[EncodedKey],
284		visit: &mut dyn FnMut(&EncodedKey, &EncodedRow) -> SdkResult<()>,
285	) -> SdkResult<()> {
286		let batch =
287			self.txn.internal_state_get_many(self.node, keys)
288				.map_err(|e| SdkError::Other(e.to_string()))?;
289		for r in &batch.items {
290			visit(&r.key, &r.row)?;
291		}
292		Ok(())
293	}
294	fn state_range_visit(
295		&mut self,
296		range: EncodedKeyRange,
297		visit: &mut dyn FnMut(&EncodedKey, &EncodedRow) -> SdkResult<()>,
298	) -> SdkResult<()> {
299		let batch = self.txn.state_range_all(self.node, range).map_err(|e| SdkError::Other(e.to_string()))?;
300		for r in &batch.items {
301			visit(&r.key, &r.row)?;
302		}
303		Ok(())
304	}
305	fn store_range_visit(
306		&mut self,
307		range: EncodedKeyRange,
308		visit: &mut dyn FnMut(&EncodedKey, &EncodedRow) -> SdkResult<()>,
309	) -> SdkResult<()> {
310		let rows =
311			self.txn.range(range, RangeScope::All, 1024)
312				.collect::<Result<Vec<_>>>()
313				.map_err(|e| SdkError::Other(e.to_string()))?;
314		for r in &rows {
315			visit(&r.key, &r.row)?;
316		}
317		Ok(())
318	}
319	fn store_prefix_visit(
320		&mut self,
321		prefix: &EncodedKey,
322		visit: &mut dyn FnMut(&EncodedKey, &EncodedRow) -> SdkResult<()>,
323	) -> SdkResult<()> {
324		let batch = self.txn.prefix(prefix).map_err(|e| SdkError::Other(e.to_string()))?;
325		for r in &batch.items {
326			visit(&r.key, &r.row)?;
327		}
328		Ok(())
329	}
330}
331
332pub struct LoadedNativeOperatorInfo {
333	pub operator: String,
334	pub library_path: PathBuf,
335	pub version: String,
336	pub description: String,
337	pub input_columns: Vec<NativeOperatorColumn>,
338	pub output_columns: Vec<NativeOperatorColumn>,
339	pub capabilities: u32,
340}
341
342static GLOBAL_NATIVE_OPERATOR_LOADER: OnceLock<RwLock<NativeOperatorLoader>> = OnceLock::new();
343
344pub fn native_operator_loader() -> &'static RwLock<NativeOperatorLoader> {
345	GLOBAL_NATIVE_OPERATOR_LOADER.get_or_init(|| RwLock::new(NativeOperatorLoader::new()))
346}
347
348pub struct NativeOperatorLoader {
349	cache: LibraryCache,
350	operator_paths: HashMap<String, PathBuf>,
351}
352
353impl NativeOperatorLoader {
354	fn new() -> Self {
355		Self {
356			cache: LibraryCache::new(),
357			operator_paths: HashMap::new(),
358		}
359	}
360
361	fn load_library(&mut self, path: &Path) -> Result<bool> {
362		self.cache.check_magic(path, b"reifydb_native_operator_magic\0", NATIVE_OPERATOR_MAGIC).map_err(|_e| {
363			Error::from(NativeOperatorError::LibraryNotLoaded {
364				path: path.display().to_string(),
365			})
366		})
367	}
368
369	fn descriptor(&self, path: &Path) -> Result<NativeOperatorDescriptor> {
370		let library = self.cache.get(path).ok_or_else(|| {
371			Error::from(NativeOperatorError::LibraryNotLoaded {
372				path: path.display().to_string(),
373			})
374		})?;
375
376		let descriptor = unsafe {
377			let get_descriptor: Symbol<fn() -> NativeOperatorDescriptor> =
378				library.get(b"reifydb_native_operator_descriptor\0").map_err(|e| {
379					Error::from(NativeOperatorError::SymbolNotFound {
380						symbol: "reifydb_native_operator_descriptor",
381						cause: e.to_string(),
382					})
383				})?;
384			get_descriptor()
385		};
386
387		check_native_abi_tag(descriptor.abi_tag)?;
388
389		Ok(descriptor)
390	}
391
392	pub fn register_operator(&mut self, path: &Path) -> Result<Option<LoadedNativeOperatorInfo>> {
393		if !self.load_library(path)? {
394			return Ok(None);
395		}
396
397		let descriptor = self.descriptor(path)?;
398		self.operator_paths.insert(descriptor.name.clone(), path.to_path_buf());
399
400		Ok(Some(LoadedNativeOperatorInfo {
401			operator: descriptor.name,
402			library_path: path.to_path_buf(),
403			version: descriptor.version,
404			description: descriptor.description,
405			input_columns: descriptor.input_columns,
406			output_columns: descriptor.output_columns,
407			capabilities: descriptor.capabilities,
408		}))
409	}
410
411	pub fn has_operator(&self, operator: &str) -> bool {
412		self.operator_paths.contains_key(operator)
413	}
414
415	pub fn create_operator_by_name(
416		&mut self,
417		operator: &str,
418		operator_id: FlowNodeId,
419		config: &Config,
420	) -> Result<BoxedOperator> {
421		let path = self
422			.operator_paths
423			.get(operator)
424			.ok_or_else(|| {
425				Error::from(NativeOperatorError::OperatorNotFound {
426					operator: operator.to_string(),
427				})
428			})?
429			.clone();
430
431		if !self.load_library(&path)? {
432			return Err(Error::from(NativeOperatorError::LibraryNotLoaded {
433				path: operator.to_string(),
434			}));
435		}
436
437		self.descriptor(&path)?;
438
439		let library = self.cache.get(&path).unwrap();
440		let create: NativeOperatorCreateFn = unsafe {
441			let create_symbol: Symbol<NativeOperatorCreateFn> =
442				library.get(b"reifydb_native_operator_create\0").map_err(|e| {
443					Error::from(NativeOperatorError::SymbolNotFound {
444						symbol: "reifydb_native_operator_create",
445						cause: e.to_string(),
446					})
447				})?;
448			*create_symbol
449		};
450
451		let bridged = create(operator_id, config)?;
452		let capabilities = bridged.capabilities();
453		Ok(Box::new(NativeBridgedOperator::new(bridged, operator_id, capabilities)))
454	}
455}
456
457impl Default for NativeOperatorLoader {
458	fn default() -> Self {
459		Self::new()
460	}
461}
462
463pub struct NativeOperatorAdapter<C> {
464	logic: UnsafeCell<C>,
465	node: FlowNodeId,
466	capabilities: &'static [OperatorCapability],
467}
468
469impl<C> NativeOperatorAdapter<C> {
470	pub fn new(logic: C, node: FlowNodeId, capabilities: &'static [OperatorCapability]) -> Self {
471		Self {
472			logic: UnsafeCell::new(logic),
473			node,
474			capabilities,
475		}
476	}
477}
478
479unsafe impl<C: Send> Send for NativeOperatorAdapter<C> {}
480
481impl<C: OperatorLogic + 'static> BridgedOperator for NativeOperatorAdapter<C> {
482	fn id(&self) -> FlowNodeId {
483		self.node
484	}
485
486	fn capabilities(&self) -> &'static [OperatorCapability] {
487		self.capabilities
488	}
489
490	fn apply(&self, bridge: &mut dyn NativeBridge, change: Change) -> Result<Change> {
491		let version = change.version;
492		let changed_at = change.changed_at;
493		let mut ctx = NativeOperatorContext::new(bridge, self.node);
494		{
495			let view = NativeChangeView::new(&change);
496			let logic = unsafe { &mut *self.logic.get() };
497			run_or_abort(self.node, "apply", || logic.apply(&mut ctx, view));
498		}
499		let diffs = ctx.take_diffs();
500		Ok(Change::from_flow(self.node, version, diffs, changed_at))
501	}
502
503	fn ticks(&self) -> Option<Duration> {
504		let logic = unsafe { &*self.logic.get() };
505		logic.ticks()
506	}
507
508	fn tick(&self, bridge: &mut dyn NativeBridge, tick: Tick) -> Result<Option<Change>> {
509		let now = tick.now;
510		let mut ctx = NativeOperatorContext::new(bridge, self.node);
511		{
512			let logic = unsafe { &mut *self.logic.get() };
513			run_or_abort(self.node, "tick", || logic.tick(&mut ctx, tick));
514		}
515		let diffs = ctx.take_diffs();
516		if diffs.is_empty() {
517			return Ok(None);
518		}
519		Ok(Some(Change::from_flow(self.node, CommitVersion(now.to_nanos()), diffs, now)))
520	}
521
522	fn flush_state(&self, bridge: &mut dyn NativeBridge) -> Result<()> {
523		let mut ctx = NativeOperatorContext::new(bridge, self.node);
524		let logic = unsafe { &mut *self.logic.get() };
525		run_or_abort(self.node, "flush_state", || logic.flush_state(&mut ctx));
526		Ok(())
527	}
528}
529
530#[derive(Clone, Copy)]
531struct SendableBridged(*const dyn BridgedOperator);
532unsafe impl Send for SendableBridged {}
533
534pub struct NativeBridgedOperator {
535	inner: BoxedBridgedOperator,
536	node: FlowNodeId,
537	capabilities: &'static [OperatorCapability],
538	last_registered_txn: Cell<u64>,
539}
540
541impl NativeBridgedOperator {
542	pub fn new(inner: BoxedBridgedOperator, node: FlowNodeId, capabilities: &'static [OperatorCapability]) -> Self {
543		Self {
544			inner,
545			node,
546			capabilities,
547			last_registered_txn: Cell::new(u64::MAX),
548		}
549	}
550
551	fn ensure_flush_slot(&self, txn: &mut FlowTransaction) -> Result<()> {
552		let txn_version = txn.version().0;
553		if self.last_registered_txn.get() != txn_version {
554			let captured = SendableBridged(&*self.inner as *const dyn BridgedOperator);
555			let node = self.node;
556			let persist: PersistFn = Box::new(move |txn: &mut FlowTransaction, _value: Box<dyn Any>| {
557				let captured = captured;
558				let bridged = unsafe { &*captured.0 };
559				let mut bridge = FlowNativeBridge::new(txn, node);
560				bridged.flush_state(&mut bridge)
561			});
562			let _ = txn.operator_state::<(), _>(node, move |_txn| Ok(((), persist)))?;
563			txn.mark_state_dirty(node);
564			self.last_registered_txn.set(txn_version);
565		}
566		Ok(())
567	}
568}
569
570unsafe impl Send for NativeBridgedOperator {}
571
572impl Operator for NativeBridgedOperator {
573	fn id(&self) -> FlowNodeId {
574		self.node
575	}
576
577	fn capabilities(&self) -> &[OperatorCapability] {
578		self.capabilities
579	}
580
581	fn apply(&self, txn: &mut FlowTransaction, change: Change) -> Result<Change> {
582		self.ensure_flush_slot(txn)?;
583		let mut bridge = FlowNativeBridge::new(txn, self.node);
584		self.inner.apply(&mut bridge, change)
585	}
586
587	fn ticks(&self) -> Option<Duration> {
588		self.inner.ticks()
589	}
590
591	fn tick(&self, txn: &mut FlowTransaction, tick: Tick) -> Result<Option<Change>> {
592		self.ensure_flush_slot(txn)?;
593		let mut bridge = FlowNativeBridge::new(txn, self.node);
594		self.inner.tick(&mut bridge, tick)
595	}
596}
597
598#[cfg(test)]
599mod tests {
600	use reifydb_abi::constants::OPERATOR_ABI_TAG;
601	use reifydb_extension::operator::ffi_loader::check_operator_abi_tag;
602
603	use super::{NATIVE_ABI_TAG, check_native_abi_tag};
604
605	// A plugin whose abi_tag does not match the host's must be refused, so an
606	// operator built against a different reifydb/toolchain is never loaded.
607	#[test]
608	fn native_abi_tag_accepts_match_rejects_mismatch() {
609		assert!(check_native_abi_tag(NATIVE_ABI_TAG).is_ok());
610		assert!(check_native_abi_tag(NATIVE_ABI_TAG ^ 0x1).is_err());
611		assert!(check_native_abi_tag(0).is_err());
612	}
613
614	#[test]
615	fn ffi_abi_tag_accepts_match_rejects_mismatch() {
616		assert!(check_operator_abi_tag(OPERATOR_ABI_TAG).is_ok());
617		assert!(check_operator_abi_tag(OPERATOR_ABI_TAG ^ 0x1).is_err());
618		assert!(check_operator_abi_tag(0).is_err());
619	}
620
621	// The two tags must be distinct and must reject each other, so a native
622	// `.so` can never validate against the ffi check or vice versa.
623	#[test]
624	fn native_and_ffi_tags_do_not_accept_each_other() {
625		assert_ne!(NATIVE_ABI_TAG, OPERATOR_ABI_TAG);
626		assert!(check_native_abi_tag(OPERATOR_ABI_TAG).is_err());
627		assert!(check_operator_abi_tag(NATIVE_ABI_TAG).is_err());
628	}
629}