web_server_abstraction/
ffi.rs

1//! FFI Layer - Multi-language SDK Support
2//!
3//! This module provides a C-compatible Foreign Function Interface (FFI) that
4//! enables the web server abstraction to be used from other programming languages.
5
6use crate::types::HttpMethod;
7
8use std::{
9    collections::HashMap,
10    ffi::{CStr, CString},
11    os::raw::{c_char, c_int},
12    ptr,
13    sync::{Mutex, OnceLock},
14};
15
16/// Global FFI context with thread-safe access
17static FFI_CONTEXT: OnceLock<Mutex<FfiContext>> = OnceLock::new();
18
19/// FFI context for managing server instances and state
20pub struct FfiContext {
21    servers: HashMap<u32, Box<dyn FfiServer>>,
22    next_id: u32,
23    last_error: Option<String>,
24}
25
26impl FfiContext {
27    fn new() -> Self {
28        Self {
29            servers: HashMap::new(),
30            next_id: 1,
31            last_error: None,
32        }
33    }
34
35    fn add_server(&mut self, server: Box<dyn FfiServer>) -> u32 {
36        let id = self.next_id;
37        self.next_id += 1;
38        self.servers.insert(id, server);
39        id
40    }
41
42    fn get_server(&mut self, id: u32) -> Option<&mut Box<dyn FfiServer>> {
43        self.servers.get_mut(&id)
44    }
45
46    fn remove_server(&mut self, id: u32) -> Option<Box<dyn FfiServer>> {
47        self.servers.remove(&id)
48    }
49
50    fn set_error(&mut self, error: String) {
51        self.last_error = Some(error);
52    }
53
54    fn get_last_error(&self) -> Option<&String> {
55        self.last_error.as_ref()
56    }
57}
58
59/// FFI-compatible server trait
60trait FfiServer: Send + Sync {
61    fn add_route(&mut self, path: &str, method: HttpMethod, handler_id: u32) -> Result<(), String>;
62    fn bind(&mut self, addr: &str) -> Result<(), String>;
63    fn start(&mut self) -> Result<(), String>;
64    fn stop(&mut self) -> Result<(), String>;
65    fn is_running(&self) -> bool;
66}
67
68/// Mock FFI server for testing
69struct MockFfiServer {
70    routes: Vec<(String, HttpMethod, u32)>,
71    address: Option<String>,
72    running: bool,
73}
74
75impl MockFfiServer {
76    fn new() -> Self {
77        Self {
78            routes: Vec::new(),
79            address: None,
80            running: false,
81        }
82    }
83}
84
85impl FfiServer for MockFfiServer {
86    fn add_route(&mut self, path: &str, method: HttpMethod, handler_id: u32) -> Result<(), String> {
87        self.routes.push((path.to_string(), method, handler_id));
88        Ok(())
89    }
90
91    fn bind(&mut self, addr: &str) -> Result<(), String> {
92        self.address = Some(addr.to_string());
93        Ok(())
94    }
95
96    fn start(&mut self) -> Result<(), String> {
97        if self.address.is_none() {
98            return Err("Server not bound to an address".to_string());
99        }
100        self.running = true;
101        Ok(())
102    }
103
104    fn stop(&mut self) -> Result<(), String> {
105        self.running = false;
106        Ok(())
107    }
108
109    fn is_running(&self) -> bool {
110        self.running
111    }
112}
113
114/// Get mutable reference to FFI context
115fn get_ffi_context() -> std::sync::MutexGuard<'static, FfiContext> {
116    FFI_CONTEXT
117        .get_or_init(|| Mutex::new(FfiContext::new()))
118        .lock()
119        .unwrap()
120}
121
122/// Convert C string to Rust string
123unsafe fn c_str_to_string(c_str: *const c_char) -> Result<String, String> {
124    if c_str.is_null() {
125        return Err("Null pointer provided".to_string());
126    }
127
128    unsafe {
129        match CStr::from_ptr(c_str).to_str() {
130            Ok(s) => Ok(s.to_string()),
131            Err(_) => Err("Invalid UTF-8 string".to_string()),
132        }
133    }
134}
135
136/// Convert Rust string to C string (caller must free)
137fn string_to_c_str(s: &str) -> *mut c_char {
138    match CString::new(s) {
139        Ok(c_string) => c_string.into_raw(),
140        Err(_) => ptr::null_mut(),
141    }
142}
143
144/// Convert HttpMethod enum to C int
145#[allow(dead_code)]
146fn http_method_to_c_int(method: HttpMethod) -> c_int {
147    match method {
148        HttpMethod::GET => 0,
149        HttpMethod::POST => 1,
150        HttpMethod::PUT => 2,
151        HttpMethod::DELETE => 3,
152        HttpMethod::PATCH => 4,
153        HttpMethod::HEAD => 5,
154        HttpMethod::OPTIONS => 6,
155        HttpMethod::TRACE => 7,
156        HttpMethod::CONNECT => 8,
157    }
158}
159
160/// Convert C int to HttpMethod enum
161fn c_int_to_http_method(method: c_int) -> Result<HttpMethod, String> {
162    match method {
163        0 => Ok(HttpMethod::GET),
164        1 => Ok(HttpMethod::POST),
165        2 => Ok(HttpMethod::PUT),
166        3 => Ok(HttpMethod::DELETE),
167        4 => Ok(HttpMethod::PATCH),
168        5 => Ok(HttpMethod::HEAD),
169        6 => Ok(HttpMethod::OPTIONS),
170        7 => Ok(HttpMethod::TRACE),
171        8 => Ok(HttpMethod::CONNECT),
172        _ => Err(format!("Invalid HTTP method: {}", method)),
173    }
174}
175
176/// C-compatible result codes
177pub const FFI_SUCCESS: c_int = 0;
178pub const FFI_ERROR_NULL_POINTER: c_int = -1;
179pub const FFI_ERROR_INVALID_ARGUMENT: c_int = -2;
180pub const FFI_ERROR_SERVER_NOT_FOUND: c_int = -3;
181pub const FFI_ERROR_INTERNAL: c_int = -4;
182
183// ================================
184// FFI FUNCTIONS - C INTERFACE
185// ================================
186
187/// Initialize the FFI library
188/// Returns FFI_SUCCESS on success
189#[unsafe(no_mangle)]
190pub extern "C" fn ws_ffi_init() -> c_int {
191    // Context is automatically initialized on first access
192    FFI_SUCCESS
193}
194
195/// Create a new web server instance
196/// Returns server ID on success, negative value on error
197#[unsafe(no_mangle)]
198pub extern "C" fn ws_create_server() -> c_int {
199    let mut context = get_ffi_context();
200    let server = Box::new(MockFfiServer::new());
201    let id = context.add_server(server);
202    id as c_int
203}
204
205/// Bind server to an address
206/// Returns FFI_SUCCESS on success, negative value on error
207#[unsafe(no_mangle)]
208pub unsafe extern "C" fn ws_bind_server(server_id: c_int, address: *const c_char) -> c_int {
209    let mut context = get_ffi_context();
210
211    let addr = match unsafe { c_str_to_string(address) } {
212        Ok(s) => s,
213        Err(e) => {
214            context.set_error(e);
215            return FFI_ERROR_INVALID_ARGUMENT;
216        }
217    };
218
219    match context.get_server(server_id as u32) {
220        Some(server) => match server.bind(&addr) {
221            Ok(()) => FFI_SUCCESS,
222            Err(e) => {
223                context.set_error(e);
224                FFI_ERROR_INTERNAL
225            }
226        },
227        None => {
228            context.set_error("Server not found".to_string());
229            FFI_ERROR_SERVER_NOT_FOUND
230        }
231    }
232}
233
234/// Add a route to the server
235/// Returns FFI_SUCCESS on success, negative value on error
236#[unsafe(no_mangle)]
237pub unsafe extern "C" fn ws_add_route(
238    server_id: c_int,
239    path: *const c_char,
240    method: c_int,
241    handler_id: c_int,
242) -> c_int {
243    let mut context = get_ffi_context();
244
245    let path_str = match unsafe { c_str_to_string(path) } {
246        Ok(s) => s,
247        Err(e) => {
248            context.set_error(e);
249            return FFI_ERROR_INVALID_ARGUMENT;
250        }
251    };
252
253    let http_method = match c_int_to_http_method(method) {
254        Ok(m) => m,
255        Err(e) => {
256            context.set_error(e);
257            return FFI_ERROR_INVALID_ARGUMENT;
258        }
259    };
260
261    match context.get_server(server_id as u32) {
262        Some(server) => match server.add_route(&path_str, http_method, handler_id as u32) {
263            Ok(()) => FFI_SUCCESS,
264            Err(e) => {
265                context.set_error(e);
266                FFI_ERROR_INTERNAL
267            }
268        },
269        None => {
270            context.set_error("Server not found".to_string());
271            FFI_ERROR_SERVER_NOT_FOUND
272        }
273    }
274}
275
276/// Start the server
277/// Returns FFI_SUCCESS on success, negative value on error
278#[unsafe(no_mangle)]
279pub extern "C" fn ws_start_server(server_id: c_int) -> c_int {
280    let mut context = get_ffi_context();
281
282    match context.get_server(server_id as u32) {
283        Some(server) => match server.start() {
284            Ok(()) => FFI_SUCCESS,
285            Err(e) => {
286                context.set_error(e);
287                FFI_ERROR_INTERNAL
288            }
289        },
290        None => {
291            context.set_error("Server not found".to_string());
292            FFI_ERROR_SERVER_NOT_FOUND
293        }
294    }
295}
296
297/// Stop the server
298/// Returns FFI_SUCCESS on success, negative value on error
299#[unsafe(no_mangle)]
300pub extern "C" fn ws_stop_server(server_id: c_int) -> c_int {
301    let mut context = get_ffi_context();
302
303    match context.get_server(server_id as u32) {
304        Some(server) => match server.stop() {
305            Ok(()) => FFI_SUCCESS,
306            Err(e) => {
307                context.set_error(e);
308                FFI_ERROR_INTERNAL
309            }
310        },
311        None => {
312            context.set_error("Server not found".to_string());
313            FFI_ERROR_SERVER_NOT_FOUND
314        }
315    }
316}
317
318/// Check if server is running
319/// Returns 1 if running, 0 if not running, negative value on error
320#[unsafe(no_mangle)]
321pub extern "C" fn ws_is_server_running(server_id: c_int) -> c_int {
322    let mut context = get_ffi_context();
323
324    match context.get_server(server_id as u32) {
325        Some(server) => {
326            if server.is_running() {
327                1
328            } else {
329                0
330            }
331        }
332        None => {
333            context.set_error("Server not found".to_string());
334            FFI_ERROR_SERVER_NOT_FOUND
335        }
336    }
337}
338
339/// Destroy a server instance
340/// Returns FFI_SUCCESS on success, negative value on error
341#[unsafe(no_mangle)]
342pub extern "C" fn ws_destroy_server(server_id: c_int) -> c_int {
343    let mut context = get_ffi_context();
344
345    match context.remove_server(server_id as u32) {
346        Some(_) => FFI_SUCCESS,
347        None => {
348            context.set_error("Server not found".to_string());
349            FFI_ERROR_SERVER_NOT_FOUND
350        }
351    }
352}
353
354/// Get the last error message
355/// Returns pointer to error string (caller must free with ws_free_string)
356/// Returns null if no error
357#[unsafe(no_mangle)]
358pub extern "C" fn ws_get_last_error() -> *mut c_char {
359    let context = get_ffi_context();
360
361    match context.get_last_error() {
362        Some(error) => string_to_c_str(error),
363        None => ptr::null_mut(),
364    }
365}
366
367/// Free a string allocated by the FFI
368#[unsafe(no_mangle)]
369pub unsafe extern "C" fn ws_free_string(s: *mut c_char) {
370    if !s.is_null() {
371        unsafe {
372            let _ = CString::from_raw(s);
373        }
374    }
375}
376
377/// Get library version
378/// Returns pointer to version string (caller must free with ws_free_string)
379#[unsafe(no_mangle)]
380pub extern "C" fn ws_get_version() -> *mut c_char {
381    string_to_c_str(env!("CARGO_PKG_VERSION"))
382}
383
384/// Cleanup the FFI library (call at program exit)
385#[unsafe(no_mangle)]
386pub extern "C" fn ws_ffi_cleanup() -> c_int {
387    // Note: In a real implementation, you'd clean up any remaining resources
388    FFI_SUCCESS
389}
390
391// ================================
392// LANGUAGE-SPECIFIC HELPERS
393// ================================
394
395/// Python bindings helper structures
396#[repr(C)]
397pub struct PythonServerHandle {
398    pub server_id: c_int,
399    pub is_valid: c_int,
400}
401
402/// Create Python-compatible server handle
403#[unsafe(no_mangle)]
404pub extern "C" fn ws_create_python_server() -> PythonServerHandle {
405    let server_id = ws_create_server();
406    PythonServerHandle {
407        server_id,
408        is_valid: if server_id >= 0 { 1 } else { 0 },
409    }
410}
411
412/// Node.js bindings helper - create server with callback support
413#[unsafe(no_mangle)]
414pub extern "C" fn ws_create_nodejs_server(
415    _callback: extern "C" fn(*const c_char, c_int, *const c_char) -> c_int,
416) -> c_int {
417    // In a real implementation, you'd store the callback for later use
418    ws_create_server()
419}
420
421/// Go bindings helper - create server with Go-style error handling
422#[unsafe(no_mangle)]
423pub unsafe extern "C" fn ws_create_go_server(error_out: *mut *mut c_char) -> c_int {
424    let server_id = ws_create_server();
425
426    if server_id < 0 {
427        let error = ws_get_last_error();
428        if !error.is_null() {
429            unsafe {
430                *error_out = error;
431            }
432        }
433    } else {
434        unsafe {
435            *error_out = ptr::null_mut();
436        }
437    }
438
439    server_id
440}
441
442// ================================
443// HEADER GENERATION HELPER
444// ================================
445
446/// Generate C header file content for FFI bindings
447pub fn generate_c_header() -> String {
448    r#"
449#ifndef WEB_SERVER_ABSTRACTION_FFI_H
450#define WEB_SERVER_ABSTRACTION_FFI_H
451
452#ifdef __cplusplus
453extern "C" {
454#endif
455
456// Result codes
457#define FFI_SUCCESS 0
458#define FFI_ERROR_NULL_POINTER -1
459#define FFI_ERROR_INVALID_ARGUMENT -2
460#define FFI_ERROR_SERVER_NOT_FOUND -3
461#define FFI_ERROR_INTERNAL -4
462
463// HTTP methods
464#define HTTP_GET 0
465#define HTTP_POST 1
466#define HTTP_PUT 2
467#define HTTP_DELETE 3
468#define HTTP_PATCH 4
469#define HTTP_HEAD 5
470#define HTTP_OPTIONS 6
471#define HTTP_TRACE 7
472#define HTTP_CONNECT 8
473
474// Core functions
475int ws_ffi_init(void);
476int ws_create_server(void);
477int ws_bind_server(int server_id, const char* address);
478int ws_add_route(int server_id, const char* path, int method, int handler_id);
479int ws_start_server(int server_id);
480int ws_stop_server(int server_id);
481int ws_is_server_running(int server_id);
482int ws_destroy_server(int server_id);
483char* ws_get_last_error(void);
484void ws_free_string(char* s);
485char* ws_get_version(void);
486int ws_ffi_cleanup(void);
487
488// Language-specific helpers
489typedef struct {
490    int server_id;
491    int is_valid;
492} PythonServerHandle;
493
494PythonServerHandle ws_create_python_server(void);
495int ws_create_nodejs_server(int (*callback)(const char*, int, const char*));
496int ws_create_go_server(char** error_out);
497
498#ifdef __cplusplus
499}
500#endif
501
502#endif // WEB_SERVER_ABSTRACTION_FFI_H
503"#
504    .to_string()
505}
506
507/// Save C header to file
508pub fn save_c_header(path: &str) -> Result<(), std::io::Error> {
509    std::fs::write(path, generate_c_header())
510}
511
512#[cfg(test)]
513mod tests {
514    use super::*;
515
516    #[test]
517    fn test_ffi_server_lifecycle() {
518        // Initialize FFI
519        assert_eq!(ws_ffi_init(), FFI_SUCCESS);
520
521        // Create server
522        let server_id = ws_create_server();
523        assert!(server_id >= 0);
524
525        // Bind server
526        let addr = CString::new("127.0.0.1:8080").unwrap();
527        assert_eq!(
528            unsafe { ws_bind_server(server_id, addr.as_ptr()) },
529            FFI_SUCCESS
530        );
531
532        // Add route
533        let path = CString::new("/test").unwrap();
534        assert_eq!(
535            unsafe { ws_add_route(server_id, path.as_ptr(), 0, 1) },
536            FFI_SUCCESS
537        );
538
539        // Start server
540        assert_eq!(ws_start_server(server_id), FFI_SUCCESS);
541        assert_eq!(ws_is_server_running(server_id), 1);
542
543        // Stop server
544        assert_eq!(ws_stop_server(server_id), FFI_SUCCESS);
545        assert_eq!(ws_is_server_running(server_id), 0);
546
547        // Destroy server
548        assert_eq!(ws_destroy_server(server_id), FFI_SUCCESS);
549
550        // Cleanup
551        assert_eq!(ws_ffi_cleanup(), FFI_SUCCESS);
552    }
553
554    #[test]
555    fn test_ffi_error_handling() {
556        ws_ffi_init();
557
558        // Test invalid server ID
559        assert_eq!(ws_start_server(999), FFI_ERROR_SERVER_NOT_FOUND);
560
561        // Get error message
562        let error = ws_get_last_error();
563        assert!(!error.is_null());
564
565        // Free error string
566        unsafe { ws_free_string(error) };
567    }
568
569    #[test]
570    fn test_ffi_version() {
571        let version = ws_get_version();
572        assert!(!version.is_null());
573        unsafe { ws_free_string(version) };
574    }
575
576    #[test]
577    fn test_python_bindings() {
578        ws_ffi_init();
579        let handle = ws_create_python_server();
580        assert_eq!(handle.is_valid, 1);
581        assert!(handle.server_id >= 0);
582        ws_destroy_server(handle.server_id);
583    }
584
585    #[test]
586    fn test_c_header_generation() {
587        let header = generate_c_header();
588        assert!(header.contains("#ifndef WEB_SERVER_ABSTRACTION_FFI_H"));
589        assert!(header.contains("int ws_create_server(void);"));
590        assert!(header.contains("#define FFI_SUCCESS 0"));
591    }
592}