Simplified the Modbus slave code by replacing the large case statements—sprinkled throughout—with function pointer tables. Compared to computed jump tables, they may seem heavyweight, but they have four significant advantages:
They allow arbitrary case identifiers—no need to be sequential (or even numeric!),
They aren’t limited to 256 cases,
They greatly simplify the code, even reducing its size (the overhead is amortized quickly), and
They may be daisy-chained, allowing derivation and polymorphism.
The last one is particularly useful in the Modbus application, since it allows users of the library to override message dispatching. Each caller can define its own command-code-to-function mapping, but fall back to the default method to inherit built-in processing and error handling.
For example, here’s a snippet from the Ifos project showing how the function pointer table is defined. This one includes some functions to handle user-defined command codes (the first three entries), as well as standard Modbus messages. The last entry, with an id of -1, points to the default handler provided by the library. This action is a catch-all that will be executed if no other case is matched.
Modbus.dispatchMsg() does nothing more than load the command id from the received frame, then drops through to VTable.dispatch(), which searches for an associated function. In this case, the command id domain is sparse, so a computed jump table would require many dummy values. The other alternative, a series of comparisons, quickly becomes unwieldy:
As written, this approach requires 8-bit values (since they must fit in the working register), making it inappropriate for Modbus, which uses 16-bit subfunction ids. The code is repetitive, hard to read/maintain, and scales poorly—replacing the comparisons with 16-bit versions would bloat it even more—whereas iterating over a table of function pointers follows basic DRY principles. In fact, switch statements become totally data-driven; the same table look-up routine is used for every one.
Ifos, and the Modbus library itself, also chains handlers together, effectively providing for virtual functions. Since most Modbus command implementations are application-specific, the library only supports a few commands by default. Each application defines a table of function pointers to the command handlers it provides, possibly overriding handlers provided higher up and falling back to Modbus.builtin() for everything else. Modbus.dispatchMsg() may call Modbus.builtin(), which may call Diag.diagnostics(), which may call Diag.returnQuery(), all using the same mechanism:
The VTable.dispatch() method does a simple linear search to find the the correct function pointer. A lower-order algorithm would probably be offset by the overhead of implementing it over program memory (where the tables are stored). It would also require the table to be ordered, which isn’t desirable.
The method above depends on a pseudo-stack frame provided by the Util module, but it amounts to nothing more than 4 bytes of storage. It also requires a simple 16-bit comparison routine: