I like concrete examples...
so I decided to create my own implementation of message-passing plumbing.
It may be useful to others as a starting point or just to discuss.
The high-level parts (see below) are: message structures (like
struct example_msg), and their corresponding encoder functions (like
send_message_example_1()). Also included are preamble/final envelope helpers. The intent is to make the code compile/work in both sender and receiver environments (8 bit MCUs to 64 bit CPUs), rather than each having their own independent implementation linked only via documented wire-protocol.
#include "msg.h"
char *_msg_type_example = "EXAMPLE_TYPE";
struct example_msg {
int errcd;
char *errmsg;
float severity;
char *stack[10];
int bloblen;
char *blob;
int magic[5];
};
void send_message_example_1(int16_t sn, struct example_msg *s) {
send_message_preamble(sn, _msg_type_example, 1);
write_int16((int16_t)s->errcd);
write_asciiz(s->errmsg);
write_float(s->severity);
for (int j = 0; j < NELEMS(s->stack); j++)
write_asciiz(s->stack[j]);
write_int16((int16_t)s->bloblen);
write_blob(s->blob, s->bloblen);
for (int j = 0; j < NELEMS(s->magic); j++)
write_int16((int16_t)s->magic[j]);
send_message_final();
}
void send_message_preamble(int16_t sn, char *ty, int16_t vn) {
_isLittleEndian = isLittleEndian(); // write_xxx() and read_xxx() need this initialized
write_asciiz(_msg_label); // magic start-of-message label
crcInit(); // crc starts from here onward
write_int16(sn); // sequence number
write_asciiz(ty); // message type
write_int16(vn); // message version
}
void send_message_final() {
write_int16((int16_t)crcDone());
write_break();
}
struct example_msg ex_msg;
void send() {
ex_msg.errcd=0x1234; ex_msg.errmsg="my errmsg text here"; ex_msg.severity=-9.3;
ex_msg.stack[0] = "1st"; ex_msg.stack[1] = "2nd";
ex_msg.stack[2] = "3rd"; ex_msg.stack[3] = "4th";
ex_msg.stack[4] = "5th"; ex_msg.stack[5] = NULL;
ex_msg.stack[6] = "7th"; ex_msg.stack[7] = "8th";
ex_msg.stack[8] = "9th"; ex_msg.stack[9] = "10th";
ex_msg.blob = "this is a BLOB without trailing zero";
ex_msg.bloblen = strlen(ex_msg.blob);
ex_msg.magic[0] = 0xE301; ex_msg.magic[1] = 0xE302;
ex_msg.magic[2] = 0xE303; ex_msg.magic[3] = 0xE304;
ex_msg.magic[4] = 0xE305;
send_message_example_1(0x5678, &ex_msg);
}
Calling
send() above will produce the output below.
Notice that null-terminated strings (char *, asciiz) use the \0 as a field delimiter. Ints just occupy a fixed space (2 bytes) and are in big-endian format. One could change endian-ness or expand to 4, 8 bytes. I'm not sure if signed/unsigned would need to be accommodated; not too many ones-compliment machines around. write_float() just prints <whole>.<fraction> as asciiz. And opaque blobs (which may contain zeros) require a length integer preceeding it. Pointers to/arrays of sub-structs can be described in their own encoder/decoder functions.
./mmm s | od -A x -t x1z -v
000000 4d 59 4d 53 47 00 56 78 45 58 41 4d 50 4c 45 5f >MYMSG.VxEXAMPLE_<
000010 54 59 50 45 00 00 01 12 34 6d 79 20 65 72 72 6d >TYPE....4my errm<
000020 73 67 20 74 65 78 74 20 68 65 72 65 00 2d 39 2e >sg text here.-9.<
000030 33 30 30 30 30 30 00 31 73 74 00 32 6e 64 00 33 >300000.1st.2nd.3<
000040 72 64 00 34 74 68 00 35 74 68 00 00 37 74 68 00 >rd.4th.5th..7th.<
000050 38 74 68 00 39 74 68 00 31 30 74 68 00 00 24 74 >8th.9th.10th..$t<
000060 68 69 73 20 69 73 20 61 20 42 4c 4f 42 20 77 69 >his is a BLOB wi<
000070 74 68 6f 75 74 20 74 72 61 69 6c 69 6e 67 20 7a >thout trailing z<
000080 65 72 6f e3 01 e3 02 e3 03 e3 04 e3 05 00 57 >ero...........W<
The receiver writes all characters as they arrive to a circular buffer (enough space to accommodate the largest message). Once a
BREAK signal is detected, the current buffer is then processed. The receiver doesn't know what to expect, except that all messages have a envelope consisting of a magic label to denote start of message, message sequence number, type, version, some amount of fields, then a CRC. Assuming the last two bytes before the BREAK are CRC, the code can recalculate/validate the whole message before it even attempts to decode the envelope or payload fields. The assumption being that noise can add/change random characters.
The
recv_message() function (see below) is called after a
BREAK is received to determine which message was sent. It will read and validate the envelope, then call the appropriate decoder function (like
recv_message_example_1()) to re-hydrate the original structure.
int8_t recv_message(int16_t **snp, char **typ, int16_t **vnp, char **msgp) {
uint8_t e;
static int16_t sn; // allocate sequence number
// recv_message_preamble() will allocate for type
static int16_t vn; // allocate message version
*snp = &sn; // tell caller
// recv_message_preamble() will tell caller
*vnp = &vn; // tell caller
*msgp = NULL; // init in case of early exit
if ((e = recv_message_preamble(*snp, typ, *vnp)) < 0) return e;
if (*typ != NULL) {
if (strcmp(*typ, _msg_type_example) == 0 && vn == 1) {
static struct example_msg ex_msg;
if ((e = recv_message_example_1(&ex_msg)) < 0) return e;
*msgp = (char *)&ex_msg;
} // else if(strcmp(.....) == 0 && vn == ?) {
}
if ((e = recv_message_final()) < 0) return e;
return SUCCESS; // structure has been filled
}
int8_t recv_message_example_1(struct example_msg *s) {
char *p;
if ((p = read_int16()) == NULL) return EOBUF;
s->errcd = (int)(*((int16_t *)p));
static char errmsg[50];
if ((p = read_asciiz(errmsg, sizeof(errmsg))) == NULL) return EOBUF;
s->errmsg = p;
// TODO: finish up the rest
return SUCCESS; // structure has been filled
}
int8_t recv_message_preamble(int16_t *sn, char **ty, int16_t *vn) {
uint8_t e;
char *p;
*sn = 0; // init in case of early exit
*ty = NULL; // init in case of early exit
*vn = 0; // init in case of early exit
_isLittleEndian = isLittleEndian(); // write_xxx() and read_xxx() need this initialized
if (!find_asciiz(_msg_label)) return NOMSG; // no message leader found
// crcInit();
// if ((e = peek_validate_crc()) < 0) return e; // non-destructive read until end of buffer to check crc in last sizeof(CRCTYPE) chars
crcInit();
// sequence number
if ((p = read_int16()) == NULL) return EOBUF;
*sn = *((int16_t *)p);
// message type
static char msgty[10];
if ((p = read_asciiz(msgty, sizeof(msgty))) == NULL) return EOBUF;
*ty = p;
// message version
if ((p = read_int16()) == NULL) return EOBUF;
*vn = *((int16_t *)p);
return SUCCESS;
}
int8_t recv_message_final() {
char *p;
if ((p = read_int16()) == NULL) return EOBUF;
uint8_t sent_crc = (uint8_t)(*((int16_t *)p));
if (sent_crc != crcDone()) return BADCRC;
return SUCCESS;
}
I haven't finished the receiver part just yet; seg-fault somewhere + unfinished decoder code.
Yeah, I went a bit overboard with double indirction (type **) and static function vars as a way to not use malloc().