mbus-ffi 0.13.0

Native C FFI and browser WASM bindings for modbus-rs client APIs, with optional generated server bindings
Documentation
//! Modbus Serial (RTU / ASCII) client — ID-based C API.

#![cfg(feature = "c-client")]

use mbus_client::services::ClientServices;
use mbus_core::transport::SerialMode;

use super::app::CApp;
use super::callbacks::MbusCallbacks;
use super::config::{MbusSerialConfig, serial_config_from_c};
use super::pool::{MBUS_INVALID_CLIENT_ID, MbusClientId, pool_free};

#[cfg(feature = "serial-rtu")]
use super::pool::pool_allocate_serial_rtu;

#[cfg(feature = "serial-ascii")]
use super::pool::pool_allocate_serial_ascii;

#[cfg(any(feature = "serial-rtu", feature = "serial-ascii"))]
use super::pool::with_serial_client_uniform;
use crate::c::error::MbusStatusCode;
use crate::c::transport::{MbusTransportCallbacks, validate_transport_callbacks};

#[cfg(feature = "serial-rtu")]
use crate::c::transport::CRtuTransport;

#[cfg(feature = "serial-ascii")]
use crate::c::transport::CAsciiTransport;

// ── Lifecycle ─────────────────────────────────────────────────────────────────

/// Create a new Modbus Serial client.
///
/// Returns a `MbusClientId` on success, or `MBUS_INVALID_CLIENT_ID` on failure.
///
/// # Safety
/// `config`, `transport_callbacks`, and `callbacks` must be valid non-null
/// pointers for the duration of this call. They are not retained after the
/// call returns.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn mbus_serial_client_new(
    config: *const MbusSerialConfig,
    transport_callbacks: *const MbusTransportCallbacks,
    callbacks: *const MbusCallbacks,
) -> MbusClientId {
    if callbacks.is_null() || transport_callbacks.is_null() {
        return MBUS_INVALID_CLIENT_ID;
    }

    let serial_config = match unsafe { serial_config_from_c(config) } {
        Ok(c) => c,
        Err(_) => return MBUS_INVALID_CLIENT_ID,
    };

    let cb = unsafe { callbacks.read() };
    if cb.on_current_millis.is_none() {
        return MBUS_INVALID_CLIENT_ID;
    }
    let transport_cb = unsafe { transport_callbacks.read() };
    if !validate_transport_callbacks(&transport_cb) {
        return MBUS_INVALID_CLIENT_ID;
    }

    #[allow(unreachable_code, unreachable_patterns)]
    match serial_config.mode {
        #[cfg(feature = "serial-rtu")]
        SerialMode::Rtu => {
            let app = CApp::new(cb);
            let transport = CRtuTransport::new(transport_cb);
            let inner = match ClientServices::new_serial(transport, app, serial_config) {
                Ok(i) => i,
                Err(_) => return MBUS_INVALID_CLIENT_ID,
            };
            match pool_allocate_serial_rtu(inner) {
                Ok(id) => id,
                Err(_) => MBUS_INVALID_CLIENT_ID,
            }
        }
        #[cfg(feature = "serial-ascii")]
        SerialMode::Ascii => {
            let app = CApp::new(cb);
            let transport = CAsciiTransport::new(transport_cb);
            let inner = match ClientServices::new_serial(transport, app, serial_config) {
                Ok(i) => i,
                Err(_) => return MBUS_INVALID_CLIENT_ID,
            };
            match pool_allocate_serial_ascii(inner) {
                Ok(id) => id,
                Err(_) => MBUS_INVALID_CLIENT_ID,
            }
        }
        _ => MBUS_INVALID_CLIENT_ID,
    }
}

/// Free a Modbus Serial client created by [`mbus_serial_client_new`].
///
/// After this call the ID is invalid and must not be used.
/// Passing `MBUS_INVALID_CLIENT_ID` is a no-op.
#[unsafe(no_mangle)]
pub extern "C" fn mbus_serial_client_free(id: MbusClientId) {
    if id != MBUS_INVALID_CLIENT_ID {
        pool_free(id);
    }
}

// ── Connection management ─────────────────────────────────────────────────────

/// Open the serial port with the configured parameters.
#[unsafe(no_mangle)]
pub extern "C" fn mbus_serial_connect(id: MbusClientId) -> MbusStatusCode {
    with_serial_client_uniform!(id, |inner| match inner.reconnect() {
        Ok(()) => MbusStatusCode::MbusOk,
        Err(e) => MbusStatusCode::from(e),
    })
    .unwrap_or_else(|e| e)
}

/// Close the serial port.
///
/// Pending in-flight requests are failed immediately with `MBUS_ERR_CONNECTION_LOST`.
/// The client ID remains valid; call [`mbus_serial_connect`] to reopen the port.
#[unsafe(no_mangle)]
pub extern "C" fn mbus_serial_disconnect(id: MbusClientId) -> MbusStatusCode {
    with_serial_client_uniform!(id, |inner| {
        inner.disconnect();
        MbusStatusCode::MbusOk
    })
    .unwrap_or_else(|e| e)
}

/// Returns `1` if the serial port is currently open, `0` otherwise.
#[unsafe(no_mangle)]
pub extern "C" fn mbus_serial_is_connected(id: MbusClientId) -> u8 {
    with_serial_client_uniform!(id, |inner| if inner.is_connected() { 1 } else { 0 }).unwrap_or(0)
}

// ── Poll ──────────────────────────────────────────────────────────────────────

/// Drive the serial Modbus state machine once.
///
/// Call periodically from your application loop. All registered callbacks are
/// invoked synchronously from within this call.
#[unsafe(no_mangle)]
pub extern "C" fn mbus_serial_poll(id: MbusClientId) {
    let _ = with_serial_client_uniform!(id, |inner| inner.poll());
}

/// Returns `1` if the client has in-flight requests awaiting response/timeout,
/// `0` otherwise.
#[unsafe(no_mangle)]
pub extern "C" fn mbus_serial_has_pending_requests(id: MbusClientId) -> u8 {
    with_serial_client_uniform!(id, |inner| {
        if inner.has_pending_requests() { 1 } else { 0 }
    })
    .unwrap_or(0)
}

/// Disconnect then reconnect the serial port.
#[unsafe(no_mangle)]
pub extern "C" fn mbus_serial_reconnect(id: MbusClientId) -> MbusStatusCode {
    with_serial_client_uniform!(id, |inner| match inner.reconnect() {
        Ok(()) => MbusStatusCode::MbusOk,
        Err(e) => MbusStatusCode::from(e),
    })
    .unwrap_or_else(|e| e)
}