Eric Sosman said:
Aha! You don't need (or want) packed structs at all:
You want a way to pluck information from a plain vanilla
struct and write it in an externally-defined format.
...
You write one super-
function that accepts a void* struct pointer and a "struct
descriptor," usually an array of byte offsets and type codes:
struct struct_descriptor {
size_t offset;
enum { BYTE, BYTEPAIR, BYTEQUAD, ..., STOP } type; };
Thanks, Eric (and whoever the other guy is). Both your suggestions
are along the same lines, and, of course, already occurred to me.
And, as per the drawbacks you pointed out (plus being a pain in
the neck to maintain two structs per struct), already dismissed
by me.
I'd actually finished (what I think is) a more elegant solution,
that I called smemf() that's like memcpy() but under format
control, including additonal format specifiers for hex, for bits,
and for other stuff. The code actually works fine, but still
uncompleted is 723 lines (though that includes >>many<< comments),
which is somewhat of a tail-wagging-dog situation which I also
want to avoid.
The point of smemf is that a single format string replaces
that entire extra struct. So it's still "extra work for mother",
but much less in-your-face when reading the program that uses it.
Since it's not done (and the copyright not registered) yet,
I'm not releasing it, but (since I can't copyright the idea,
anyway) below is its (still somewhat incomplete) main comment block
describing its usage and functional specs, in case anyone else wants
to take a stab at implementation. Also, some stuff is done but not
documented, e.g., little/big endian flag to control which way %d works,
the bit-field specifier, etc. But you'll get the idea. And after that,
the additional details are obvious to anyone who thinks about it.
/* ==========================================================================
* Function: smemf ( unsigned char *mem, char *format, ... )
* Purpose: Construct a formatted block of memory, typically containing
* binary data, e.g., network packets, gif images, etc.
* Behaves much like (a subset of) sprintf, but the intent
* to accommodate binary data requires a few significant
* exceptions, as explained in the Notes section below.
* --------------------------------------------------------------------------
* Arguments: mem (O) (unsigned char *) to memory block
* to be formatted.
* format (I) (char *) to null-terminated string containing
* specifications for how the variable arg list
* following it should be formatted in mem.
* See Notes below.
* ... as many value args as needed to satisfy
* the format specification above
* --------------------------------------------------------------------------
* Returns: ( int ) # bytes in returned mem,
* 0 for any error.
* --------------------------------------------------------------------------
* Notes: o Like sprintf, mem must already be allocated by caller,
* and be large enough to accommodate the accompanying
* format specification argument.
* o format is sprintf-like, but with some significant exceptions
* and additions to facilitate smemf's different purpose.
* The one most significant similarity and difference is...
* * Like sprintf, conversion specifications are introduced by
* the % character and terminated by a conversion specifier
* (see list and discussions below).
* * But unlike sprintf, ordinary characters occurring
* outside conversion specifications aren't immediately
* copied into mem...
* * Instead, literals that you want formatted in mem
* must always be followed by corresponding conversion
* specifications.
* * For example, 123abc%s formats the next >>six bytes<<
* of mem with that >>ascii character<< string.
* But 123abc%x interprets that same string as
* >>hex digits<<, formatting the next >>three bytes<<
* of mem accordingly.
* * And that's why ordinary characters must be followed
* by a corresponding conversion specification, i.e.,
* because unlike sprintf, which always interprets ordinary
* characters as ascii, smemf formats binary memory blocks,
* too, and therefore needs a conversion specification
* to interpret the intended meaning of literals.
* * Additional notes:
* - When %s isn't preceded by a literal field,
* then smemf interprets the next argument from your
* argument list, in the usual way like sprintf.
* But 123abc%s uses no arguments from your argument list.
* - Field widths like 123abc%10s generate a 10-byte field,
* left-justified with your 123abc literal, and
* right-filled with four blanks. See the field width
* discussion below for additional information about
* optional right-justification, non-blank filler, etc.
* - Leading/trailing whitespace is ignored, so
* " 123abc %s " is the same as "123abc%s", but any
* embedded whitespace like "123 abc%s" is respected
* (although "123 abc%x" would still be an error,
* while " 123abc %x " becomes okay).
* - Leading/trailing (or pure) whitespace is obtained
* by surrounding the literal with its own quotes,
* e.g., format = " \" 123abc \" %s " includes one blank
* before and after 123abc.
* - On the very rare occasion when you want a literal
* quote character, escape it, i.e., smemf needs
* to see \" in your format string, so you'd need to
* write format = " 123\\\"abc %s " to actually format
* the string 123"abc in mem. That's confusing, but
* a straightforward application of the obvious rules.
* o The conversion specifiers recognized by smemf are
* s,S, x,X, d,D, all discussed in detail below.
* Note that x,X behave identically, as do d,D,
* but s,S have different behaviors, discussed in detail below.
* But first, some general remarks...
* * s,S,x,X are default left-justified, i.e., the first byte
* (or first hex digit for x,X) from your literal or argument
* goes into the next available byte (or hex digit) of mem.
* But %+etc (i.e., a + flag following %) right-justifies
* your literal or argument instead, e.g., 123abc%+10s
* generates a 10-byte field, left-filled with four blanks,
* then followed by your right-justified 123abc literal.
* And 123abc%+10x generates a five-byte field, left-filled
* with two leading 0 bytes, followed by three bytes
* containing your 12,3A,BC.
* o The s conversion specifier...
* *
* o The S conversion specifier...
* o The x,X conversion specifier...
* o The d,D conversion specifier...
* * A literal 123%d is taken as the decimal integer 123,
* or the argument for %d is taken as an int.
* * Justification flags following % are ignored.
* The bits comprising your int are "right-justified" in
* your specified field, i.e., low-order bit is rightmost.
* * A width %10d means 10 bits. But all fields are byte-sized,
* and 10-bits is "promoted" to 2-bytes, with your int value
* "right-justified" (explained above) in that 16-bit field.
* However, if your int value is greater than 1023=2^10-1,
* then your value is "truncated" to its low-order 10 bits,
* even though that 16-bit field could accommodate more bits.
* * If % width.precision d is given, precision is ignored.
* * If width is not given, it defaults to 16(bits) if your
* value is less than 65536, or to 32(bits) otherwise.
* * Example: 47%d generates two bytes containing 00,2F.
* And 47%17d generates three bytes 00,00,2F.
* ======================================================================= */