1use std::collections::BTreeMap;
4use std::ffi::c_void;
5use std::sync::atomic::{AtomicU64, Ordering};
6use std::sync::{LazyLock, Mutex};
7
8use crate::config::{parse_port_spec, RouteConfig, RouteMode};
9use crate::context::Context;
10use crate::status::{Error, Result};
11
12#[repr(C)]
16#[derive(Clone, Copy)]
17pub struct KernelVTable {
18 pub create: Option<unsafe extern "C" fn(*mut c_void) -> *mut c_void>,
19 pub get_contract: Option<unsafe extern "C" fn(*mut c_void, *mut c_void)>,
20 pub open: Option<unsafe extern "C" fn(*mut c_void, *mut c_void) -> i32>,
21 pub process: Option<unsafe extern "C" fn(*mut c_void, *mut c_void) -> i32>,
22 pub close: Option<unsafe extern "C" fn(*mut c_void, *mut c_void) -> i32>,
23 pub destroy: Option<unsafe extern "C" fn(*mut c_void)>,
24}
25
26#[derive(Clone, Copy)]
27pub struct KernelReg {
28 pub vtable: KernelVTable,
29 pub factory: *mut c_void,
30 pub language: KernelLanguage,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34#[repr(u32)]
35pub enum KernelLanguage {
36 Unknown = 0,
37 Rust = 1,
38 Cpp = 2,
39 Python = 3,
40 C = 4,
41}
42
43impl KernelLanguage {
44 pub fn label(self) -> &'static str {
45 match self {
46 Self::Unknown => "unknown",
47 Self::Rust => "Rust",
48 Self::Cpp => "C++",
49 Self::Python => "Python",
50 Self::C => "C",
51 }
52 }
53}
54
55unsafe impl Send for KernelReg {}
58unsafe impl Sync for KernelReg {}
59
60static REGISTRY: LazyLock<Mutex<BTreeMap<String, KernelReg>>> =
62 LazyLock::new(|| Mutex::new(BTreeMap::new()));
63
64pub fn register(name: &str, vtable: KernelVTable, factory: *mut c_void) -> Result<()> {
65 register_with_language(name, vtable, factory, KernelLanguage::Unknown)
66}
67
68pub fn register_with_language(
69 name: &str,
70 vtable: KernelVTable,
71 factory: *mut c_void,
72 language: KernelLanguage,
73) -> Result<()> {
74 if name.is_empty() {
75 return Err(Error::InvalidArg("kernel name must not be empty".into()));
76 }
77 if vtable.process.is_none() {
78 return Err(Error::InvalidArg(format!(
79 "kernel `{name}` vtable is missing process (the only required callback)"
80 )));
81 }
82 let mut reg = REGISTRY.lock().expect("registry lock poisoned");
83 if reg.contains_key(name) {
84 return Err(Error::InvalidArg(format!(
85 "kernel `{name}` already registered"
86 )));
87 }
88 reg.insert(
89 name.to_string(),
90 KernelReg {
91 vtable,
92 factory,
93 language,
94 },
95 );
96 Ok(())
97}
98
99pub fn lookup(name: &str) -> Option<KernelReg> {
100 REGISTRY
101 .lock()
102 .expect("registry lock poisoned")
103 .get(name)
104 .copied()
105}
106
107pub fn registered_names() -> Vec<String> {
108 REGISTRY
109 .lock()
110 .expect("registry lock poisoned")
111 .keys()
112 .cloned()
113 .collect()
114}
115
116fn not_registered(name: &str) -> Error {
119 let registered = registered_names();
120 let suggestion = crate::diagnostic::did_you_mean(name, registered.iter().map(String::as_str));
121 Error::NotFound(format!(
122 "kernel `{name}` not registered{suggestion}. registered: [{}]",
123 registered.join(", ")
124 ))
125}
126
127pub struct KernelInstance {
129 self_ptr: *mut c_void,
130 vtable: KernelVTable,
131 kernel_name: String,
132 language: KernelLanguage,
133 internal: Option<RouteRuntime>,
134}
135
136struct RouteRuleStats {
137 evaluated: AtomicU64,
138 matched: AtomicU64,
139 emitted: AtomicU64,
140}
141
142struct RouteRuntime {
143 config: RouteConfig,
144 rules: Vec<RouteRuleStats>,
145 default_emitted: AtomicU64,
146 unmatched: AtomicU64,
147 dropped: AtomicU64,
148 errors: AtomicU64,
149 missing_metadata: AtomicU64,
150 evaluation_errors: AtomicU64,
151}
152
153#[derive(Debug, Clone)]
154pub(crate) struct RouteStatsSnapshot {
155 pub config: RouteConfig,
156 pub rules: Vec<(u64, u64, u64)>,
157 pub default_emitted: u64,
158 pub unmatched: u64,
159 pub dropped: u64,
160 pub errors: u64,
161 pub missing_metadata: u64,
162 pub evaluation_errors: u64,
163}
164
165unsafe impl Send for KernelInstance {}
168unsafe impl Sync for KernelInstance {}
169
170impl KernelInstance {
171 pub fn create(kernel_name: &str) -> Result<Self> {
172 let reg = lookup(kernel_name).ok_or_else(|| not_registered(kernel_name))?;
173 let self_ptr = match reg.vtable.create {
174 Some(f) => unsafe { f(reg.factory) },
176 None => std::ptr::null_mut(), };
178 Ok(Self {
179 self_ptr,
180 vtable: reg.vtable,
181 kernel_name: kernel_name.to_string(),
182 language: reg.language,
183 internal: None,
184 })
185 }
186
187 pub fn create_route(config: RouteConfig) -> Self {
188 let rules = config
189 .routes
190 .iter()
191 .map(|_| RouteRuleStats {
192 evaluated: AtomicU64::new(0),
193 matched: AtomicU64::new(0),
194 emitted: AtomicU64::new(0),
195 })
196 .collect();
197 Self {
198 self_ptr: std::ptr::null_mut(),
199 vtable: KernelVTable {
200 create: None,
201 get_contract: None,
202 open: None,
203 process: None,
204 close: None,
205 destroy: None,
206 },
207 kernel_name: "__lmflow.route".into(),
208 language: KernelLanguage::Rust,
209 internal: Some(RouteRuntime {
210 config,
211 rules,
212 default_emitted: AtomicU64::new(0),
213 unmatched: AtomicU64::new(0),
214 dropped: AtomicU64::new(0),
215 errors: AtomicU64::new(0),
216 missing_metadata: AtomicU64::new(0),
217 evaluation_errors: AtomicU64::new(0),
218 }),
219 }
220 }
221
222 pub(crate) fn route_stats(&self) -> Option<RouteStatsSnapshot> {
223 let route = self.internal.as_ref()?;
224 Some(RouteStatsSnapshot {
225 config: route.config.clone(),
226 rules: route
227 .rules
228 .iter()
229 .map(|stats| {
230 (
231 stats.evaluated.load(Ordering::Relaxed),
232 stats.matched.load(Ordering::Relaxed),
233 stats.emitted.load(Ordering::Relaxed),
234 )
235 })
236 .collect(),
237 default_emitted: route.default_emitted.load(Ordering::Relaxed),
238 unmatched: route.unmatched.load(Ordering::Relaxed),
239 dropped: route.dropped.load(Ordering::Relaxed),
240 errors: route.errors.load(Ordering::Relaxed),
241 missing_metadata: route.missing_metadata.load(Ordering::Relaxed),
242 evaluation_errors: route.evaluation_errors.load(Ordering::Relaxed),
243 })
244 }
245
246 pub(crate) fn reset_stats(&self) {
247 let Some(route) = &self.internal else { return };
248 for stats in &route.rules {
249 stats.evaluated.store(0, Ordering::Relaxed);
250 stats.matched.store(0, Ordering::Relaxed);
251 stats.emitted.store(0, Ordering::Relaxed);
252 }
253 route.default_emitted.store(0, Ordering::Relaxed);
254 route.unmatched.store(0, Ordering::Relaxed);
255 route.dropped.store(0, Ordering::Relaxed);
256 route.errors.store(0, Ordering::Relaxed);
257 route.missing_metadata.store(0, Ordering::Relaxed);
258 route.evaluation_errors.store(0, Ordering::Relaxed);
259 }
260
261 pub fn kernel_name(&self) -> &str {
262 &self.kernel_name
263 }
264
265 pub fn language(&self) -> KernelLanguage {
266 self.language
267 }
268
269 pub unsafe fn fill_contract(kernel_name: &str, contract: *mut c_void) -> Result<()> {
274 let reg = lookup(kernel_name).ok_or_else(|| not_registered(kernel_name))?;
275 if let Some(f) = reg.vtable.get_contract {
276 unsafe { f(reg.factory, contract) };
278 }
279 Ok(())
280 }
281
282 pub unsafe fn open(&self, ctx: *mut c_void) -> i32 {
290 let status = match self.vtable.open {
291 Some(f) => unsafe { f(self.self_ptr, ctx) },
292 None => 0,
293 };
294 unsafe { &mut *(ctx as *mut Context) }.callback_status(status)
295 }
296 pub unsafe fn process(&self, ctx: *mut c_void) -> i32 {
299 if let Some(route) = &self.internal {
300 let context = unsafe { &mut *(ctx as *mut Context) };
301 return match route_process(route, context) {
302 Ok(()) => 0,
303 Err(error) => {
304 context.set_error(&error.to_string());
305 -1
306 }
307 };
308 }
309 let status = match self.vtable.process {
310 Some(f) => unsafe { f(self.self_ptr, ctx) },
311 None => 0,
312 };
313 unsafe { &mut *(ctx as *mut Context) }.callback_status(status)
314 }
315 pub unsafe fn close(&self, ctx: *mut c_void) -> i32 {
318 let status = match self.vtable.close {
319 Some(f) => unsafe { f(self.self_ptr, ctx) },
320 None => 0,
321 };
322 unsafe { &mut *(ctx as *mut Context) }.callback_status(status)
323 }
324}
325
326fn route_process(route: &RouteRuntime, context: &mut Context) -> Result<()> {
327 let packet = context.input(0).cloned().unwrap_or_default();
328 let mut matched = false;
329 let mut default_output = None;
330 for (index, rule) in route.config.routes.iter().enumerate() {
331 if rule.default {
332 default_output = Some(rule.to.as_str());
333 continue;
334 }
335 route.rules[index].evaluated.fetch_add(1, Ordering::Relaxed);
336 let predicate = rule
337 .when
338 .as_ref()
339 .ok_or_else(|| Error::InvalidArg("route rule missing condition".into()))?;
340 route.missing_metadata.fetch_add(
341 predicate.missing_metadata_count(packet.metadata()),
342 Ordering::Relaxed,
343 );
344 let hit = match predicate.evaluate(packet.metadata(), packet.timestamp()) {
345 Ok(hit) => hit,
346 Err(error) => {
347 route.evaluation_errors.fetch_add(1, Ordering::Relaxed);
348 return Err(error);
349 }
350 };
351 if hit {
352 route.rules[index].matched.fetch_add(1, Ordering::Relaxed);
353 let out = context
354 .out_ports
355 .index_by_name(&rule.to)
356 .ok_or_else(|| Error::InvalidArg(format!("unknown route output `{}`", rule.to)))?;
357 context.emit(out, packet.clone())?;
358 route.rules[index].emitted.fetch_add(1, Ordering::Relaxed);
359 context.shared.counter_add(
360 &format!("route.{}.matched.{}", context.node_name, rule.to),
361 1,
362 );
363 context.shared.counter_add(
364 &format!("route.{}.emitted.{}", context.node_name, rule.to),
365 1,
366 );
367 matched = true;
368 if route.config.mode == RouteMode::First {
369 break;
370 }
371 }
372 }
373 if !matched {
374 if let Some(port) = default_output {
375 let out = context
376 .out_ports
377 .index_by_name(port)
378 .ok_or_else(|| Error::InvalidArg(format!("unknown default output `{port}`")))?;
379 context.emit(out, packet)?;
380 route.default_emitted.fetch_add(1, Ordering::Relaxed);
381 return Ok(());
382 }
383 route.unmatched.fetch_add(1, Ordering::Relaxed);
384 match route.config.unmatched.as_str() {
385 "drop" => {
386 route.dropped.fetch_add(1, Ordering::Relaxed);
387 context
388 .shared
389 .counter_add(&format!("route.{}.dropped", context.node_name), 1);
390 Ok(())
391 }
392 "error" => {
393 route.errors.fetch_add(1, Ordering::Relaxed);
394 Err(Error::Kernel("route packet did not match any rule".into()))
395 }
396 port => {
397 let out = context.out_ports.index_by_name(port).ok_or_else(|| {
398 Error::InvalidArg(format!("unknown unmatched output `{port}`"))
399 })?;
400 context
401 .shared
402 .counter_add(&format!("route.{}.emitted.{}", context.node_name, port), 1);
403 context.emit(out, packet)?;
404 Ok(())
405 }
406 }
407 } else {
408 Ok(())
409 }
410}
411
412impl std::fmt::Debug for KernelInstance {
413 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
414 write!(f, "KernelInstance{{`{}`}}", self.kernel_name)
415 }
416}
417
418impl Drop for KernelInstance {
419 fn drop(&mut self) {
420 if let Some(f) = self.vtable.destroy {
421 if !self.self_ptr.is_null() {
422 unsafe { f(self.self_ptr) };
423 }
424 }
425 self.self_ptr = std::ptr::null_mut();
426 }
427}
428
429#[derive(Debug, Default, Clone)]
433pub struct PortTable {
434 names: Vec<String>,
435 tags: Vec<(String, usize)>,
436 by_name: BTreeMap<String, usize>,
437 by_tag: BTreeMap<(String, usize), usize>,
438}
439
440impl PortTable {
441 pub fn build(decls: &[String], what: &str) -> Result<Self> {
442 let mut t = PortTable::default();
443 let mut auto: BTreeMap<String, usize> = BTreeMap::new();
445 for decl in decls {
446 let spec = parse_port_spec(decl)?;
447 let idx = match spec.index {
448 Some(i) => i,
449 None => {
450 let c = auto.entry(spec.tag.clone()).or_insert(0);
451 let i = *c;
452 *c += 1;
453 i
454 }
455 };
456 let flat = t.names.len();
457 let key = (spec.tag.clone(), idx);
458 if t.by_tag.contains_key(&key) {
459 return Err(Error::InvalidArg(format!(
460 "{what}: port `{decl}` (tag=`{}`, index={idx}) is a duplicate",
461 spec.tag
462 )));
463 }
464 if t.by_name.contains_key(&spec.name) {
465 return Err(Error::InvalidArg(format!(
466 "{what}: port name `{}` is duplicated within the same kernel",
467 spec.name
468 )));
469 }
470 t.by_tag.insert(key, flat);
471 t.by_name.insert(spec.name.clone(), flat);
472 t.names.push(spec.name);
473 t.tags.push((spec.tag, idx));
474 }
475 t.check_tag_index_continuity(what)?;
476 Ok(t)
477 }
478
479 fn check_tag_index_continuity(&self, what: &str) -> Result<()> {
481 let mut per_tag: BTreeMap<&str, Vec<usize>> = BTreeMap::new();
482 for (tag, idx) in &self.tags {
483 per_tag.entry(tag.as_str()).or_default().push(*idx);
484 }
485 for (tag, mut idxs) in per_tag {
486 idxs.sort_unstable();
487 for (expect, got) in idxs.iter().enumerate() {
488 if expect != *got {
489 return Err(Error::InvalidArg(format!(
490 "{what}: tag `{tag}` index must be contiguous from 0, got {idxs:?}"
491 )));
492 }
493 }
494 }
495 Ok(())
496 }
497
498 pub fn len(&self) -> usize {
499 self.names.len()
500 }
501 pub fn is_empty(&self) -> bool {
502 self.names.is_empty()
503 }
504 pub fn name(&self, flat: usize) -> Option<&str> {
505 self.names.get(flat).map(|s| s.as_str())
506 }
507 pub fn names(&self) -> &[String] {
508 &self.names
509 }
510 pub fn id_by_tag(&self, tag: &str, index: usize) -> Option<usize> {
511 self.by_tag.get(&(tag.to_string(), index)).copied()
512 }
513 pub fn index_by_name(&self, name: &str) -> Option<usize> {
514 self.by_name.get(name).copied()
515 }
516}
517
518#[derive(Debug)]
523pub struct Contract {
524 pub inputs: std::sync::Arc<PortTable>,
525 pub outputs: std::sync::Arc<PortTable>,
526 pub input_types: Vec<u64>,
528 pub output_types: Vec<u64>,
529 pub required_side_packets: Vec<String>,
530 error: Option<String>,
531}
532
533impl Contract {
534 pub fn new(inputs: std::sync::Arc<PortTable>, outputs: std::sync::Arc<PortTable>) -> Self {
535 let ni = inputs.len();
536 let no = outputs.len();
537 Self {
538 inputs,
539 outputs,
540 input_types: vec![0; ni],
541 output_types: vec![0; no],
542 required_side_packets: Vec::new(),
543 error: None,
544 }
545 }
546
547 pub(crate) fn record_error(&mut self, message: impl Into<String>) {
548 if self.error.is_none() {
549 self.error = Some(message.into());
550 }
551 }
552
553 pub(crate) fn take_error(&mut self) -> Option<String> {
554 self.error.take()
555 }
556}
557
558#[cfg(test)]
559mod tests {
560 use super::*;
561
562 fn decls(v: &[&str]) -> Vec<String> {
563 v.iter().map(|s| s.to_string()).collect()
564 }
565
566 #[test]
567 fn flat_index_follows_declaration_order() {
568 let t = PortTable::build(&decls(&["ZZZ:z", "plain", "AAA:a"]), "test").unwrap();
570 assert_eq!(t.name(0), Some("z"));
571 assert_eq!(t.name(1), Some("plain"));
572 assert_eq!(t.name(2), Some("a"));
573 }
574
575 #[test]
576 fn lookup_by_tag_and_by_name() {
577 let t = PortTable::build(
578 &decls(&["frames", "VIDEO:cam0", "MASK:0:m0", "MASK:1:m1"]),
579 "test",
580 )
581 .unwrap();
582 assert_eq!(t.id_by_tag("VIDEO", 0), Some(1));
583 assert_eq!(t.id_by_tag("MASK", 1), Some(3));
584 assert_eq!(t.id_by_tag("MASK", 5), None);
585 assert_eq!(t.index_by_name("frames"), Some(0));
586 assert_eq!(t.index_by_name("nope"), None);
587 assert_eq!(t.id_by_tag("", 0), Some(0));
589 }
590
591 #[test]
592 fn auto_index_per_tag() {
593 let t = PortTable::build(&decls(&["A:x", "A:y", "B:z"]), "test").unwrap();
594 assert_eq!(t.id_by_tag("A", 0), Some(0));
595 assert_eq!(t.id_by_tag("A", 1), Some(1));
596 assert_eq!(t.id_by_tag("B", 0), Some(2));
597 }
598
599 #[test]
600 fn rejects_duplicate_and_discontinuous() {
601 assert!(
602 PortTable::build(&decls(&["A:0:x", "A:0:y"]), "t").is_err(),
603 "same tag and index duplicated"
604 );
605 assert!(
606 PortTable::build(&decls(&["a", "a"]), "t").is_err(),
607 "duplicate port name"
608 );
609 let err = PortTable::build(&decls(&["A:1:x"]), "t").unwrap_err();
610 assert!(
611 err.to_string().contains("contiguous"),
612 "index has a gap: {err}"
613 );
614 }
615
616 #[test]
617 fn register_rejects_vtable_without_process() {
618 let vt = KernelVTable {
619 create: None,
620 get_contract: None,
621 open: None,
622 process: None,
623 close: None,
624 destroy: None,
625 };
626 let err = register("__no_process__", vt, std::ptr::null_mut()).unwrap_err();
627 assert!(err.to_string().contains("process"), "{err}");
628 }
629
630 #[test]
631 fn lookup_missing_kernel_lists_available() {
632 let err = KernelInstance::create("__definitely_missing__").unwrap_err();
633 assert!(err.to_string().contains("registered"), "{err}");
635 }
636}