ombrac_client/
ffi.rs

1use std::ffi::{CStr, c_char};
2use std::sync::{Arc, Mutex};
3
4use figment::Figment;
5use figment::providers::{Format, Json, Serialized};
6use tokio::runtime::{Builder, Runtime};
7
8use ombrac_macros::{error, info};
9
10use crate::config::{ConfigFile, ServiceConfig};
11#[cfg(feature = "tracing")]
12use crate::logging::LogCallback;
13#[cfg(feature = "transport-quic")]
14use crate::service::QuicServiceBuilder;
15use crate::service::Service;
16
17// A global, thread-safe handle to the running service instance.
18static SERVICE_HANDLE: Mutex<Option<ServiceHandle>> = Mutex::new(None);
19
20// Encapsulates the service instance and its associated Tokio runtime.
21struct ServiceHandle {
22    #[cfg(feature = "transport-quic")]
23    service: Option<
24        Box<Service<ombrac_transport::quic::client::Client, ombrac_transport::quic::Connection>>,
25    >,
26    runtime: Runtime,
27}
28
29/// A helper function to safely convert a C string pointer to a Rust string slice.
30/// Returns an empty string if the pointer is null.
31unsafe fn c_str_to_str<'a>(s: *const c_char) -> &'a str {
32    if s.is_null() {
33        return "";
34    }
35    unsafe { CStr::from_ptr(s).to_str().unwrap_or("") }
36}
37
38/// Initializes the logging system to use a C-style callback for log messages.
39///
40/// This function must be called before `ombrac_client_service_startup` if you wish to
41/// receive logs in a C-compatible way. It sets up a global logger that will
42/// forward all log records to the provided callback function.
43///
44/// # Arguments
45///
46/// * `callback` - A function pointer of type `LogCallback`. See the definition of
47///   `LogCallback` for the expected signature and log level mappings.
48///
49/// # Safety
50///
51/// The provided `callback` function pointer must be valid and remain valid for
52/// the lifetime of the program. If a null pointer is passed, logging will be
53/// disabled.
54#[cfg(feature = "tracing")]
55#[unsafe(no_mangle)]
56pub unsafe extern "C" fn ombrac_client_set_log_callback(callback: *const LogCallback) {
57    let callback = if callback.is_null() {
58        None
59    } else {
60        Some(unsafe { *callback })
61    };
62    crate::logging::set_log_callback(callback);
63}
64
65/// Initializes and starts the service with a given JSON configuration.
66///
67/// This function sets up the asynchronous runtime, parses the configuration,
68/// and launches the main service. It must be called before any other service
69/// operations. The service must be shut down via `ombrac_client_service_shutdown` to ensure
70/// a clean exit.
71///
72/// # Arguments
73///
74/// * `config_json` - A pointer to a null-terminated UTF-8 string containing the
75///   service configuration in JSON format.
76///
77/// # Returns
78///
79/// * `0` on success.
80/// * `-1` on failure (e.g., invalid configuration, service already running, or
81///   runtime initialization failed).
82///
83/// # Safety
84///
85/// The caller must ensure that `config_json` is a valid pointer to a
86/// null-terminated C string. This function is not thread-safe and should not be
87/// called concurrently with `ombrac_client_service_shutdown`.
88#[unsafe(no_mangle)]
89pub unsafe extern "C" fn ombrac_client_service_startup(config_json: *const c_char) -> i32 {
90    let config_str = unsafe { c_str_to_str(config_json) };
91
92    let config_file: ConfigFile = match Figment::new()
93        .merge(Serialized::defaults(ConfigFile::default()))
94        .merge(Json::string(config_str))
95        .extract()
96    {
97        Ok(cfg) => cfg,
98        Err(_e) => {
99            error!("Failed to parse config JSON: {_e}");
100            return -1;
101        }
102    };
103
104    let service_config = match (config_file.secret, config_file.server) {
105        (Some(secret), Some(server)) => ServiceConfig {
106            secret,
107            server,
108            handshake_option: config_file.handshake_option,
109            endpoint: config_file.endpoint,
110            #[cfg(feature = "transport-quic")]
111            transport: config_file.transport,
112            #[cfg(feature = "tracing")]
113            logging: config_file.logging,
114        },
115        (None, _) => {
116            error!("Configuration error: missing required field `secret` in JSON config");
117            return -1;
118        }
119        (_, None) => {
120            error!("Configuration error: missing required field `server` in JSON config");
121            return -1;
122        }
123    };
124
125    #[cfg(feature = "tracing")]
126    crate::logging::init_for_ffi(&service_config.logging);
127
128    let runtime = match Builder::new_multi_thread().enable_all().build() {
129        Ok(rt) => rt,
130        Err(_e) => {
131            error!("Failed to create Tokio runtime: {}", _e);
132            return -1;
133        }
134    };
135
136    let service = runtime.block_on(async {
137        #[cfg(feature = "transport-quic")]
138        Service::build::<QuicServiceBuilder>(Arc::new(service_config)).await
139    });
140
141    #[cfg(feature = "transport-quic")]
142    let service = match service {
143        Ok(s) => s,
144        Err(e) => {
145            error!("Failed to build service: {}", e);
146            return -1;
147        }
148    };
149
150    #[cfg(not(feature = "transport-quic"))]
151    {
152        error!("The application was compiled without a transport feature");
153        return -1;
154    }
155
156    let mut handle_guard = SERVICE_HANDLE.lock().unwrap();
157    if handle_guard.is_some() {
158        error!("Service is already running. Please shut down the existing service first.");
159        return -1;
160    }
161
162    *handle_guard = Some(ServiceHandle {
163        #[cfg(feature = "transport-quic")]
164        service: Some(Box::new(service)),
165        runtime,
166    });
167
168    info!("Service started successfully");
169
170    0
171}
172
173/// Triggers a network rebind on the underlying transport.
174///
175/// This is useful in scenarios where the network environment changes,
176/// to ensure the client can re-establish its connection through a new socket.
177///
178/// # Returns
179///
180/// * `0` on success.
181/// * `-1` if the service is not running or the rebind operation fails.
182///
183/// # Safety
184///
185/// This function is not thread-safe and should not be called concurrently with
186/// other service management functions.
187#[unsafe(no_mangle)]
188pub extern "C" fn ombrac_client_service_rebind() -> i32 {
189    let handle_guard = SERVICE_HANDLE.lock().unwrap();
190    if let Some(handle) = handle_guard.as_ref() {
191        #[cfg(feature = "transport-quic")]
192        if let Some(service) = &handle.service {
193            let result = handle.runtime.block_on(service.rebind());
194            if let Err(e) = result {
195                error!("Failed to rebind: {}", e);
196                return -1;
197            } else {
198                info!("Service rebind successful");
199                return 0;
200            }
201        }
202    }
203    -1
204}
205
206/// Shuts down the running service and releases all associated resources.
207///
208/// This function will gracefully stop the service and terminate the asynchronous
209/// runtime. It is safe to call even if the service was not started or has
210/// already been stopped.
211///
212/// # Returns
213///
214/// * `0` on completion.
215///
216/// # Safety
217///
218/// This function is not thread-safe and should not be called concurrently with
219/// `ombrac_client_service_startup`.
220#[unsafe(no_mangle)]
221pub extern "C" fn ombrac_client_service_shutdown() -> i32 {
222    let mut handle_guard = SERVICE_HANDLE.lock().unwrap();
223
224    if let Some(mut handle) = handle_guard.take() {
225        info!("Shutting down service");
226
227        #[cfg(feature = "transport-quic")]
228        if let Some(service) = handle.service.take() {
229            handle.runtime.block_on(async {
230                service.shutdown().await;
231            });
232        }
233
234        handle.runtime.shutdown_background();
235
236        info!("Service shut down complete.");
237    } else {
238        info!("Service was not running.");
239    }
240
241    0
242}
243
244/// Returns the version of the ombrac-client library.
245///
246/// The returned string is a null-terminated UTF-8 string. The memory for this
247/// string is managed by the library and should not be freed by the caller.
248#[unsafe(no_mangle)]
249pub extern "C" fn ombrac_client_get_version() -> *const c_char {
250    const VERSION_WITH_NULL: &str = concat!(env!("CARGO_PKG_VERSION"), "\0");
251    VERSION_WITH_NULL.as_ptr() as *const c_char
252}