mbus-ffi
WASM/JS and Native C/C++ FFI bindings for the modbus-rs stack.
Position In Workspace
mbus-ffi is an implementation wrapper crate inside this workspace. It encapsulates the core state machines of the modbus-rs stack, mapping them natively across two distinct abstraction boundaries:
- Web Run-times (WASM) via Javascript Promises.
- Native Run-times (C/C++) via opaque pointers, static client ID pools, and dependency-injected function callbacks.
Native C/C++ Bindings (FFI)
The native FFI is designed specifically for Strict no_std and embedded use cases:
- Zero Heap Allocations: The FFI path absolutely avoids
alloc(NoBox,Vec, or dynamic dispatch). - Static Client Pool: All Modbus clients allocated for native FFI exist in a thread-safe static pool using an ID-based system (
MbusClientId), preventing raw pointers from leaking across boundaries to easily integrate with memory-unsafe languages. - Zero OS dependencies: TCP/Serial abstractions are stripped out native compilation. The host application performs all network sockets, timer integrations, and byte transmissions by sending them upward into the Modbus stack via
MbusTransportCallbacks. panic=abort: Automatically detectsno_stdexecution properties gracefully throughbuild.rs.
Pool Configuration
Pool sizing is determined strictly at compile time. By default, the system provisions exactly 1 slot.
To increase maximum clients, set both environment variables explicitly:
MBUS_MAX_TCP_CLIENTS=10 MBUS_MAX_SERIAL_CLIENTS=10
MBUS_MAX_TCP_CLIENTS: valid range1..=127MBUS_MAX_SERIAL_CLIENTS: valid range1..=126
Build & Link
mbus-ffi supports compiling directly to shared (.so/.dylib) and static (.a) libraries:
Note: Even in strict no_std environments, standard LLVM targets like target/debug on mac/linux will naturally map underlying memory routines (memcpy, memmove) strictly via system libc. When targeting explicit embedded system triples like thumbv7em-none-eabihf, compiler built-ins will resolve them.
Automatic mbus_ffi.h Header Generation
We utilize cbindgen to define memory-perfect opaque wrappers for external model parsing:
Header / Feature Compatibility
mbus_ffi.h is generated from the Rust API shape of the enabled feature set.
The checked-in header is intended for native C builds with:
C API Quick Start (Transport Polling)
Instead of passing system sockets, you attach your exact runtime logic using POSIX or embedded UART controls directly via MbusTransportCallbacks:
// 1. Setup specific connection rules
struct MbusTcpConfig config = ;
config.host = "192.168.1.10";
config.port = 502;
// ... (timeouts/retries)
// 2. Setup your OS networking functions
struct MbusTransportCallbacks transport = ;
transport.userdata = &my_posix_socket_context;
transport.on_connect = my_os_connect;
transport.on_send = my_os_send;
transport.on_recv = my_os_recv;
// ...
// 3. Setup Response callbacks
struct MbusCallbacks app_callbacks = ;
app_callbacks.on_read_coils = my_app_read_coils;
// ...
MbusClientId client_id = ;
// Request the connection internally
;
;
// Must be continuously ticked within your device's task loop
while
For a full operational POSIX socket example and a self-contained serial PTY smoke path, view mbus-ffi/examples/c_smoke_cmake/main.c.
C Smoke Example
Build the smoke example with xtask:
This configures and builds the CMake target, then runs a PTY-backed serial RTU smoke test via CTest. The smoke binary also supports manual execution:
The --serial-pty mode is self-contained and does not require hardware. It creates a pseudo-terminal pair,
opens the slave side through the FFI serial client, and serves a single RTU read-coils response from the
master side so CI can exercise the serial path deterministically.
Running FFI Tests
The FFI layer has three distinct test paths:
- Rust-side unit tests inside
mbus-ffi - Native C binding-layer tests compiled from
mbus-ffi/tests/c_api/test_binding_layer.c - Higher-level native smoke tests under
examples/c_smoke_cmake
Rust-side FFI tests
Run the Rust unit tests and doc tests for mbus-ffi:
This validates the Rust-side FFI mapping code such as config translation, error/status mapping, and static client-pool behavior.
Native C binding-layer tests
First build the native FFI library with the C API enabled:
Then compile the standalone C binding test source:
Run it against the freshly built shared library:
macOS:
DYLD_LIBRARY_PATH="/target/debug"
Linux:
LD_LIBRARY_PATH="/target/debug"
This exercises the C ABI directly from C code, covering null-handling, invalid client IDs, status strings, accessor helpers, and native error paths.
If you already have the CMake-based test harness built under mbus-ffi/tests/c_api/build, you can also run:
Native C smoke test
Run the end-to-end smoke path with xtask:
This builds mbus-ffi with --features c,full, configures the CMake smoke project,
builds the c_smoke_test in ./mbus-ffi/examples/c_smoke_cmake/main.c, and runs the registered CTest cases.
Use this path for a higher-level native integration check in addition to the direct C binding-layer tests above.
Thread Safety
The C FFI layer is designed to be thread-safe, but it requires the host application to provide four lock/unlock symbols that the Rust library resolves at link time:
// Declared in mbus_ffi.h — you must define all four in your C/C++ project.
void ;
void ;
void ;
void ;
These are not function pointers in MbusTransportCallbacks — they are ordinary extern symbols
that the linker expects to find in your object files, exactly like implementing malloc for a
bare-metal libc. The header declares them; your application defines them.
| Symbol | Called when |
|---|---|
mbus_pool_lock / mbus_pool_unlock |
Any operation that allocates or frees a client slot |
mbus_client_lock(id) / mbus_client_unlock(id) |
Any operation that reads or mutates a specific client |
Single-threaded — define them as no-ops:
void
void
void
void
Multi-threaded — back them with real mutexes:
static pthread_mutex_t g_pool_mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_mutex_t g_client_mutex = ;
void
void
void
void
Callbacks fire inside the client lock
All response callbacks (on_read_coils, on_request_failed, …) are invoked
synchronously from within mbus_tcp_poll / mbus_serial_poll, while
mbus_client_lock(id) is already held for that client.
Consequence: do not call any mbus_tcp_* / mbus_serial_* API from inside a
callback — the attempt to re-acquire the same client lock will deadlock or return
MBUS_ERR_BUSY. Queue follow-up requests in a user-owned buffer and enqueue
them after poll returns.
WASM Browser Bindings
mbus-ffi securely exports internal modbus logic to JavaScript via wasm-pack, exposing:
WasmModbusClient(WebSocket transport mapper)WasmSerialModbusClient+request_serial_port()(Web Serial hardware mapper)
All APIs are Promise-based and are designed specifically for browser runtimes (wasm32). Building native targets does not interact with Javascript wrappers.
Build WASM Package
Generated JS/WASM package is written to mbus-ffi/pkg.
Quick Start (WebSocket)
import init from "./pkg/mbus_ffi.js";
await ;
const client = ;
const regs = await client.;
console.log;
Quick Start (Web Serial)
import init from "./pkg/mbus_ffi.js";
await ;
// Must be called from a user gesture (e.g. button double click)
const portHandle = await ;
const client = ;
const ok = await client.;
Example Web Pages
Use the browser examples under mbus-ffi/examples:
network_smoke.html(WebSocket/TCP path)serial_smoke.html(Web Serial path, full serial API smoke runner)
Serve the examples over localhost:
Supported Modbus Operations
Both FFI wrappers expose the same internal client services configured by feature flags:
coils: read single/multiple, write single/multipleregisters: read holding/input, write single/multiple, mask write, read-write multiplediscrete-inputs: read single/multiplefifo: readfile-record: read/writediagnostics: exception status, diagnostics, comm event counter/log, report server id, read device IDfull: Enables all Modbus service features.
Licensing
Copyright (C) 2025 Raghava Challari
This project is currently licensed under the GNU General Public License v3.0 (GPLv3).
For details, refer to the LICENSE file or the GPLv3 official site.
This crate is licensed under GPLv3. If you require a commercial license to use this crate in a proprietary project, please contact ch.raghava44@gmail.com to purchase a license.
Contact
Name: Raghava Ch
Email: ch.raghava44@gmail.com
Repository: github.com/Raghava-Ch/modbus-rs