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}