Skip to content
Advanced

ShrinkWrap in C

shrink_wrap/c/shrink_wrap.h and shrink_wrap/c/shrink_wrap.c are the ShrinkWrap reader and writer for C99: the same bytes as the Rust BufReader and BufWriter, for firmware that is not written in Rust. Copy the two files into the project, nothing else is needed: no malloc, nothing from libc beyond <stdint.h>, <stddef.h>, <stdbool.h>, <string.h> (memcpy, memset) and the freestanding <limits.h>.

This is the runtime only (SW-43). Code generated from an API (types, a device-side dispatcher, introspection) is planned on top of it (API-26), until then values are written and read field by field, in the order and with the calls the Rust code generated by #[derive_shrink_wrap] uses.

Writing

#include "shrink_wrap.h"

sw_octet buf[64];
sw_writer wr;
size_t len;

sw_writer_init(&wr, buf, sizeof buf / sizeof buf[0]);
sw_write_bool(&wr, true);
sw_write_un8(&wr, 3, 5);        /* 3 bits */
sw_write_u16(&wr, 420);         /* aligns to the byte, Little Endian */
sw_write_str(&wr, "hi", 2);     /* the length goes to the back of the buffer */
if (sw_writer_finish(&wr, &len) != SW_OK) {
    /* too small a buffer: the error of the first write that failed */
}
/* send buf[0..len]: D0 A4 01 68 69 02 */

sw_writer_finish() encodes the lengths collected at the back of the buffer (reversed UNib32, see wire format internals) right after the data and returns the size. An Unsized value inside another value (every struct or enum that isn't final_structure, self_describing or sized) is framed with sw_unsized_begin() / sw_unsized_end(), as the Rust BufWriter::write does:

sw_unsized u;
sw_unsized_begin(&wr, &u);
/* the fields of the value */
sw_unsized_end(&wr, &u);

Reading

sw_reader rd;
sw_reader_init(&rd, buf, len);
bool flag = sw_read_bool(&rd);
uint_least8_t three_bits = sw_read_un8(&rd, 3);
uint_least16_t num = sw_read_u16(&rd);
sw_bytes name = sw_read_str(&rd);   /* points into buf, name.len bytes, no NUL */
if (sw_reader_error(&rd) != SW_OK) {
    /* malformed or truncated: flag, num and name are 0 / empty from the failing read on */
}

An Unsized value is read through a reader split off for its size, so fields added by a newer version are skipped and missing ones read as out of bounds:

sw_reader v = sw_reader_split_unsized(&rd);
/* read the fields from v */
sw_reader_join(&rd, &v);            /* v's error, if any, is rd's now */

Errors

There are no Results: the first error is kept in the reader or writer, as the Rust _latch reads do (SW-41). A call that fails moves nothing, and while an error is kept no call moves, reads give 0 (an empty sw_bytes) and writes write nothing. So a run of calls needs one check at the end, and nothing read after an error can steer the code (a discriminant is 0, a flag false), but don't use a value as a loop bound before checking. sw_reader_take_error() / sw_writer_take_error() clear it, sw_writer_reset() starts over.

The numbers of sw_error are those of the Rust shrink_wrap::Error.

Differences to the Rust crate, none of them on the wire: every failing call keeps its error (in Rust only the _latch reads do), a failing call moves nothing (a Rust read can align or consume bits before it fails), and a writer stops at its first error (the Rust _latch writes go on).

Targets with a 16-bit char (TI C2000)

A buffer is an array of sw_octet (uint_least8_t) with one octet in the low 8 bits of each element, where CHAR_BIT is 16 too. Every access goes through two accessors that mask to 8 bits, values are kept in unsigned / uint32_t / uint64_t, and shifts don't rely on int being 32 bits. Data received packed two octets per word has to be unpacked into one octet per element first, and packed after writing. Also there:

  • float must be 32-bit IEEE 754, checked at compile time;
  • double must be 64 bits for sw_read_f64() / sw_write_f64(). Where it is 32 bits (C2000 with the old COFF ABI, AVR), define SW_NO_F64 and read the bits with sw_read_u64();
  • sw_write_str() takes the low 8 bits of each char.

Checked so far: builds without warnings with -std=c99 -pedantic -Wconversion -Wsign-conversion on the host, and for MSP430 (16-bit int and size_t), Cortex-M0 and RV32 with clang. Not tested on a C2000 compiler yet.

Requantize

sw_requantize.h and sw_requantize.c next to them convert a stored number to another scale and storage with a named rounding and overflow policy, the same number as Rust's exact_units::Requantizer and the typed Fixed::requantize, bit for bit (see Rounding and overflow). They don't need the reader and writer, and nothing from libc beyond <stdint.h>, <stdbool.h> and <string.h>.

The factor (the source scale over the target scale) is worked out on the host, as typed Rust does at compile time, and given as constants: an exact ratio num / den (two 64-bit halves each) or, for a factor that isn't a fraction of integers (π, a half exponent), the fixed point m * 2^shift of Magnitude::fixed_point(); f_num / f_den is what a float is multiplied with. shrink_wrap_c::requantize::CFactor::of(&magnitude) computes them and c_initializer() writes them as C. Millivolts in an int16_t to decivolts in an int8_t, rounded to the nearest with ties to even:

#include "sw_requantize.h"

static const sw_requantizer mv_to_dv = {
    {.ratio = true, .num_hi = 0, .num_lo = 1, .den_hi = 0, .den_lo = 100, .f_num = 1.0, .f_den = 100.0},
    {SW_NUM_INT, 8},
    SW_ROUND_NEAREST_EVEN,
    SW_OVERFLOW_CHECKED,
};

sw_num x = {SW_NUM_INT, {.i = 1250}}, dv;
if (sw_requantize(&mv_to_dv, x, &dv)) {
    /* dv.v.i == 12 */
}

Integers are converted exactly in 128 bits (sign and magnitude in two 64-bit halves, no __int128), whatever the storages: rounding is SW_ROUND_TRUNCATE, _FLOOR, _CEIL, _NEAREST_EVEN or _NEAREST_AWAY, overflow SW_OVERFLOW_CHECKED (false, no value), _SATURATE or _WRAP. A float storage (SW_NUM_F32, SW_NUM_F64) multiplies in doubles; with SW_NO_F64 only integers are there. The conversion decides the policy at run time, so for a hot path on a small MCU the typed Rust conversion (constants and policy fixed at compile time) is cheaper.

Tests

shrink_wrap/c is also a Rust crate (shrink_wrap_c, not published) that builds the C with the cc crate and checks it against the Rust one in just test: golden vectors for every primitive, the length layouts and finish(), the errors and the latch (tests/golden.rs), and a proptest cross-check of random sequences of writes and reads, in both directions, on truncated and corrupted input too (tests/cross.rs). sw_requantize() is cross-checked against exact_units::Requantizer on random factors, storages, policies and values and on every storage's edge values (tests/requantize.rs), and runs the requantize cases of the conformance corpus. just lint builds the C on its own with -std=c99 -Wall -Wextra -Werror -pedantic.

Not ported: TailSize, Series, fill_*, and the code size tricks of the Rust writer (finish_no_lens, _latch writes).