Skip to main content

lmflow/
kernel_api.rs

1//! Sugar for writing kernels in Rust: [`Kernel`] + [`register_kernel`], plus the safe
2//! [`KernelCtx`] / [`KernelContract`] wrappers.
3//!
4//! This layer sits **on top of** the C ABI kernel vtable — the same thing
5//! `include/lmflow/flow.hpp`'s `KernelAdapter<T>` does for C++. It is not a native fast path that
6//! bypasses the vtable; its purpose is to make Rust a first-class language for writing kernels.
7//!
8//! Implement [`Kernel`], call [`register_kernel`] **before**
9//! [`Graph::from_yaml`](crate::Graph::from_yaml), then refer to the kernel by name from YAML as
10//! `kernel: <name>`.
11//!
12//! ```
13//! use lmflow::packet::type_id;
14//! use lmflow::{register_kernel, Kernel, KernelContract, KernelCtx, Packet};
15//!
16//! #[derive(Default)]
17//! struct Scale {
18//!     factor: i64,
19//! }
20//!
21//! impl Kernel for Scale {
22//!     fn get_contract(c: &mut KernelContract) {
23//!         c.input_type(0, type_id::I64);
24//!         c.output_type(0, type_id::I64);
25//!     }
26//!
27//!     fn open(&mut self, cc: &mut KernelCtx) -> lmflow::Result<()> {
28//!         self.factor = cc.option_i64("factor", 2);
29//!         Ok(())
30//!     }
31//!
32//!     fn process(&mut self, cc: &mut KernelCtx) -> lmflow::Result<()> {
33//!         let v = cc
34//!             .input(0)
35//!             .and_then(|p| p.as_i64())
36//!             .ok_or_else(|| cc.fail("input 0 must be an i64"))?;
37//!         cc.emit(0, Packet::from_i64(v * self.factor))
38//!     }
39//! }
40//!
41//! # fn main() -> lmflow::Result<()> {
42//! register_kernel::<Scale>("Scale")?;
43//! # Ok(())
44//! # }
45//! ```
46
47use std::ffi::c_void;
48use std::panic::{catch_unwind, AssertUnwindSafe};
49
50use crate::context::Context;
51use crate::kernel::{register_with_language, Contract, KernelLanguage, KernelVTable};
52use crate::packet::Packet;
53use crate::status::{code, Error, Result};
54use crate::timestamp::Timestamp;
55
56/// The interface for writing a kernel in Rust.
57///
58/// `Default` is how the engine constructs an instance (`T::default()` at create time); real
59/// initialisation — reading node options, allocating long-lived state — belongs in
60/// [`open`](Kernel::open). Only [`process`](Kernel::process) is required.
61pub trait Kernel: Default + 'static {
62    /// Declare port types and required side packets. Optional: declaring nothing means every
63    /// port accepts any payload type.
64    fn get_contract(_c: &mut KernelContract) {}
65    /// Called once when the graph starts: read options, allocate long-lived state.
66    fn open(&mut self, _cc: &mut KernelCtx) -> Result<()> {
67        Ok(())
68    }
69    /// Called on every activation: read the inputs, produce output. **Required.**
70    fn process(&mut self, cc: &mut KernelCtx) -> Result<()>;
71    /// Called once when the stream closes: flush, release, finalise.
72    fn close(&mut self, _cc: &mut KernelCtx) -> Result<()> {
73        Ok(())
74    }
75}
76
77/// The context of one kernel callback — a safe wrapper around the internal `Context`.
78///
79/// Valid **only for the duration of the callback**; it must not be stored. While a kernel runs the
80/// engine holds no internal lock, so a kernel is free to block, lock, or acquire the GIL here.
81pub struct KernelCtx<'a> {
82    inner: &'a mut Context,
83}
84
85impl KernelCtx<'_> {
86    pub fn num_inputs(&self) -> usize {
87        self.inner.in_ports.len()
88    }
89    pub fn num_outputs(&self) -> usize {
90        self.inner.out_ports.len()
91    }
92    /// Borrow input packet `idx` (single-packet policies). Under the `batch` policy use
93    /// [`input_count`](Self::input_count) / [`input_at`](Self::input_at) to walk the batch.
94    pub fn input(&self, idx: usize) -> Option<&Packet> {
95        self.inner.input(idx)
96    }
97    pub fn input_count(&self, idx: usize) -> usize {
98        self.inner.input_count(idx)
99    }
100    pub fn input_at(&self, idx: usize, k: usize) -> Option<&Packet> {
101        self.inner.input_at(idx, k)
102    }
103    /// Take ownership of an input packet — the first step to a copy-free mutation, since
104    /// copy-on-write only avoids the copy when this is the sole reference.
105    pub fn take_input(&mut self, idx: usize) -> Packet {
106        self.inner.take_input(idx)
107    }
108    /// The (aligned) timestamp of this activation.
109    pub fn input_timestamp(&self) -> Timestamp {
110        self.inner.input_ts
111    }
112    /// Whether this input port is finished — upstream closed and drained.
113    pub fn input_is_done(&self, idx: usize) -> bool {
114        self.inner.inputs_done.get(idx).copied().unwrap_or(false)
115    }
116    /// Produce a packet on output port `out_idx`.
117    /// An unset timestamp inherits this activation's timestamp. The resulting timestamp
118    /// must be legal stream data; in `close`, supply an explicit timestamp because the
119    /// activation timestamp is `Done`.
120    pub fn emit(&mut self, out_idx: usize, pkt: Packet) -> Result<()> {
121        self.inner.emit(out_idx, pkt)
122    }
123    /// Zero-copy passthrough: forward input `in_idx` to output `out_idx`.
124    pub fn forward(&mut self, in_idx: usize, out_idx: usize) -> Result<()> {
125        self.inner.forward(in_idx, out_idx)
126    }
127    /// Advance the downstream timestamp bound when producing nothing — otherwise consumers keep
128    /// waiting for a packet that will never arrive.
129    pub fn set_next_bound(&mut self, out_idx: usize, bound: Timestamp) {
130        self.inner.set_next_bound(out_idx, bound)
131    }
132    /// For a source kernel (no inputs): report that it has produced everything, so the engine
133    /// closes the stream and terminates.
134    pub fn source_done(&mut self) {
135        self.inner.source_done = true;
136    }
137    /// For a source kernel (no inputs): cooperatively release the worker and ask the engine to
138    /// invoke the source again after `delay`.
139    pub fn source_yield(&mut self, delay: std::time::Duration) {
140        self.inner.source_yield = Some(delay);
141    }
142    /// Record a failure reason (usually paired with `return Err(cc.fail(...))`).
143    pub fn set_error(&mut self, msg: &str) {
144        self.inner.set_error(msg);
145    }
146    /// Build a kernel error carrying a reason, for `return Err(cc.fail("..."))`.
147    pub fn fail(&self, msg: &str) -> Error {
148        Error::Kernel(msg.to_string())
149    }
150
151    // ---- 日志与计数器(与 C++ 侧 `cc.Log` / `cc.CounterAdd` 对应)----
152    /// Write an engine log line (levels are the `LOG_*` constants in [`crate::runtime`]). This
153    /// goes through the engine's log callback rather than stealing the host's stdout.
154    pub fn log(&self, level: i32, msg: &str) {
155        self.inner.log(level, msg);
156    }
157    /// Whether a log sink is installed. **Ask before formatting** — `format!` allocates, and
158    /// kernel logging is often per-packet, so it should cost nothing when nobody is listening:
159    ///
160    /// ```ignore
161    /// if cc.log_enabled() {
162    ///     cc.log(LOG_DEBUG, &format!("frame {}", n));
163    /// }
164    /// ```
165    pub fn log_enabled(&self) -> bool {
166        crate::runtime::log_enabled()
167    }
168    /// Add to a named **per-graph** counter. Easier to assert on than a log line — read it back
169    /// afterwards with [`crate::Graph::counter_value`].
170    pub fn counter_add(&self, name: &str, delta: i64) {
171        self.inner.shared.counter_add(name, delta);
172    }
173
174    // ---- node options(路径支持点号嵌套,如 "roi.x")----
175    /// Whether the node declared this option. Paths may be dotted, e.g. `"roi.x"`.
176    pub fn has_option(&self, key: &str) -> bool {
177        self.inner.options.has(key)
178    }
179    pub fn option_i64(&self, key: &str, def: i64) -> i64 {
180        self.inner.options.i64(key).unwrap_or(def)
181    }
182    pub fn option_f64(&self, key: &str, def: f64) -> f64 {
183        self.inner.options.f64(key).unwrap_or(def)
184    }
185    pub fn option_bool(&self, key: &str, def: bool) -> bool {
186        self.inner.options.bool(key).unwrap_or(def)
187    }
188    pub fn option_str<'s>(&'s self, key: &str, def: &'s str) -> &'s str {
189        self.inner.options.str(key).unwrap_or(def)
190    }
191    /// Whether a host-injected constant input exists.
192    pub fn has_side_packet(&self, name: &str) -> bool {
193        self.inner.side_packets.contains_key(name)
194    }
195    /// Borrow a host-injected constant input.
196    pub fn side_packet(&self, name: &str) -> Option<&Packet> {
197        self.inner.side_packets.get(name)
198    }
199}
200
201/// The port-contract declaration handed to [`Kernel::get_contract`] — a safe wrapper around the
202/// internal `Contract`, valid only for the duration of that call.
203///
204/// Type ids live in [`crate::packet::type_id`](crate::packet) (`I64` / `F64` / `BUFFER` / …);
205/// `0` means "any type". Invalid indexes, invalid type ids, duplicate/empty side-packet names,
206/// and panics make graph construction fail instead of being ignored.
207pub struct KernelContract<'a> {
208    inner: &'a mut Contract,
209}
210
211impl KernelContract<'_> {
212    pub fn num_inputs(&self) -> usize {
213        self.inner.input_types.len()
214    }
215    pub fn num_outputs(&self) -> usize {
216        self.inner.output_types.len()
217    }
218    /// Declare the payload type of input port `i` (`0` = any; use
219    /// `crate::packet::type_id::I64` and friends for the built-in types).
220    pub fn input_type(&mut self, i: usize, type_id: u64) {
221        if let Some(t) = self.inner.input_types.get_mut(i) {
222            *t = type_id;
223        } else {
224            self.inner.record_error(format!(
225                "input port index {i} is out of range (num_inputs={})",
226                self.inner.input_types.len()
227            ));
228        }
229    }
230    /// Declare the payload type of output port `i`. See [`input_type`](Self::input_type).
231    pub fn output_type(&mut self, i: usize, type_id: u64) {
232        if let Some(t) = self.inner.output_types.get_mut(i) {
233            *t = type_id;
234        } else {
235            self.inner.record_error(format!(
236                "output port index {i} is out of range (num_outputs={})",
237                self.inner.output_types.len()
238            ));
239        }
240    }
241    /// Accept any payload type on input port `i` (the default).
242    pub fn input_any(&mut self, i: usize) {
243        self.input_type(i, 0);
244    }
245    /// Produce any payload type on output port `i` (the default).
246    pub fn output_any(&mut self, i: usize) {
247        self.output_type(i, 0);
248    }
249    /// Declare a required side packet; if the host forgets to inject it, graph init fails.
250    pub fn require_side_packet(&mut self, name: &str) {
251        if name.is_empty() {
252            self.inner
253                .record_error("required side packet name must not be empty");
254        } else {
255            self.inner.required_side_packets.push(name.to_string());
256        }
257    }
258}
259
260// ---------------------------------------------------------------- 泛型 extern "C" 蹦床
261// 签名对齐 crate::kernel::KernelVTable(ctx/contract 用 *mut c_void)。异常/panic 绝不穿越
262// extern "C" 边界:catch_unwind 兜住,失败返回状态码(与 flow.hpp 的 try/catch 对称)。
263
264unsafe extern "C" fn tramp_create<T: Kernel>(_factory: *mut c_void) -> *mut c_void {
265    match catch_unwind(|| Box::into_raw(Box::<T>::default()) as *mut c_void) {
266        Ok(p) => p,
267        Err(_) => std::ptr::null_mut(), // 构造 panic → null self;后续 open/process 因此报错、图失败
268    }
269}
270
271unsafe extern "C" fn tramp_destroy<T: Kernel>(self_: *mut c_void) {
272    if !self_.is_null() {
273        // Catch inside the C trampoline: an outer FFI guard cannot catch a
274        // panic that has already crossed this non-unwinding ABI boundary.
275        let result = catch_unwind(AssertUnwindSafe(|| drop(Box::from_raw(self_ as *mut T))));
276        if result.is_err() {
277            crate::runtime::log_warn(&format!(
278                "Rust kernel `{}` panicked during destruction (ignored)",
279                std::any::type_name::<T>()
280            ));
281        }
282    }
283}
284
285unsafe extern "C" fn tramp_get_contract<T: Kernel>(_self: *mut c_void, c: *mut c_void) {
286    if c.is_null() {
287        return;
288    }
289    let result = catch_unwind(AssertUnwindSafe(|| {
290        let mut kc = KernelContract {
291            inner: &mut *(c as *mut Contract),
292        };
293        T::get_contract(&mut kc);
294    }));
295    if let Err(payload) = result {
296        let message = payload
297            .downcast_ref::<&str>()
298            .map(|message| (*message).to_string())
299            .or_else(|| payload.downcast_ref::<String>().cloned())
300            .unwrap_or_else(|| "Rust panic with non-string payload".to_string());
301        (*(c as *mut Contract)).record_error(format!("Rust panic: {message}"));
302    }
303}
304
305/// open / process / close 三个 phase 的公共骨架。
306unsafe fn run_phase<T: Kernel>(
307    self_: *mut c_void,
308    ctx: *mut c_void,
309    f: impl FnOnce(&mut T, &mut KernelCtx) -> Result<()>,
310) -> i32 {
311    if self_.is_null() || ctx.is_null() {
312        return code::KERNEL; // create 失败(构造 panic)等
313    }
314    let r = catch_unwind(AssertUnwindSafe(|| {
315        let k = &mut *(self_ as *mut T);
316        let mut cc = KernelCtx {
317            inner: &mut *(ctx as *mut Context),
318        };
319        match f(k, &mut cc) {
320            Ok(()) => code::OK,
321            Err(e) => {
322                cc.inner.set_error(&e.to_string());
323                e.code()
324            }
325        }
326    }));
327    match r {
328        Ok(rc) => rc,
329        Err(_) => {
330            // 闭包已 unwind(其 &mut 借用随之释放),重新借 ctx 记录原因(该槽调用期独占存活)。
331            (*(ctx as *mut Context)).set_error("kernel panicked");
332            code::PANIC
333        }
334    }
335}
336
337unsafe extern "C" fn tramp_open<T: Kernel>(self_: *mut c_void, ctx: *mut c_void) -> i32 {
338    run_phase::<T>(self_, ctx, |k, cc| k.open(cc))
339}
340unsafe extern "C" fn tramp_process<T: Kernel>(self_: *mut c_void, ctx: *mut c_void) -> i32 {
341    run_phase::<T>(self_, ctx, |k, cc| k.process(cc))
342}
343unsafe extern "C" fn tramp_close<T: Kernel>(self_: *mut c_void, ctx: *mut c_void) -> i32 {
344    run_phase::<T>(self_, ctx, |k, cc| k.close(cc))
345}
346
347/// Register a Rust kernel under `name`.
348///
349/// Call once **before [`Graph::from_yaml`](crate::Graph::from_yaml)**; YAML then refers to it as
350/// `kernel: name`. Registering a name twice is an error, as it is for the built-ins.
351pub fn register_kernel<T: Kernel>(name: &str) -> Result<()> {
352    let vt = KernelVTable {
353        create: Some(tramp_create::<T>),
354        get_contract: Some(tramp_get_contract::<T>),
355        open: Some(tramp_open::<T>),
356        process: Some(tramp_process::<T>),
357        close: Some(tramp_close::<T>),
358        destroy: Some(tramp_destroy::<T>),
359    };
360    register_with_language(name, vt, std::ptr::null_mut(), KernelLanguage::Rust)
361}