mbus-ffi 0.16.0

Native C FFI and browser WASM bindings for modbus-rs client APIs, with optional generated server bindings
Documentation

mbus-ffi

WASM/JS, Native C/C++ FFI, Python, .NET (C#), Go, and Node.js bindings for the modbus-rs stack.


Python Bindings (modbus-rs on PyPI)

The python feature compiles mbus-ffi into a Python extension module via PyO3 and Maturin. PyPI package: modbus-rs · Import name: modbus_rs

Installation

pip install modbus-rs

Quick Start — Synchronous TCP client

import modbus_rs

with modbus_rs.TcpTransport.connect("192.168.1.10", port=502) as transport:
    client = transport.create_client(unit_id=1)
    regs = client.read_holding_registers(0, 10)
    print(regs)                     # [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
    client.write_coil(0, True)
    coils = client.read_coils(0, 8)
    print(coils)                    # [True, False, ...]

Quick Start — Asyncio TCP client

import asyncio
import modbus_rs

async def main():
    async with await modbus_rs.AsyncTcpTransport.connect("192.168.1.10") as transport:
        client = transport.create_client(unit_id=1)
        regs = await client.read_holding_registers(0, 10)
        print(regs)

asyncio.run(main())

Quick Start — Serial client (RTU)

import modbus_rs

with modbus_rs.RtuTransport.open("/dev/ttyUSB0", baud_rate=9600) as transport:
    client = transport.create_client(unit_id=1)
    regs = client.read_holding_registers(0, 5)
    print(regs)

Quick Start — TCP server

import asyncio
import modbus_rs

class MyApp(modbus_rs.ModbusApp):
    def handle_read_holding_registers(self, address, count):
        return [address + i for i in range(count)]

    def handle_write_register(self, address, value):
        pass  # accept silently

async def main():
    async with modbus_rs.AsyncTcpServer("0.0.0.0", MyApp(), port=5020, unit_id=1) as srv:
        await srv.serve_forever()

asyncio.run(main())

Exception hierarchy

ModbusError
├── ModbusTimeout
├── ModbusConnectionError
├── ModbusProtocolError
│   └── ModbusDeviceException
└── ModbusConfigError

Building from source

Requires a Rust toolchain and Maturin (pip install maturin).

# install into an active venv (editable / dev mode)

cd mbus-ffi && maturin develop --features python-full


# build a release wheel

cd mbus-ffi && maturin build --release --features python-full

Running the Python test suite

pip install pytest pytest-asyncio

source .venv/bin/activate

pytest mbus-ffi/tests/python/ -v

Modbus TCP Gateway (python-gateway feature)

Enable the optional python-gateway feature to expose TcpGateway (sync) and AsyncTcpGateway (asyncio) classes that bridge an upstream Modbus TCP listener to one or more downstream Modbus TCP servers.

cd mbus-ffi && maturin develop --features python-full

Minimal sync example:

import modbus_rs

gw = modbus_rs.TcpGateway("0.0.0.0:5020")
ch = gw.add_tcp_downstream("192.168.1.10", 502)
gw.add_unit_route(unit=1, channel=ch)
gw.serve_forever()  # call gw.stop() from another thread to exit

Runnable demos: python/examples/python_gateway/sync_demo.py and python/examples/python_gateway/async_demo.py.

The event_handler=GatewayEventHandler() constructor argument can be passed to receive telemetry and lifecycle events (such as forwarding, responses, timeouts, routing misses, and disconnects) from the underlying async gateway server.


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:

  1. Web Run-times (WASM) via Javascript Promises.
  2. 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 (No Box, 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 detects no_std execution properties gracefully through build.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 cargo build -p mbus-ffi --features full

  • MBUS_MAX_TCP_CLIENTS: valid range 1..=255
  • MBUS_MAX_SERIAL_CLIENTS: valid range 1..=255

Build & Link

mbus-ffi supports compiling directly to shared (.so/.dylib) and static (.a) libraries:

cargo build --release -p mbus-ffi --features full

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 C Header Generation

We use cbindgen to emit the public C headers from the current Rust API surface.

Client header:

cbindgen --config mbus-ffi/cbindgen_client.toml --crate mbus-ffi --output target/mbus-ffi/include/modbus_rs_client.h

Server header:

cbindgen --config mbus-ffi/cbindgen_server.toml --crate mbus-ffi --output target/mbus-ffi/include/modbus_rs_server.h

The workspace also provides an xtask helper to generate the client header, build the release FFI library, and bundle them together:

# Default: bundles to target/mbus-ffi/

cargo run -p xtask -- gen-client-lib


# Custom SDK path (creates include/ and library/)

cargo run -p xtask -- gen-client-lib --out-dir /path/to/my_sdk

Header / Feature Compatibility

modbus_rs_client.h is generated from the Rust API shape of the enabled feature set. The generated header is intended for native C builds with:

--features full

For native server builds, modbus_rs_server.h is generated from the c-server API surface, while YAML-driven server apps additionally emit target/mbus-ffi/include/mbus_server_app.h from the device config.

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:

#include "modbus_rs_client.h"

// 1. Setup specific connection rules
struct MbusTcpConfig config = {0};
config.host = "192.168.1.10";
config.port = 502;
// ... (timeouts/retries)

// 2. Setup your OS networking functions
struct MbusTransportCallbacks transport = {0};
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 = {0};
app_callbacks.on_read_coils = my_app_read_coils;
// ...

MbusClientId client_id = mbus_tcp_client_new(&config, &transport, &app_callbacks);

// Request the connection internally
mbus_tcp_connect(client_id);
mbus_tcp_read_coils(client_id, 42 /* txn_id */, 1 /* unit_id */, 0 /* address */, 10 /* quantity */);

// Must be continuously ticked within your device's task loop
while(1) {
    mbus_tcp_poll(client_id);
}

For a full operational POSIX socket example and a self-contained serial PTY smoke path, view mbus-ffi/examples/c_client_demo/main.c.

C Smoke Example

Build the client smoke demo with xtask:

cargo run -p xtask -- build-c-demo --demo c_client_demo

This configures and builds the CMake target for mbus-ffi/examples/c_client_demo, then runs its registered CTest cases. The smoke binary also supports manual execution:

cd mbus-ffi/examples/c_client_demo/build

./c_smoke_test --serial-pty

./c_smoke_test --tcp 127.0.0.1 502

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.

Modbus TCP Gateway (c-gateway feature)

The c-gateway feature exposes a no_std C API that mirrors the client/server pool design: the application provides upstream and downstream transport callbacks (MbusTransportCallbacks) and mbus_pool_lock / mbus_pool_unlock plus mbus_gateway_lock(id) / mbus_gateway_unlock(id) routines, then drives the gateway with mbus_gateway_poll(id) from its event loop.

cargo build -p mbus-ffi --features c-gateway --no-default-features

A complete end-to-end CMake demo (POSIX sockets, in-process echo Modbus server, event logging) lives at examples/c_gateway_demo/main.c. Build and run it with:

cd mbus-ffi/examples/c_gateway_demo

mkdir build && cd build

CC=/usr/bin/clang cmake ..   # macOS: avoid Homebrew llvm

make

./c_gateway_demo

Running FFI Tests

The FFI layer has three distinct test paths:

  1. Rust-side unit tests inside mbus-ffi
  2. Native C binding-layer tests compiled from mbus-ffi/tests/c_api/test_binding_layer.c
  3. Higher-level native smoke tests under examples/c_client_demo

Rust-side FFI tests

Run the Rust unit tests and doc tests for mbus-ffi:

cargo test -p 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:

cargo build -p mbus-ffi --features full

Then compile the standalone C binding test source:

cc -I target/mbus-ffi/include mbus-ffi/tests/c_api/test_binding_layer.c -L target/debug -lmbus_ffi -o /tmp/test_binding_layer

Run it against the freshly built shared library:

macOS:

DYLD_LIBRARY_PATH="$PWD/target/debug" /tmp/test_binding_layer

Linux:

LD_LIBRARY_PATH="$PWD/target/debug" /tmp/test_binding_layer

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:

./mbus-ffi/tests/c_api/build/c_api_tests

Native C smoke test

Build or run the end-to-end client smoke demo with xtask:

cargo run -p xtask -- build-c-demo --demo c_client_demo

cargo run -p xtask -- run-c-demo --demo c_client_demo --mode serial-pty

This builds mbus-ffi with the demo's declared feature set, configures the CMake project in mbus-ffi/examples/c_client_demo, builds c_smoke_test, and can run the PTY-backed smoke path without external hardware.

C Server — Two Integration Approaches

There are two ways to build a Modbus TCP server with the C FFI.

Approach 1: Hand-Written Handlers (c_server_demo)

All FC callbacks are wired directly in main.c without any code generation. This is the simplest starting point when the register layout is small or fixed.

# Build and self-test

cargo run -p xtask -- build-c-demo --demo c_server_demo


# Static link

cargo run -p xtask -- build-c-demo --demo c_server_demo --static

Source: mbus-ffi/examples/c_server_demo/main.c. It creates a Modbus TCP server via mbus_tcp_server_new, registers FC01/FC05/FC03 callbacks, runs an in-process self-test with synthetic ADUs, and validates responses.

Approach 2: YAML-Driven Code Generation (c_server_demo_yaml)

The device register/coil map is declared in a YAML file. build.rs generates a type-safe Rust dispatcher into $OUT_DIR at compile time (never tracked in git), and gen-server-app regenerates the matching C header.

# Build, generate C header, compile, and self-test

MBUS_SERVER_APP_CONFIG=mbus-ffi/examples/c_server_demo_yaml/mbus_server_app.example.yaml \

	cargo run -p xtask -- build-c-demo --demo c_server_demo_yaml --static --features c-server

Or through xtask (sets the env var automatically from demo.yaml):

cargo run -p xtask -- build-c-demo --demo c_server_demo_yaml --static

Source: mbus-ffi/examples/c_server_demo_yaml/. Full walkthrough: examples/c_server_demo_yaml/README.md.

When MBUS_SERVER_APP_CONFIG is not set, build.rs falls back to the bundled examples/c_server_demo_yaml/mbus_server_app.example.yaml when building inside this workspace. That fallback exists to keep workspace builds and tests working; external mbus-ffi consumers should set MBUS_SERVER_APP_CONFIG explicitly.

Choosing an approach

Hand-written YAML-driven
Register map changes Edit C callbacks directly Edit YAML → rebuild auto-updates C header and Rust dispatcher
Generated files in git None None
Rust dispatcher source Written by hand Generated by build.rs into $OUT_DIR
Write-notification hooks Your C function, wired manually Declared in YAML, called by generated dispatcher
Best for Quick integration / small maps Production devices with many registers

.NET (C#) Bindings

The dotnet feature compiles the mbus_dn_* entry-point family — a flat, versioned C ABI specifically designed for .NET P/Invoke ([LibraryImport]). The managed code:

# Build the native cdylib with .NET support and all FCs

cargo build -p mbus-ffi --features dotnet-full

Output: target/debug/mbus_ffi.dll (Windows) / libmbus_ffi.so (Linux) / libmbus_ffi.dylib (macOS).

Native DLL search

[LibraryImport("mbus_ffi")] loads the native library from the application output directory, PATH, or the working directory. The example projects copy the DLL from target\debug\ (or target\release\) automatically via MSBuild. See DLL Deployment for the full copy rule and VS 2022 setup.

Quick start (C#)

using ModbusRs;

using var client = new ModbusTcpClient("192.168.1.10", 502);
await client.ConnectAsync();

ushort[] regs = await client.ReadHoldingRegistersAsync(unitId: 1, address: 0, quantity: 4);
bool[] coils  = await client.ReadCoilsAsync(unitId: 1, address: 0, quantity: 8);

await client.DisconnectAsync();

📖 Full .NET Binding Documentation →


Go Bindings

The go feature compiles libmbus_ffi into a native static archive or shared library, exposing Go APIs through a cgo module at github.com/Raghava-Ch/modbus-rs/mbus-ffi/go.

Build & Test

The bindings require cgo. Build the native library and run the Go test suite:

# Build the native static library for Go and run tests

./mbus-ffi/go/scripts/build_native.sh

cd mbus-ffi/go

go test ./...

Quick Start (Go)

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/Raghava-Ch/modbus-rs/mbus-ffi/go/client/tcp"
)

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	client, err := tcp.NewClient("192.168.1.10", 502)
	if err != nil {
		panic(err)
	}
	defer client.Close()

	if err := client.Connect(ctx); err != nil {
		panic(err)
	}

	regs, err := client.ReadHoldingRegisters(ctx, 1, 0, 10)
	if err != nil {
		panic(err)
	}
	fmt.Println(regs)
}

📖 Full Go Binding Documentation →


Node.js Bindings (modbus-rs on npm)

The nodejs feature compiles mbus-ffi into a Node.js native addon via napi-rs, exposing an idiomatic Promise-based JavaScript and TypeScript API. The package source lives in mbus-ffi/javascript/.

# Build the native addon with all Modbus features

cargo build -p mbus-ffi --features nodejs-full

Then build the npm package

cd mbus-ffi/javascript npm install npm run build


Minimum supported Node.js version: **20 LTS**. Prebuilt binaries are
published per-platform via the `optionalDependencies` mechanism so end
users do not need a Rust toolchain.

### Quick start (JavaScript)

```js
import { AsyncTcpTransport } from 'modbus-rs';

const transport = await AsyncTcpTransport.connect({
  host: '192.168.1.10',
  port: 502,
  timeoutMs: 2000,
});

const client = transport.createClient({ unitId: 1 });

const regs = await client.readHoldingRegisters({ address: 0, quantity: 4 });
await client.writeMultipleRegisters({ address: 10, values: [1, 2, 3, 4] });
await transport.close();

📖 Full JavaScript Binding Documentation →


Thread Safety

The C FFI layer is designed to be thread-safe, but it requires the host application to provide lock/unlock symbols that the Rust library resolves at link time.

For the client FFI, define:

// Required by the native client pool and client operations.
void mbus_pool_lock(void);
void mbus_pool_unlock(void);
void mbus_client_lock(MbusClientId id);
void mbus_client_unlock(MbusClientId id);

For the server FFI, define:

// Required by the native server pool and server operations.
void mbus_pool_lock(void);
void mbus_pool_unlock(void);
void mbus_server_lock(MbusServerId id);
void mbus_server_unlock(MbusServerId id);

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. Your application defines them and the Rust FFI resolves them at link time.

Symbol Called when
mbus_pool_lock / mbus_pool_unlock Any operation that allocates or frees a client or server slot
mbus_client_lock(id) / mbus_client_unlock(id) Any operation that reads or mutates a specific client
mbus_server_lock(id) / mbus_server_unlock(id) Any operation that polls, connects, disconnects, or mutates a specific server

Single-threaded — define them as no-ops:

void mbus_pool_lock(void)  {}
void mbus_pool_unlock(void) {}
void mbus_client_lock(MbusClientId id)   { (void)id; }
void mbus_client_unlock(MbusClientId id) { (void)id; }
void mbus_server_lock(MbusServerId id)   { (void)id; }
void mbus_server_unlock(MbusServerId id) { (void)id; }

Multi-threaded — back them with real mutexes:

static pthread_mutex_t g_pool_mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_mutex_t g_client_mutex[256] = { [0 ... 255] = PTHREAD_MUTEX_INITIALIZER };
static pthread_mutex_t g_server_mutex[256] = { [0 ... 255] = PTHREAD_MUTEX_INITIALIZER };

void mbus_pool_lock(void)   { pthread_mutex_lock(&g_pool_mutex); }
void mbus_pool_unlock(void) { pthread_mutex_unlock(&g_pool_mutex); }
void mbus_client_lock(MbusClientId id)   { pthread_mutex_lock(&g_client_mutex[id]); }
void mbus_client_unlock(MbusClientId id) { pthread_mutex_unlock(&g_client_mutex[id]); }
void mbus_server_lock(MbusServerId id)   { pthread_mutex_lock(&g_server_mutex[id & 0xFFu]); }
void mbus_server_unlock(MbusServerId id) { pthread_mutex_unlock(&g_server_mutex[id & 0xFFu]); }

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.

For the server FFI, hold mbus_server_lock(id) around mbus_tcp_server_poll, mbus_tcp_server_connect, mbus_tcp_server_disconnect, and the corresponding serial server calls. Use mbus_pool_lock / mbus_pool_unlock for server allocation and free paths.


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)
  • WasmTcpServer + WasmTcpGatewayConfig (WASM server binding over network transport)
  • WasmSerialServer + WasmSerialServerConfig (WASM server binding over serial transport)

All APIs are Promise-based and are designed specifically for browser runtimes (wasm32). Building native targets does not interact with Javascript wrappers.

WASM Server Transport Ownership Boundary

WASM transport implementations are owned by transport crates only:

  • mbus-network owns the WASM websocket transport implementation.
  • mbus-serial owns the WASM Web Serial transport implementation.

mbus-ffi server bindings orchestrate lifecycle and JS callback bridging; they do not reimplement transport I/O.

WASM Server Protocol Contract (Current vs Planned)

Current contractual surface (available now):

  • WasmTcpServer / WasmSerialServer lifecycle: start(), stop(), is_running()
  • JS callback bridge via dispatch_request(...) (direct value or Promise return)
  • Raw frame passthrough helpers: send_frame(...), recv_frame(...)
  • WebSocket readiness helpers (TCP server): transport_connecting(), transport_connected()
  • Transport ownership remains in mbus-network and mbus-serial

Not part of the current server binding contract (planned later):

  • Built-in Modbus function-code dispatcher/routing inside WasmTcpServer / WasmSerialServer
  • Guaranteed typed FC response helpers at the WASM server binding layer
  • Fully managed end-to-end server request loop owned by mbus-ffi

This separation is intentional for phase stability: current bindings provide lifecycle + bridge + transport plumbing, while higher-level protocol handling is integrated incrementally.

Build WASM Package

To build the distribution packages, navigate to the wasm directory and use the npm build script:

cd mbus-ffi/wasm

npm install

npm run build

This builds both the web and bundler targets under the dist/ folder.

Quick Start (Web Target - Direct Imports)

For direct browser usage without a bundler, import from dist/web/modbus-rs.js and await the initialization promise:

import init, { WasmTcpTransport } from "./wasm/dist/web/modbus-rs.js";

await init();

// Create the connection transport
const transport = new WasmTcpTransport("ws://127.0.0.1:8080", {
  responseTimeoutMs: 3000,
  retryAttempts: 1,
  tickIntervalMs: 20,
});

// Bind lightweight clients to different Unit IDs sharing the same WebSocket transport
const client1 = transport.createClient({ unitId: 1 });
const client2 = transport.createClient({ unitId: 2 });

const regs = await client1.read_holding_registers(0, 2);
console.log(Array.from(regs));

Quick Start (Bundlers - Vite, Svelte, React, etc.)

When using a bundler (Vite, Webpack, etc.), import directly from the package:

import { WasmTcpTransport } from "modbus-rs-wasm";

// Create the transport and clients
const transport = new WasmTcpTransport("ws://127.0.0.1:8080", { responseTimeoutMs: 3000 });
const client = transport.createClient({ unitId: 1 });

Quick Start (Web Serial)

import init, { request_serial_port, WasmSerialTransport } from "./wasm/dist/web/modbus-rs.js";

await init();

// Must be called from a user gesture (e.g. click handler)
const portHandle = await request_serial_port();

// Open one transport for the physical serial port
const transport = new WasmSerialTransport(portHandle, {
  mode: "rtu",            // "rtu" | "ascii"
  baudRate: 9600,
  dataBits: 8,
  stopBits: 1,
  parity: "even",
  responseTimeoutMs: 1000,
  retryAttempts: 3,
  tickIntervalMs: 20,
});

// Create multiple clients on the same port (e.g. RS-485 multi-drop)
const slave1 = transport.createClient({ unitId: 1 });
const slave2 = transport.createClient({ unitId: 2 });

const ok = await slave1.read_single_coil(0);

Quick Start (WASM Server Bindings)

import init, {
  WasmTcpServer,
  WasmTcpGatewayConfig,
  WasmSerialServer,
  WasmSerialServerConfig,
} from "./wasm/dist/web/modbus-rs.js";

await init();

const tcpServer = new WasmTcpServer(
  new WasmTcpGatewayConfig("ws://127.0.0.1:8080"),
  (req) => Promise.resolve({ ok: true, echo: req })
);
await tcpServer.start();

const serialServer = new WasmSerialServer(
  WasmSerialServerConfig.rtu(),
  (req) => Promise.resolve(req)
);
// Attach browser SerialPort object from navigator.serial.requestPort() before start.

Server Observability (Status + Last Error)

Use status_snapshot() for lightweight counters and lifecycle state, and last_error_message() / clear_last_error() to track and reset the latest binding-level failure.

const snap = tcpServer.status_snapshot();
console.log({
  transport: snap.transport(),
  running: snap.running(),
  connected: snap.transport_connected(),
  dispatched: snap.dispatched_requests(),
  sent: snap.sent_frames(),
  received: snap.received_frames(),
  hasError: snap.last_error_present(),
});

const lastErr = tcpServer.last_error_message();
if (lastErr !== undefined) {
  console.warn("server last error:", lastErr);
  tcpServer.clear_last_error();
}

Example Web Pages

Use the browser examples under mbus-ffi/wasm/examples:

  • wasm_client/network_smoke.html (WebSocket/TCP path client)
  • wasm_client/serial_smoke.html (Web Serial path client, full serial API smoke runner)
  • wasm_server/network_smoke.html (WASM TCP server lifecycle + dispatch + frame passthrough)
  • wasm_server/serial_smoke.html (WASM Serial server lifecycle + dispatch + frame passthrough)

Serve the examples over localhost:

cd mbus-ffi/wasm

python3 -m http.server 8089

Feature Gates and Supported Modbus Operations

mbus-ffi exposes internal Modbus stack features configured via Cargo feature flags. These are divided into Service (Function Code) Features, Transport Features, Language/Platform Binding Features, and Convenience Aliases.

1. Service Features (Function Codes)

These features enable compile-time support for specific Modbus function codes:

Feature Description Mapped Function Codes
coils Read/Write coils FC01 (Read Coils), FC05 (Write Single Coil), FC0F (Write Multiple Coils)
holding-registers Read/Write holding registers FC03 (Read Holding Registers), FC06 (Write Single Register), FC10 (Write Multiple Registers), FC16 (Mask Write), FC17 (Read/Write Multiple)
input-registers Read input registers FC04 (Read Input Registers)
registers Convenience helper Enables both holding-registers and input-registers
discrete-inputs Read discrete inputs FC02 (Read Discrete Inputs)
fifo Read FIFO queue FC18 (Read FIFO Queue)
file-record Read/Write file records FC14 (Read File Record), FC15 (Write File Record)
diagnostics Server diagnostics & IDs FC07 (Read Exception Status), FC08 (Diagnostics), FC0B (Comm Event Counter), FC0C (Comm Event Log), FC11 (Report Server ID), FC2B (Read Device ID)
traffic Traffic monitoring Enables traffic notification callbacks on async clients/servers

2. Transport Features

These features control compile-time transport support:

Feature Description
network-tcp Enables Modbus TCP network transport support
serial-rtu Enables Modbus RTU serial transport support
serial-ascii Enables Modbus ASCII serial transport support

3. Language & Platform Bindings

These features compile native binding interfaces and platform bindings:

Feature Description
wasm Enables browser WebAssembly bindings and transports (mbus-network/wasm, mbus-serial/wasm)
c-client Enables native C FFI client APIs and static client pool
c-server Enables native C FFI server APIs (YAML dispatch, custom handlers)
c-gateway Enables native C FFI gateway APIs
server-traffic Enables traffic callbacks specifically for the native C server
python Enables Python bindings via PyO3 and Maturin
python-gateway Enables Python bindings for TcpGateway / AsyncTcpGateway
go Enables Go cgo bindings
go-traffic Enables traffic notification callbacks for the Go server/client
nodejs Enables Node.js native addon bindings via napi-rs
nodejs-traffic Enables traffic callbacks for the Node.js server/client
dotnet Enables flat C ABI bindings specifically for .NET P/Invoke

4. Convenience Aliases

These bundle related features for simple compile commands:

Feature Alias Bundled Features
full c-client, c-server, c-gateway, coils, registers, discrete-inputs, fifo, file-record, diagnostics, traffic, error-trait, network-tcp, serial-rtu, serial-ascii
server-full c-server along with all service and transport features
python-full python, c-client, c-gateway, coils, registers, discrete-inputs, fifo, file-record, diagnostics, traffic, error-trait, network-tcp, serial-rtu, serial-ascii
go-full go, c-client, c-gateway, coils, registers, discrete-inputs, fifo, file-record, diagnostics, traffic, error-trait, network-tcp, serial-rtu, serial-ascii
nodejs-full nodejs, c-client, c-gateway, coils, registers, discrete-inputs, fifo, file-record, diagnostics, traffic, error-trait, network-tcp, serial-rtu, serial-ascii
dotnet-full dotnet, c-client, c-gateway, coils, registers, discrete-inputs, fifo, file-record, diagnostics, traffic, error-trait, network-tcp, serial-rtu, serial-ascii

WASM Test Coverage

WASM browser tests are in mbus-ffi/tests/wasm_e2e.rs and cover:

  • Promise behavior and typed payload mapping for client APIs.
  • WASM server lifecycle/dispatch surface (WasmTcpServer, WasmSerialServer).
  • Adapter passthrough to transport crates (mbus-network, mbus-serial).

Run wasm-target checks:

cargo check -p mbus-ffi --target wasm32-unknown-unknown --features wasm-client

cargo check -p mbus-ffi --target wasm32-unknown-unknown --features wasm-full

Run browser E2E tests:

bash mbus-ffi/scripts/run_wasm_browser_tests.sh

Prerequisites for browser E2E test runs:

  • wasm-pack installed and available on PATH
  • Chromium-based browser installed (google-chrome, chromium, or chromium-browser)
  • chromedriver installed on PATH with major version matching local browser

Notes:

  • The script enforces these prerequisites before executing tests.
  • Canonical command target is fixed to wasm-pack test --headless --chrome --features wasm-full --test wasm_e2e.

Licensing

Copyright (C) 2025 Raghava Challari

This project is 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. Commercial licenses are also available for proprietary use; contact ch.raghava44@gmail.com.

Contact

Name: Raghava Ch
Email: ch.raghava44@gmail.com
Repository: github.com/Raghava-Ch/modbus-rs