Skip to main content

wxdragon/
ipc.rs

1//! IPC (Inter-Process Communication) module for wxDragon.
2//!
3//! This module provides safe wrappers around wxWidgets' generic IPC classes (wxServer,
4//! wxClient, wxConnection). The underlying transport is platform-dependent:
5//! - **Windows**: Uses DDE (Dynamic Data Exchange), which is OS-native and does not
6//!   trigger firewall prompts.
7//! - **Unix/macOS**: Uses TCP sockets. Unix domain sockets are also supported when a
8//!   file path is passed as the service name instead of a port number.
9//!
10//! # Overview
11//!
12//! wxIPC provides a simple client-server model:
13//! - A **Server** listens on a service (port) and accepts connections
14//! - A **Client** connects to a server on a host/service/topic
15//! - A **Connection** is established between client and server for data exchange
16//!
17//! # Example
18//!
19//! ```rust,no_run
20//! use wxdragon::ipc::{IPCServer, IPCClient, IPCConnection, IPCFormat};
21//!
22//! // Server side
23//! let server = IPCServer::new(|topic| {
24//!     println!("Client connecting to topic: {}", topic);
25//!     Some(IPCConnection::builder()
26//!         .on_execute(|_topic, data, _format| {
27//!             println!("Received execute: {:?}", data);
28//!             true
29//!         })
30//!         .build())
31//! });
32//! server.create("4242"); // Listen on port 4242
33//!
34//! // Client side
35//! let client = IPCClient::new();
36//! if let Some(conn) = client.make_connection("localhost", "4242", "test") {
37//!     conn.execute_string("Hello, server!");
38//! }
39//! ```
40
41use std::ffi::{CStr, CString};
42use std::os::raw::c_void;
43use std::ptr;
44use wxdragon_sys as ffi;
45
46/// IPC data format for Execute, Request, Poke, and Advise operations.
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48#[repr(i32)]
49pub enum IPCFormat {
50    /// Text format (CF_TEXT)
51    Text = 1,
52    /// Bitmap format (CF_BITMAP)
53    Bitmap = 2,
54    /// Metafile format (CF_METAFILEPICT)
55    Metafile = 3,
56    /// Unicode text format (CF_UNICODETEXT)
57    UnicodeText = 13,
58    /// UTF-8 text format
59    Utf8Text = 14,
60    /// Private/binary data format
61    Private = 20,
62}
63
64impl From<ffi::wxd_IPCFormat> for IPCFormat {
65    fn from(format: ffi::wxd_IPCFormat) -> Self {
66        match format {
67            ffi::wxd_IPCFormat_WXD_IPC_TEXT => IPCFormat::Text,
68            ffi::wxd_IPCFormat_WXD_IPC_BITMAP => IPCFormat::Bitmap,
69            ffi::wxd_IPCFormat_WXD_IPC_METAFILE => IPCFormat::Metafile,
70            ffi::wxd_IPCFormat_WXD_IPC_UNICODETEXT => IPCFormat::UnicodeText,
71            ffi::wxd_IPCFormat_WXD_IPC_UTF8TEXT => IPCFormat::Utf8Text,
72            ffi::wxd_IPCFormat_WXD_IPC_PRIVATE => IPCFormat::Private,
73            _ => IPCFormat::Text,
74        }
75    }
76}
77
78impl From<IPCFormat> for ffi::wxd_IPCFormat {
79    fn from(format: IPCFormat) -> Self {
80        match format {
81            IPCFormat::Text => ffi::wxd_IPCFormat_WXD_IPC_TEXT,
82            IPCFormat::Bitmap => ffi::wxd_IPCFormat_WXD_IPC_BITMAP,
83            IPCFormat::Metafile => ffi::wxd_IPCFormat_WXD_IPC_METAFILE,
84            IPCFormat::UnicodeText => ffi::wxd_IPCFormat_WXD_IPC_UNICODETEXT,
85            IPCFormat::Utf8Text => ffi::wxd_IPCFormat_WXD_IPC_UTF8TEXT,
86            IPCFormat::Private => ffi::wxd_IPCFormat_WXD_IPC_PRIVATE,
87        }
88    }
89}
90
91// =============================================================================
92// Connection Callbacks - stored in a Box and passed to C++
93// =============================================================================
94
95// Type aliases for callback types to reduce complexity warnings
96type ExecuteCallback = Box<dyn FnMut(&str, &[u8], IPCFormat) -> bool>;
97type RequestCallback = Box<dyn FnMut(&str, &str, IPCFormat) -> Option<Vec<u8>>>;
98type PokeCallback = Box<dyn FnMut(&str, &str, &[u8], IPCFormat) -> bool>;
99type AdviseTopicCallback = Box<dyn FnMut(&str, &str) -> bool>;
100type AdviseDataCallback = Box<dyn FnMut(&str, &str, &[u8], IPCFormat) -> bool>;
101type DisconnectCallback = Box<dyn FnMut() -> bool>;
102type AcceptConnectionCallback = Box<dyn FnMut(&str) -> Option<IPCConnection>>;
103
104/// Internal structure holding all connection callbacks.
105struct ConnectionCallbacks {
106    on_execute: Option<ExecuteCallback>,
107    on_request: Option<RequestCallback>,
108    on_poke: Option<PokeCallback>,
109    on_start_advise: Option<AdviseTopicCallback>,
110    on_stop_advise: Option<AdviseTopicCallback>,
111    on_advise: Option<AdviseDataCallback>,
112    on_disconnect: Option<DisconnectCallback>,
113    /// Buffer for OnRequest response data (must outlive the callback return)
114    request_buffer: Vec<u8>,
115}
116
117impl ConnectionCallbacks {
118    fn new() -> Self {
119        Self {
120            on_execute: None,
121            on_request: None,
122            on_poke: None,
123            on_start_advise: None,
124            on_stop_advise: None,
125            on_advise: None,
126            on_disconnect: None,
127            request_buffer: Vec::new(),
128        }
129    }
130}
131
132// =============================================================================
133// C Callback Trampolines
134// =============================================================================
135
136// Allow unsafe operations in unsafe fns for cleaner trampoline code
137#[allow(unsafe_op_in_unsafe_fn)]
138unsafe extern "C" fn on_execute_trampoline(
139    user_data: *mut c_void,
140    topic: *const i8,
141    data: *const c_void,
142    size: usize,
143    format: ffi::wxd_IPCFormat,
144) -> bool {
145    if user_data.is_null() {
146        return false;
147    }
148    let callbacks = &mut *(user_data as *mut ConnectionCallbacks);
149    if let Some(ref mut cb) = callbacks.on_execute {
150        let topic_str = if topic.is_null() {
151            ""
152        } else {
153            CStr::from_ptr(topic).to_str().unwrap_or("")
154        };
155        let data_slice = if data.is_null() || size == 0 {
156            &[]
157        } else {
158            std::slice::from_raw_parts(data as *const u8, size)
159        };
160        return cb(topic_str, data_slice, IPCFormat::from(format));
161    }
162    false
163}
164
165#[allow(unsafe_op_in_unsafe_fn)]
166unsafe extern "C" fn on_request_trampoline(
167    user_data: *mut c_void,
168    topic: *const i8,
169    item: *const i8,
170    out_size: *mut usize,
171    format: ffi::wxd_IPCFormat,
172) -> *const c_void {
173    if user_data.is_null() {
174        return ptr::null();
175    }
176    let callbacks = &mut *(user_data as *mut ConnectionCallbacks);
177    if let Some(ref mut cb) = callbacks.on_request {
178        let topic_str = if topic.is_null() {
179            ""
180        } else {
181            CStr::from_ptr(topic).to_str().unwrap_or("")
182        };
183        let item_str = if item.is_null() {
184            ""
185        } else {
186            CStr::from_ptr(item).to_str().unwrap_or("")
187        };
188        if let Some(data) = cb(topic_str, item_str, IPCFormat::from(format)) {
189            // Store in buffer so it outlives this function
190            callbacks.request_buffer = data;
191            if !out_size.is_null() {
192                *out_size = callbacks.request_buffer.len();
193            }
194            return callbacks.request_buffer.as_ptr() as *const c_void;
195        }
196    }
197    if !out_size.is_null() {
198        *out_size = 0;
199    }
200    ptr::null()
201}
202
203#[allow(unsafe_op_in_unsafe_fn)]
204unsafe extern "C" fn on_poke_trampoline(
205    user_data: *mut c_void,
206    topic: *const i8,
207    item: *const i8,
208    data: *const c_void,
209    size: usize,
210    format: ffi::wxd_IPCFormat,
211) -> bool {
212    if user_data.is_null() {
213        return false;
214    }
215    let callbacks = &mut *(user_data as *mut ConnectionCallbacks);
216    if let Some(ref mut cb) = callbacks.on_poke {
217        let topic_str = if topic.is_null() {
218            ""
219        } else {
220            CStr::from_ptr(topic).to_str().unwrap_or("")
221        };
222        let item_str = if item.is_null() {
223            ""
224        } else {
225            CStr::from_ptr(item).to_str().unwrap_or("")
226        };
227        let data_slice = if data.is_null() || size == 0 {
228            &[]
229        } else {
230            std::slice::from_raw_parts(data as *const u8, size)
231        };
232        return cb(topic_str, item_str, data_slice, IPCFormat::from(format));
233    }
234    false
235}
236
237#[allow(unsafe_op_in_unsafe_fn)]
238unsafe extern "C" fn on_start_advise_trampoline(user_data: *mut c_void, topic: *const i8, item: *const i8) -> bool {
239    if user_data.is_null() {
240        return false;
241    }
242    let callbacks = &mut *(user_data as *mut ConnectionCallbacks);
243    if let Some(ref mut cb) = callbacks.on_start_advise {
244        let topic_str = if topic.is_null() {
245            ""
246        } else {
247            CStr::from_ptr(topic).to_str().unwrap_or("")
248        };
249        let item_str = if item.is_null() {
250            ""
251        } else {
252            CStr::from_ptr(item).to_str().unwrap_or("")
253        };
254        return cb(topic_str, item_str);
255    }
256    false
257}
258
259#[allow(unsafe_op_in_unsafe_fn)]
260unsafe extern "C" fn on_stop_advise_trampoline(user_data: *mut c_void, topic: *const i8, item: *const i8) -> bool {
261    if user_data.is_null() {
262        return false;
263    }
264    let callbacks = &mut *(user_data as *mut ConnectionCallbacks);
265    if let Some(ref mut cb) = callbacks.on_stop_advise {
266        let topic_str = if topic.is_null() {
267            ""
268        } else {
269            CStr::from_ptr(topic).to_str().unwrap_or("")
270        };
271        let item_str = if item.is_null() {
272            ""
273        } else {
274            CStr::from_ptr(item).to_str().unwrap_or("")
275        };
276        return cb(topic_str, item_str);
277    }
278    false
279}
280
281#[allow(unsafe_op_in_unsafe_fn)]
282unsafe extern "C" fn on_advise_trampoline(
283    user_data: *mut c_void,
284    topic: *const i8,
285    item: *const i8,
286    data: *const c_void,
287    size: usize,
288    format: ffi::wxd_IPCFormat,
289) -> bool {
290    if user_data.is_null() {
291        return false;
292    }
293    let callbacks = &mut *(user_data as *mut ConnectionCallbacks);
294    if let Some(ref mut cb) = callbacks.on_advise {
295        let topic_str = if topic.is_null() {
296            ""
297        } else {
298            CStr::from_ptr(topic).to_str().unwrap_or("")
299        };
300        let item_str = if item.is_null() {
301            ""
302        } else {
303            CStr::from_ptr(item).to_str().unwrap_or("")
304        };
305        let data_slice = if data.is_null() || size == 0 {
306            &[]
307        } else {
308            std::slice::from_raw_parts(data as *const u8, size)
309        };
310        return cb(topic_str, item_str, data_slice, IPCFormat::from(format));
311    }
312    false
313}
314
315#[allow(unsafe_op_in_unsafe_fn)]
316unsafe extern "C" fn on_disconnect_trampoline(user_data: *mut c_void) -> bool {
317    if user_data.is_null() {
318        return true;
319    }
320    let callbacks = &mut *(user_data as *mut ConnectionCallbacks);
321    if let Some(ref mut cb) = callbacks.on_disconnect {
322        return cb();
323    }
324    true
325}
326
327#[allow(unsafe_op_in_unsafe_fn)]
328unsafe extern "C" fn free_connection_callbacks(user_data: *mut c_void) {
329    if !user_data.is_null() {
330        let _ = Box::from_raw(user_data as *mut ConnectionCallbacks);
331    }
332}
333
334// =============================================================================
335// IPCConnection
336// =============================================================================
337
338/// A connection between an IPC client and server.
339///
340/// Connections are used to exchange data using Execute, Request, Poke, and Advise
341/// operations. The connection can be created by the server (in OnAcceptConnection)
342/// or returned from a client's MakeConnection call.
343pub struct IPCConnection {
344    ptr: *mut ffi::wxd_IPCConnection_t,
345    /// Whether we own the pointer and should destroy it
346    owned: bool,
347}
348
349impl IPCConnection {
350    /// Create a new connection builder.
351    pub fn builder() -> IPCConnectionBuilder {
352        IPCConnectionBuilder::new()
353    }
354
355    /// Create a connection from a raw pointer (used internally).
356    ///
357    /// # Safety
358    /// The pointer must be valid and the caller transfers ownership.
359    #[allow(dead_code)]
360    pub(crate) unsafe fn from_ptr(ptr: *mut ffi::wxd_IPCConnection_t) -> Option<Self> {
361        if ptr.is_null() {
362            None
363        } else {
364            Some(Self { ptr, owned: false })
365        }
366    }
367
368    /// Get the raw pointer (for internal use).
369    #[allow(dead_code)]
370    pub(crate) fn as_ptr(&self) -> *mut ffi::wxd_IPCConnection_t {
371        self.ptr
372    }
373
374    /// Execute a command on the remote side.
375    ///
376    /// On the server side, this triggers the client's OnExecute callback.
377    /// On the client side, this triggers the server's OnExecute callback.
378    pub fn execute(&self, data: &[u8], format: IPCFormat) -> bool {
379        if self.ptr.is_null() {
380            return false;
381        }
382        unsafe { ffi::wxd_IPCConnection_Execute(self.ptr, data.as_ptr() as *const c_void, data.len(), format.into()) }
383    }
384
385    /// Execute a string command (convenience method for text data).
386    pub fn execute_string(&self, data: &str) -> bool {
387        if self.ptr.is_null() {
388            return false;
389        }
390        let c_str = match CString::new(data) {
391            Ok(s) => s,
392            Err(_) => return false,
393        };
394        unsafe { ffi::wxd_IPCConnection_ExecuteString(self.ptr, c_str.as_ptr()) }
395    }
396
397    /// Request data from the remote side.
398    ///
399    /// Returns the data if the request was successful, None otherwise.
400    pub fn request(&self, item: &str, format: IPCFormat) -> Option<Vec<u8>> {
401        if self.ptr.is_null() {
402            return None;
403        }
404        let c_item = CString::new(item).ok()?;
405        let mut size: usize = 0;
406        let data_ptr = unsafe { ffi::wxd_IPCConnection_Request(self.ptr, c_item.as_ptr(), &mut size, format.into()) };
407        if data_ptr.is_null() || size == 0 {
408            return None;
409        }
410        let data_slice = unsafe { std::slice::from_raw_parts(data_ptr as *const u8, size) };
411        Some(data_slice.to_vec())
412    }
413
414    /// Poke data to the remote side.
415    pub fn poke(&self, item: &str, data: &[u8], format: IPCFormat) -> bool {
416        if self.ptr.is_null() {
417            return false;
418        }
419        let c_item = match CString::new(item) {
420            Ok(s) => s,
421            Err(_) => return false,
422        };
423        unsafe {
424            ffi::wxd_IPCConnection_Poke(
425                self.ptr,
426                c_item.as_ptr(),
427                data.as_ptr() as *const c_void,
428                data.len(),
429                format.into(),
430            )
431        }
432    }
433
434    /// Start an advise loop for the given item.
435    ///
436    /// The server will send updates via the OnAdvise callback when the item changes.
437    pub fn start_advise(&self, item: &str) -> bool {
438        if self.ptr.is_null() {
439            return false;
440        }
441        let c_item = match CString::new(item) {
442            Ok(s) => s,
443            Err(_) => return false,
444        };
445        unsafe { ffi::wxd_IPCConnection_StartAdvise(self.ptr, c_item.as_ptr()) }
446    }
447
448    /// Stop an advise loop for the given item.
449    pub fn stop_advise(&self, item: &str) -> bool {
450        if self.ptr.is_null() {
451            return false;
452        }
453        let c_item = match CString::new(item) {
454            Ok(s) => s,
455            Err(_) => return false,
456        };
457        unsafe { ffi::wxd_IPCConnection_StopAdvise(self.ptr, c_item.as_ptr()) }
458    }
459
460    /// Send advised data to the client (server-side only).
461    pub fn advise(&self, item: &str, data: &[u8], format: IPCFormat) -> bool {
462        if self.ptr.is_null() {
463            return false;
464        }
465        let c_item = match CString::new(item) {
466            Ok(s) => s,
467            Err(_) => return false,
468        };
469        unsafe {
470            ffi::wxd_IPCConnection_Advise(
471                self.ptr,
472                c_item.as_ptr(),
473                data.as_ptr() as *const c_void,
474                data.len(),
475                format.into(),
476            )
477        }
478    }
479
480    /// Disconnect the connection.
481    pub fn disconnect(&self) -> bool {
482        if self.ptr.is_null() {
483            return false;
484        }
485        unsafe { ffi::wxd_IPCConnection_Disconnect(self.ptr) }
486    }
487
488    /// Check if the connection is still connected.
489    pub fn is_connected(&self) -> bool {
490        if self.ptr.is_null() {
491            return false;
492        }
493        unsafe { ffi::wxd_IPCConnection_IsConnected(self.ptr) }
494    }
495}
496
497impl Drop for IPCConnection {
498    fn drop(&mut self) {
499        if self.owned && !self.ptr.is_null() {
500            unsafe { ffi::wxd_IPCConnection_Destroy(self.ptr) };
501        }
502    }
503}
504
505/// Builder for creating an IPCConnection with callbacks.
506pub struct IPCConnectionBuilder {
507    callbacks: ConnectionCallbacks,
508}
509
510impl IPCConnectionBuilder {
511    /// Create a new connection builder.
512    pub fn new() -> Self {
513        Self {
514            callbacks: ConnectionCallbacks::new(),
515        }
516    }
517
518    /// Set the OnExecute callback (server-side: called when client executes a command).
519    pub fn on_execute<F>(mut self, callback: F) -> Self
520    where
521        F: FnMut(&str, &[u8], IPCFormat) -> bool + 'static,
522    {
523        self.callbacks.on_execute = Some(Box::new(callback));
524        self
525    }
526
527    /// Set the OnRequest callback (server-side: called when client requests data).
528    pub fn on_request<F>(mut self, callback: F) -> Self
529    where
530        F: FnMut(&str, &str, IPCFormat) -> Option<Vec<u8>> + 'static,
531    {
532        self.callbacks.on_request = Some(Box::new(callback));
533        self
534    }
535
536    /// Set the OnPoke callback (server-side: called when client pokes data).
537    pub fn on_poke<F>(mut self, callback: F) -> Self
538    where
539        F: FnMut(&str, &str, &[u8], IPCFormat) -> bool + 'static,
540    {
541        self.callbacks.on_poke = Some(Box::new(callback));
542        self
543    }
544
545    /// Set the OnStartAdvise callback (server-side: called when client starts advise).
546    pub fn on_start_advise<F>(mut self, callback: F) -> Self
547    where
548        F: FnMut(&str, &str) -> bool + 'static,
549    {
550        self.callbacks.on_start_advise = Some(Box::new(callback));
551        self
552    }
553
554    /// Set the OnStopAdvise callback (server-side: called when client stops advise).
555    pub fn on_stop_advise<F>(mut self, callback: F) -> Self
556    where
557        F: FnMut(&str, &str) -> bool + 'static,
558    {
559        self.callbacks.on_stop_advise = Some(Box::new(callback));
560        self
561    }
562
563    /// Set the OnAdvise callback (client-side: called when server sends advised data).
564    pub fn on_advise<F>(mut self, callback: F) -> Self
565    where
566        F: FnMut(&str, &str, &[u8], IPCFormat) -> bool + 'static,
567    {
568        self.callbacks.on_advise = Some(Box::new(callback));
569        self
570    }
571
572    /// Set the OnDisconnect callback (both sides: called when connection is terminated).
573    pub fn on_disconnect<F>(mut self, callback: F) -> Self
574    where
575        F: FnMut() -> bool + 'static,
576    {
577        self.callbacks.on_disconnect = Some(Box::new(callback));
578        self
579    }
580
581    /// Build the connection.
582    pub fn build(self) -> IPCConnection {
583        let callbacks_box = Box::new(self.callbacks);
584        let user_data = Box::into_raw(callbacks_box) as *mut c_void;
585
586        let ptr = unsafe {
587            ffi::wxd_IPCConnection_Create(
588                user_data,
589                Some(on_execute_trampoline),
590                Some(on_request_trampoline),
591                Some(on_poke_trampoline),
592                Some(on_start_advise_trampoline),
593                Some(on_stop_advise_trampoline),
594                Some(on_advise_trampoline),
595                Some(on_disconnect_trampoline),
596                Some(free_connection_callbacks),
597            )
598        };
599
600        IPCConnection { ptr, owned: true }
601    }
602}
603
604impl Default for IPCConnectionBuilder {
605    fn default() -> Self {
606        Self::new()
607    }
608}
609
610// =============================================================================
611// IPCServer
612// =============================================================================
613
614/// Callback data for the server's OnAcceptConnection.
615struct ServerCallbacks {
616    on_accept: AcceptConnectionCallback,
617}
618
619#[allow(unsafe_op_in_unsafe_fn)]
620unsafe extern "C" fn on_accept_connection_trampoline(user_data: *mut c_void, topic: *const i8) -> *mut ffi::wxd_IPCConnection_t {
621    if user_data.is_null() {
622        return ptr::null_mut();
623    }
624    let callbacks = &mut *(user_data as *mut ServerCallbacks);
625    let topic_str = if topic.is_null() {
626        ""
627    } else {
628        CStr::from_ptr(topic).to_str().unwrap_or("")
629    };
630    if let Some(conn) = (callbacks.on_accept)(topic_str) {
631        // Transfer ownership to C++ - it will manage the connection
632        let ptr = conn.ptr;
633        std::mem::forget(conn);
634        ptr
635    } else {
636        ptr::null_mut()
637    }
638}
639
640#[allow(unsafe_op_in_unsafe_fn)]
641unsafe extern "C" fn free_server_callbacks(user_data: *mut c_void) {
642    if !user_data.is_null() {
643        let _ = Box::from_raw(user_data as *mut ServerCallbacks);
644    }
645}
646
647/// An IPC server that listens for client connections.
648///
649/// The server listens on a service and accepts connections from clients.
650/// On Windows, the service name identifies a DDE service. On Unix/macOS, the service
651/// can be a port number (TCP) or a file path (Unix domain socket).
652/// When a client connects, the OnAcceptConnection callback is called, which should
653/// return a new IPCConnection to handle the client.
654///
655/// # Example
656///
657/// ```rust,no_run
658/// use wxdragon::ipc::{IPCServer, IPCConnection, IPCFormat};
659///
660/// let server = IPCServer::new(|topic| {
661///     println!("Client connecting to topic: {}", topic);
662///     Some(IPCConnection::builder()
663///         .on_execute(|_topic, data, _format| {
664///             println!("Received: {:?}", String::from_utf8_lossy(data));
665///             true
666///         })
667///         .build())
668/// });
669///
670/// if server.create("4242") {
671///     println!("Server listening on port 4242");
672/// }
673/// ```
674pub struct IPCServer {
675    ptr: *mut ffi::wxd_IPCServer_t,
676}
677
678impl IPCServer {
679    /// Create a new IPC server with the given OnAcceptConnection callback.
680    ///
681    /// The callback receives the topic string and should return Some(IPCConnection)
682    /// to accept the connection, or None to reject it.
683    pub fn new<F>(on_accept_connection: F) -> Self
684    where
685        F: FnMut(&str) -> Option<IPCConnection> + 'static,
686    {
687        let callbacks = ServerCallbacks {
688            on_accept: Box::new(on_accept_connection),
689        };
690        let user_data = Box::into_raw(Box::new(callbacks)) as *mut c_void;
691
692        let ptr =
693            unsafe { ffi::wxd_IPCServer_Create(user_data, Some(on_accept_connection_trampoline), Some(free_server_callbacks)) };
694
695        Self { ptr }
696    }
697
698    /// Start the server listening on the given service.
699    ///
700    /// The service can be a port number (e.g., "4242") or a Unix socket path.
701    /// Returns true if the server started successfully.
702    pub fn create(&self, service: &str) -> bool {
703        if self.ptr.is_null() {
704            return false;
705        }
706        let c_service = match CString::new(service) {
707            Ok(s) => s,
708            Err(_) => return false,
709        };
710        unsafe { ffi::wxd_IPCServer_Create_Service(self.ptr, c_service.as_ptr()) }
711    }
712}
713
714impl Drop for IPCServer {
715    fn drop(&mut self) {
716        if !self.ptr.is_null() {
717            unsafe { ffi::wxd_IPCServer_Destroy(self.ptr) };
718        }
719    }
720}
721
722// =============================================================================
723// IPCClient
724// =============================================================================
725
726/// An IPC client that connects to servers.
727///
728/// The client connects to a server on a given host, service, and topic.
729/// On Windows, DDE is used (host is ignored for local DDE connections). On Unix/macOS,
730/// TCP sockets or Unix domain sockets are used depending on the service format.
731/// If the connection is successful, it returns an IPCConnection that can be
732/// used to exchange data with the server.
733///
734/// # Example
735///
736/// ```rust,no_run
737/// use wxdragon::ipc::{IPCClient, IPCFormat};
738///
739/// let client = IPCClient::new();
740///
741/// // Connect to a server
742/// if let Some(conn) = client.make_connection("localhost", "4242", "test") {
743///     // Send a command
744///     conn.execute_string("Hello, server!");
745///
746///     // Request data
747///     if let Some(data) = conn.request("status", IPCFormat::Text) {
748///         println!("Server status: {}", String::from_utf8_lossy(&data));
749///     }
750///
751///     // Disconnect when done
752///     conn.disconnect();
753/// }
754/// ```
755pub struct IPCClient {
756    ptr: *mut ffi::wxd_IPCClient_t,
757}
758
759impl IPCClient {
760    /// Create a new IPC client.
761    pub fn new() -> Self {
762        let ptr = unsafe { ffi::wxd_IPCClient_Create() };
763        Self { ptr }
764    }
765
766    /// Connect to a server.
767    ///
768    /// # Arguments
769    ///
770    /// * `host` - The hostname or IP address of the server
771    /// * `service` - The service (port number or socket path)
772    /// * `topic` - The topic to connect to
773    ///
774    /// Returns Some(IPCConnection) if the connection was successful, None otherwise.
775    pub fn make_connection(&self, host: &str, service: &str, topic: &str) -> Option<IPCConnection> {
776        self.make_connection_with_callbacks(host, service, topic, IPCConnectionBuilder::new())
777    }
778
779    /// Connect to a server with custom callbacks.
780    ///
781    /// This allows you to specify callbacks for the connection (e.g., OnAdvise
782    /// for receiving server-pushed updates).
783    pub fn make_connection_with_callbacks(
784        &self,
785        host: &str,
786        service: &str,
787        topic: &str,
788        builder: IPCConnectionBuilder,
789    ) -> Option<IPCConnection> {
790        if self.ptr.is_null() {
791            return None;
792        }
793
794        let c_host = CString::new(host).ok()?;
795        let c_service = CString::new(service).ok()?;
796        let c_topic = CString::new(topic).ok()?;
797
798        let callbacks_box = Box::new(builder.callbacks);
799        let user_data = Box::into_raw(callbacks_box) as *mut c_void;
800
801        let conn_ptr = unsafe {
802            ffi::wxd_IPCClient_MakeConnection(
803                self.ptr,
804                c_host.as_ptr(),
805                c_service.as_ptr(),
806                c_topic.as_ptr(),
807                user_data,
808                Some(on_execute_trampoline),
809                Some(on_request_trampoline),
810                Some(on_poke_trampoline),
811                Some(on_start_advise_trampoline),
812                Some(on_stop_advise_trampoline),
813                Some(on_advise_trampoline),
814                Some(on_disconnect_trampoline),
815                Some(free_connection_callbacks),
816            )
817        };
818
819        if conn_ptr.is_null() {
820            None
821        } else {
822            Some(IPCConnection {
823                ptr: conn_ptr,
824                owned: false, // Owned by the wxWidgets system
825            })
826        }
827    }
828}
829
830impl Default for IPCClient {
831    fn default() -> Self {
832        Self::new()
833    }
834}
835
836impl Drop for IPCClient {
837    fn drop(&mut self) {
838        if !self.ptr.is_null() {
839            unsafe { ffi::wxd_IPCClient_Destroy(self.ptr) };
840        }
841    }
842}