mbus-ffi 0.16.0

Native C FFI and browser WASM bindings for modbus-rs client APIs, with optional generated server bindings
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
# 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](https://pyo3.rs) and [Maturin](https://maturin.rs).
PyPI package: **`modbus-rs`** · Import name: `modbus_rs`

### Installation


```bash
pip install modbus-rs
```

### Quick Start — Synchronous TCP client


```python
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


```python
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)


```python
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


```python
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](https://maturin.rs) (`pip install maturin`).

```bash
# 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


```bash
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.

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

Minimal sync example:

```python
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`](python/examples/python_gateway/sync_demo.py)
and [`python/examples/python_gateway/async_demo.py`](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:
```bash
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:
```bash
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:
```bash
cbindgen --config mbus-ffi/cbindgen_client.toml --crate mbus-ffi --output target/mbus-ffi/include/modbus_rs_client.h
```

Server header:

```bash
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:

```bash
# 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:

```bash
--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`:

```c
#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:

```bash
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:

```bash
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.

```bash
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`](examples/c_gateway_demo/main.c).
Build and run it with:

```bash
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`:

```bash
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:

```bash
cargo build -p mbus-ffi --features full
```

Then compile the standalone C binding test source:

```bash
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:

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

Linux:

```bash
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:

```bash
./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:

```bash
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.

```bash
# 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.

```bash
# 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`):

```bash
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`](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:

```bash
# 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](../documentation/dotnet_bindings.md#native-dll-deployment)
for the full copy rule and VS 2022 setup.

### Quick start (C#)


```csharp
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 →](../documentation/dotnet_bindings.md)**

---

## 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:

```bash
# 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)


```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 →](../documentation/go_bindings.md)**

---

## Node.js Bindings (`modbus-rs` on npm)


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

```bash
# 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 →](../documentation/javascript_bindings.md)**

---

## 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:

```c
// 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:

```c
// 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:
```c
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:
```c
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:
```bash
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:

```javascript
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:

```javascript
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)

```javascript
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)


```javascript
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.

```javascript
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:
```bash
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:

```bash
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
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](../LICENSE) file or the [GPLv3 official site](https://www.gnu.org/licenses/gpl-3.0.en.html).

This crate is licensed under GPLv3. Commercial licenses are also available for proprietary use; contact [ch.raghava44@gmail.com](mailto:ch.raghava44@gmail.com).

## Contact


**Name:** Raghava Ch  
**Email:** [ch.raghava44@gmail.com](mailto:ch.raghava44@gmail.com)  
**Repository:** [github.com/Raghava-Ch/modbus-rs](https://github.com/Raghava-Ch/modbus-rs)