Skip to main content

stdiobus_ffi/
lib.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026-present Raman Marozau <raman@worktif.com>
3// Copyright (c) 2026-present stdiobus contributors
4
5#![cfg_attr(docsrs, feature(doc_cfg))]
6
7//! FFI bindings to libstdio_bus C library
8//!
9//! This crate provides raw FFI bindings to the stdio_bus embedding API
10//! as defined in `include/stdio_bus_embed.h`.
11//!
12//! For a safe Rust API, use `stdiobus-client` instead.
13
14#![allow(non_camel_case_types)]
15#![allow(non_upper_case_globals)]
16
17use std::os::raw::{c_char, c_int, c_void};
18
19/// API version
20pub const STDIO_BUS_EMBED_API_VERSION: c_int = 2;
21
22/// Return codes (from stdio_bus.h)
23pub const STDIO_BUS_OK: c_int = 0;
24pub const STDIO_BUS_ERR: c_int = -1;
25pub const STDIO_BUS_EAGAIN: c_int = -2;
26pub const STDIO_BUS_EOF: c_int = -3;
27pub const STDIO_BUS_EFULL: c_int = -4;
28pub const STDIO_BUS_ENOTFOUND: c_int = -5;
29pub const STDIO_BUS_EINVAL: c_int = -6;
30
31/// Error codes (from stdio_bus_embed.h)
32pub const STDIO_BUS_ERR_CONFIG: c_int = -10;
33pub const STDIO_BUS_ERR_WORKER: c_int = -11;
34pub const STDIO_BUS_ERR_ROUTING: c_int = -12;
35pub const STDIO_BUS_ERR_BUFFER: c_int = -13;
36pub const STDIO_BUS_ERR_INVALID: c_int = -14;
37pub const STDIO_BUS_ERR_STATE: c_int = -15;
38
39/// Bus state
40#[repr(C)]
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum stdio_bus_state_t {
43    STDIO_BUS_STATE_CREATED = 0,
44    STDIO_BUS_STATE_STARTING = 1,
45    STDIO_BUS_STATE_RUNNING = 2,
46    STDIO_BUS_STATE_STOPPING = 3,
47    STDIO_BUS_STATE_STOPPED = 4,
48}
49
50/// Listen mode
51#[repr(C)]
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum stdio_bus_listen_mode_t {
54    STDIO_BUS_LISTEN_NONE = 0,
55    STDIO_BUS_LISTEN_TCP = 1,
56    STDIO_BUS_LISTEN_UNIX = 2,
57}
58
59/// Opaque bus handle
60#[repr(C)]
61pub struct stdio_bus_t {
62    _private: [u8; 0],
63}
64
65/// Message callback
66pub type stdio_bus_message_cb = Option<
67    extern "C" fn(
68        bus: *mut stdio_bus_t,
69        msg: *const c_char,
70        len: usize,
71        user_data: *mut c_void,
72    ),
73>;
74
75/// Error callback
76pub type stdio_bus_error_cb = Option<
77    extern "C" fn(
78        bus: *mut stdio_bus_t,
79        code: c_int,
80        message: *const c_char,
81        user_data: *mut c_void,
82    ),
83>;
84
85/// Log callback
86pub type stdio_bus_log_cb = Option<
87    extern "C" fn(
88        bus: *mut stdio_bus_t,
89        level: c_int,
90        message: *const c_char,
91        user_data: *mut c_void,
92    ),
93>;
94
95/// Worker event callback
96pub type stdio_bus_worker_cb = Option<
97    extern "C" fn(
98        bus: *mut stdio_bus_t,
99        worker_id: c_int,
100        event: *const c_char,
101        user_data: *mut c_void,
102    ),
103>;
104
105/// Client connect callback
106pub type stdio_bus_client_connect_cb = Option<
107    extern "C" fn(
108        bus: *mut stdio_bus_t,
109        client_id: c_int,
110        peer_info: *const c_char,
111        user_data: *mut c_void,
112    ),
113>;
114
115/// Client disconnect callback
116pub type stdio_bus_client_disconnect_cb = Option<
117    extern "C" fn(
118        bus: *mut stdio_bus_t,
119        client_id: c_int,
120        reason: *const c_char,
121        user_data: *mut c_void,
122    ),
123>;
124
125/// Listener configuration
126#[repr(C)]
127pub struct stdio_bus_listener_config_t {
128    pub mode: stdio_bus_listen_mode_t,
129    pub tcp_host: *const c_char,
130    pub tcp_port: u16,
131    pub unix_path: *const c_char,
132}
133
134/// Options for creating a stdio_bus instance
135#[repr(C)]
136pub struct stdio_bus_options_t {
137    pub config_path: *const c_char,
138    pub config_json: *const c_char,
139    pub listener: stdio_bus_listener_config_t,
140    pub on_message: stdio_bus_message_cb,
141    pub on_error: stdio_bus_error_cb,
142    pub on_log: stdio_bus_log_cb,
143    pub on_worker: stdio_bus_worker_cb,
144    pub on_client_connect: stdio_bus_client_connect_cb,
145    pub on_client_disconnect: stdio_bus_client_disconnect_cb,
146    pub user_data: *mut c_void,
147    pub log_level: c_int,
148}
149
150/// Statistics
151#[repr(C)]
152#[derive(Debug, Clone, Default)]
153pub struct stdio_bus_stats_t {
154    pub messages_in: u64,
155    pub messages_out: u64,
156    pub bytes_in: u64,
157    pub bytes_out: u64,
158    pub worker_restarts: u64,
159    pub routing_errors: u64,
160    pub client_connects: u64,
161    pub client_disconnects: u64,
162}
163
164extern "C" {
165    /// Create a new stdio_bus instance
166    pub fn stdio_bus_create(options: *const stdio_bus_options_t) -> *mut stdio_bus_t;
167
168    /// Start the bus (spawn workers)
169    pub fn stdio_bus_start(bus: *mut stdio_bus_t) -> c_int;
170
171    /// Process pending I/O (non-blocking)
172    pub fn stdio_bus_step(bus: *mut stdio_bus_t, timeout_ms: c_int) -> c_int;
173
174    /// Initiate graceful shutdown
175    pub fn stdio_bus_stop(bus: *mut stdio_bus_t, timeout_sec: c_int) -> c_int;
176
177    /// Destroy instance and free resources
178    pub fn stdio_bus_destroy(bus: *mut stdio_bus_t);
179
180    /// Send a message into the bus
181    pub fn stdio_bus_ingest(bus: *mut stdio_bus_t, msg: *const c_char, len: usize) -> c_int;
182
183    /// Get current bus state
184    pub fn stdio_bus_get_state(bus: *const stdio_bus_t) -> stdio_bus_state_t;
185
186    /// Get number of active workers
187    pub fn stdio_bus_worker_count(bus: *const stdio_bus_t) -> c_int;
188
189    /// Get number of active sessions
190    pub fn stdio_bus_session_count(bus: *const stdio_bus_t) -> c_int;
191
192    /// Get number of pending requests
193    pub fn stdio_bus_pending_count(bus: *const stdio_bus_t) -> c_int;
194
195    /// Get number of connected clients
196    pub fn stdio_bus_client_count(bus: *const stdio_bus_t) -> c_int;
197
198    /// Get the underlying event loop fd
199    pub fn stdio_bus_get_poll_fd(bus: *const stdio_bus_t) -> c_int;
200
201    /// Get statistics
202    pub fn stdio_bus_get_stats(bus: *const stdio_bus_t, stats: *mut stdio_bus_stats_t);
203}
204
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209
210    // ============================================================================
211    // Constants Tests
212    // ============================================================================
213
214    #[test]
215    fn test_api_version() {
216        assert_eq!(STDIO_BUS_EMBED_API_VERSION, 2);
217    }
218
219    #[test]
220    fn test_return_codes() {
221        assert_eq!(STDIO_BUS_OK, 0);
222        assert_eq!(STDIO_BUS_ERR, -1);
223        assert_eq!(STDIO_BUS_EAGAIN, -2);
224        assert_eq!(STDIO_BUS_EOF, -3);
225        assert_eq!(STDIO_BUS_EFULL, -4);
226        assert_eq!(STDIO_BUS_ENOTFOUND, -5);
227        assert_eq!(STDIO_BUS_EINVAL, -6);
228    }
229
230    #[test]
231    fn test_error_codes() {
232        assert_eq!(STDIO_BUS_ERR_CONFIG, -10);
233        assert_eq!(STDIO_BUS_ERR_WORKER, -11);
234        assert_eq!(STDIO_BUS_ERR_ROUTING, -12);
235        assert_eq!(STDIO_BUS_ERR_BUFFER, -13);
236        assert_eq!(STDIO_BUS_ERR_INVALID, -14);
237        assert_eq!(STDIO_BUS_ERR_STATE, -15);
238    }
239
240    // ============================================================================
241    // Enum Tests
242    // ============================================================================
243
244    #[test]
245    fn test_bus_state_values() {
246        assert_eq!(stdio_bus_state_t::STDIO_BUS_STATE_CREATED as i32, 0);
247        assert_eq!(stdio_bus_state_t::STDIO_BUS_STATE_STARTING as i32, 1);
248        assert_eq!(stdio_bus_state_t::STDIO_BUS_STATE_RUNNING as i32, 2);
249        assert_eq!(stdio_bus_state_t::STDIO_BUS_STATE_STOPPING as i32, 3);
250        assert_eq!(stdio_bus_state_t::STDIO_BUS_STATE_STOPPED as i32, 4);
251    }
252
253    #[test]
254    fn test_listen_mode_values() {
255        assert_eq!(stdio_bus_listen_mode_t::STDIO_BUS_LISTEN_NONE as i32, 0);
256        assert_eq!(stdio_bus_listen_mode_t::STDIO_BUS_LISTEN_TCP as i32, 1);
257        assert_eq!(stdio_bus_listen_mode_t::STDIO_BUS_LISTEN_UNIX as i32, 2);
258    }
259
260    #[test]
261    fn test_bus_state_equality() {
262        let state1 = stdio_bus_state_t::STDIO_BUS_STATE_RUNNING;
263        let state2 = stdio_bus_state_t::STDIO_BUS_STATE_RUNNING;
264        let state3 = stdio_bus_state_t::STDIO_BUS_STATE_STOPPED;
265        
266        assert_eq!(state1, state2);
267        assert_ne!(state1, state3);
268    }
269
270    #[test]
271    fn test_bus_state_copy() {
272        let state = stdio_bus_state_t::STDIO_BUS_STATE_RUNNING;
273        let copied = state;
274        assert_eq!(state, copied);
275    }
276
277    #[test]
278    fn test_bus_state_debug() {
279        let state = stdio_bus_state_t::STDIO_BUS_STATE_RUNNING;
280        let debug = format!("{:?}", state);
281        assert!(debug.contains("RUNNING"));
282    }
283
284    // ============================================================================
285    // Struct Tests
286    // ============================================================================
287
288    #[test]
289    fn test_stats_default() {
290        let stats = stdio_bus_stats_t::default();
291        
292        assert_eq!(stats.messages_in, 0);
293        assert_eq!(stats.messages_out, 0);
294        assert_eq!(stats.bytes_in, 0);
295        assert_eq!(stats.bytes_out, 0);
296        assert_eq!(stats.worker_restarts, 0);
297        assert_eq!(stats.routing_errors, 0);
298        assert_eq!(stats.client_connects, 0);
299        assert_eq!(stats.client_disconnects, 0);
300    }
301
302    #[test]
303    fn test_stats_clone() {
304        let stats = stdio_bus_stats_t {
305            messages_in: 100,
306            messages_out: 50,
307            bytes_in: 1000,
308            bytes_out: 500,
309            worker_restarts: 2,
310            routing_errors: 1,
311            client_connects: 10,
312            client_disconnects: 5,
313        };
314        
315        let cloned = stats.clone();
316        assert_eq!(cloned.messages_in, 100);
317        assert_eq!(cloned.messages_out, 50);
318    }
319
320    #[test]
321    fn test_stats_debug() {
322        let stats = stdio_bus_stats_t::default();
323        let debug = format!("{:?}", stats);
324        assert!(debug.contains("messages_in"));
325    }
326
327    #[test]
328    fn test_listener_config_size() {
329        // Should be able to create the struct
330        let config = stdio_bus_listener_config_t {
331            mode: stdio_bus_listen_mode_t::STDIO_BUS_LISTEN_NONE,
332            tcp_host: std::ptr::null(),
333            tcp_port: 0,
334            unix_path: std::ptr::null(),
335        };
336        
337        assert_eq!(config.mode, stdio_bus_listen_mode_t::STDIO_BUS_LISTEN_NONE);
338        assert_eq!(config.tcp_port, 0);
339    }
340
341    #[test]
342    fn test_options_struct() {
343        use std::ptr;
344        
345        let listener = stdio_bus_listener_config_t {
346            mode: stdio_bus_listen_mode_t::STDIO_BUS_LISTEN_NONE,
347            tcp_host: ptr::null(),
348            tcp_port: 0,
349            unix_path: ptr::null(),
350        };
351        
352        let options = stdio_bus_options_t {
353            config_path: ptr::null(),
354            config_json: ptr::null(),
355            listener,
356            on_message: None,
357            on_error: None,
358            on_log: None,
359            on_worker: None,
360            on_client_connect: None,
361            on_client_disconnect: None,
362            user_data: ptr::null_mut(),
363            log_level: 1,
364        };
365        
366        assert_eq!(options.log_level, 1);
367        assert!(options.on_message.is_none());
368    }
369
370    // ============================================================================
371    // Callback Type Tests
372    // ============================================================================
373
374    #[test]
375    fn test_callback_types_are_option() {
376        // Verify callback types can be None
377        let msg_cb: stdio_bus_message_cb = None;
378        let err_cb: stdio_bus_error_cb = None;
379        let log_cb: stdio_bus_log_cb = None;
380        let worker_cb: stdio_bus_worker_cb = None;
381        let connect_cb: stdio_bus_client_connect_cb = None;
382        let disconnect_cb: stdio_bus_client_disconnect_cb = None;
383        
384        assert!(msg_cb.is_none());
385        assert!(err_cb.is_none());
386        assert!(log_cb.is_none());
387        assert!(worker_cb.is_none());
388        assert!(connect_cb.is_none());
389        assert!(disconnect_cb.is_none());
390    }
391
392    // Note: We can't test actual FFI calls without linking to libstdio_bus
393    // Those tests would be integration tests
394}