Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,8 @@ There are up to 4 files constructed:
- The H file with all the struct definitions (the type file). If both decoding and encoding files are generated for the same CDDL, they can share the same type file.
- An optional cmake file for building the generated code together with the zcbor C libraries.

When `zcbor code` is invoked with `--stream-encode-functions`, CodeRenderer also emits streaming encode entrypoints (`cbor_stream_encode_<type>`) and the associated provider structs used to supply repeated elements and chunked `tstr` values.

CodeRenderer conducts some pruning and deduplication of the list of types and functions received from CodeGenerator.


Expand Down
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,23 @@ The tests require [Zephyr](https://github.com/zephyrproject-rtos/zephyr) (if you

The generated C code is C++ compatible.

Streaming encode entrypoints
----------------------------

For encode-heavy, low-RAM paths, `zcbor code` can optionally generate streaming encode entrypoints:

- `cbor_stream_encode_<type>(zcbor_stream_write_fn stream_write, void *stream_user_data, const <type> *input, const struct cbor_stream_providers *prov, size_t *bytes_written_out)`

Enable them with `--stream-encode-functions`.

These entrypoints write CBOR via the supplied callback (no output buffer), and can stream:

- repeated fields via iterator callbacks in `struct cbor_stream_providers`
- `bstr` values via chunk callbacks (indefinite-length byte strings)
- `tstr` values via chunk callbacks (indefinite-length text strings)

When streaming entrypoints are enabled, generated encode code uses indefinite-length containers where needed.

Build system
------------

Expand Down Expand Up @@ -436,7 +453,8 @@ zcbor code --help

```
usage: zcbor code [-h] -c CDDL [--no-prelude] [-v]
[--default-max-qty DEFAULT_MAX_QTY] [--output-c OUTPUT_C]
[--default-max-qty DEFAULT_MAX_QTY] [--repeated-as-pointers]
[--stream-encode-functions] [--output-c OUTPUT_C]
[--output-h OUTPUT_H] [--output-h-types OUTPUT_H_TYPES]
[--copy-sources] [--output-cmake OUTPUT_CMAKE] -t
ENTRY_TYPES [ENTRY_TYPES ...] [-d] [-e] [--time-header]
Expand Down Expand Up @@ -477,6 +495,13 @@ options:
as sometimes the value is needed for internal
computations. If so, the script will raise an
exception.
--repeated-as-pointers
Represent repeated fields (max_qty > 1) as pointer +
count instead of embedding fixed-size arrays in the
generated types.
--stream-encode-functions
Also generate streaming encode entrypoints
(cbor_stream_encode_<type>) for each entry type.
--output-c OUTPUT_C, --oc OUTPUT_C
Path to output C file. If both --decode and --encode
are specified, _decode and _encode will be appended to
Expand Down
40 changes: 40 additions & 0 deletions include/zcbor_common.h
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,46 @@ struct zcbor_state_constant {
#ifdef ZCBOR_MAP_SMART_SEARCH
uint8_t *map_search_elem_state_end; /**< The end of the @ref map_search_elem_state buffer. */
#endif
int (*stream_write)(void *user_data, const uint8_t *data, size_t len);
void *stream_user_data;
size_t stream_bytes_written; /**< Total bytes written in streaming mode. */

const void *stream_providers;
};

/* Alignment helper for state storage. */
#if defined(__cplusplus) && (__cplusplus >= 201103L)
#define ZCBOR_ALIGNAS(type) alignas(type)
#define ZCBOR_ALIGNOF(type) alignof(type)
#define ZCBOR_STATIC_ASSERT(cond, msg) static_assert((cond), msg)
#elif defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 201112L)
#define ZCBOR_ALIGNAS(type) _Alignas(type)
#define ZCBOR_ALIGNOF(type) _Alignof(type)
#define ZCBOR_STATIC_ASSERT(cond, msg) _Static_assert((cond), msg)
#else
/* Best-effort fallback for older toolchains. */
#if defined(__GNUC__)
#define ZCBOR_ALIGNAS(type) __attribute__((aligned(__alignof__(type))))
#define ZCBOR_ALIGNOF(type) __alignof__(type)
#else
#define ZCBOR_ALIGNAS(type)
#define ZCBOR_ALIGNOF(type) 0
#endif
#define ZCBOR_STATIC_ASSERT(cond, msg) /* no static assert available */
#endif

/* We store struct zcbor_state_constant in state storage that is aligned as
* zcbor_state_t. Ensure that is sufficient for the constant state object.
*/
ZCBOR_STATIC_ASSERT((ZCBOR_ALIGNOF(zcbor_state_t) >= ZCBOR_ALIGNOF(struct zcbor_state_constant)),
"zcbor_state_t alignment must be >= zcbor_state_constant alignment.");

/** Number of zcbor_state_t slots required to store a struct zcbor_state_constant
* object at the end of the state array.
*/
#define ZCBOR_CONST_STATE_SLOTS \
((sizeof(struct zcbor_state_constant) + sizeof(zcbor_state_t) - 1) / sizeof(zcbor_state_t))

#ifdef ZCBOR_CANONICAL
#define ZCBOR_ENFORCE_CANONICAL_DEFAULT true
#else
Expand Down Expand Up @@ -285,6 +323,8 @@ do { \
#define ZCBOR_ERR_MAP_FLAGS_NOT_AVAILABLE 20
#define ZCBOR_ERR_INVALID_VALUE_ENCODING 21 ///! When ZCBOR_CANONICAL is defined, and the incoming data is not encoded with minimal length, or uses indefinite length array.
#define ZCBOR_ERR_CONSTANT_STATE_MISSING 22
#define ZCBOR_ERR_STREAM_WRITE_FAILED 23 ///! Streaming callback returned error or wrote fewer bytes than requested
#define ZCBOR_ERR_STREAM_READ_FAILED 24 ///! Streaming provider callback returned error or invalid data
#define ZCBOR_ERR_UNKNOWN 31

/** The largest possible elem_count. */
Expand Down
11 changes: 8 additions & 3 deletions include/zcbor_decode.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,15 @@ void zcbor_new_decode_state(zcbor_state_t *state_array, size_t n_states,
* including elements in nested unordered maps.
*/
#define ZCBOR_STATE_D(name, num_backups, payload, payload_size, elem_count, n_flags) \
zcbor_state_t name[((num_backups) + 2 + ZCBOR_FLAG_STATES(n_flags))]; \
ZCBOR_ALIGNAS(zcbor_state_t) uint8_t name##_storage[ \
(((num_backups) + 1 + ZCBOR_FLAG_STATES(n_flags) + ZCBOR_CONST_STATE_SLOTS) * sizeof(zcbor_state_t)) \
]; \
zcbor_state_t *name = (zcbor_state_t *)name##_storage; \
do { \
zcbor_new_decode_state(name, ZCBOR_ARRAY_SIZE(name), payload, payload_size, elem_count, \
(uint8_t *)&name[(num_backups) + 1], ZCBOR_FLAG_STATES(n_flags) * sizeof(zcbor_state_t)); \
zcbor_new_decode_state(name, ((num_backups) + 1 + ZCBOR_FLAG_STATES(n_flags) + ZCBOR_CONST_STATE_SLOTS), \
payload, payload_size, elem_count, \
(uint8_t *)&name[(num_backups) + 1], \
ZCBOR_FLAG_STATES(n_flags) * sizeof(zcbor_state_t)); \
} while(0)


Expand Down
89 changes: 87 additions & 2 deletions include/zcbor_encode.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,13 @@ void zcbor_new_encode_state(zcbor_state_t *state_array, size_t n_states,
* @param[in] elem_count The starting elem_count (typically 1).
*/
#define ZCBOR_STATE_E(name, num_backups, payload, payload_size, elem_count) \
zcbor_state_t name[((num_backups) + 2)]; \
ZCBOR_ALIGNAS(zcbor_state_t) uint8_t name##_storage[ \
(((num_backups) + 1 + ZCBOR_CONST_STATE_SLOTS) * sizeof(zcbor_state_t)) \
]; \
zcbor_state_t *name = (zcbor_state_t *)name##_storage; \
do { \
zcbor_new_encode_state(name, ZCBOR_ARRAY_SIZE(name), payload, payload_size, elem_count); \
zcbor_new_encode_state(name, ((num_backups) + 1 + ZCBOR_CONST_STATE_SLOTS), \
payload, payload_size, elem_count); \
} while(0)


Expand Down Expand Up @@ -188,6 +192,29 @@ bool zcbor_multi_encode_minmax(size_t min_encode, size_t max_encode,
const size_t *num_encode, zcbor_encoder_t encoder,
zcbor_state_t *state, const void *input, size_t input_len);

/**
* @brief Iterator callback for streaming encode of repeated fields.
*
* Return values:
* - 1: produced one element, stored at *elem_out
* - 0: done (normal end)
* - <0: error (negative errno-style)
*
* The pointer stored in *elem_out must remain valid until the next call to next()
* (or until encoding is finished), whichever comes first.
*/
typedef int (*zcbor_repeat_next_fn)(void *ctx, const void **elem_out);

/**
* @brief Encode a repeated field by iterating elements from a callback.
*
* This is intended for streaming encode where the caller does not want (or
* cannot afford) to materialize an array + count upfront.
*/
bool zcbor_multi_encode_iter_minmax(size_t min_encode, size_t max_encode,
zcbor_encoder_t encoder, zcbor_state_t *state,
zcbor_repeat_next_fn next, void *ctx);


/* Supplementary string (bstr/tstr) encoding functions: */

Expand Down Expand Up @@ -231,6 +258,64 @@ bool zcbor_bstr_start_encode(zcbor_state_t *state);
*/
bool zcbor_bstr_end_encode(zcbor_state_t *state, struct zcbor_string *result);

/**
* @brief Chunk iterator for streaming encode of indefinite-length text/byte strings.
*
* Return values:
* - 1: produced one chunk (*ptr, *len)
* - 0: done (normal end)
* - <0: error (negative errno-style)
*/
typedef int (*zcbor_next_chunk_fn)(void *ctx, const uint8_t **ptr, size_t *len);

/* Encode an indefinite-length tstr using a chunk iterator. */
bool zcbor_tstr_encode_indefinite_chunks(zcbor_state_t *state,
zcbor_next_chunk_fn next_chunk, void *ctx);
bool zcbor_bstr_encode_indefinite_chunks(zcbor_state_t *state,
zcbor_next_chunk_fn next_chunk, void *ctx);

/** Stream write callback type.
*
* @param user_data User-provided context (e.g., UART device pointer)
* @param data Pointer to data to write
* @param len Number of bytes to write
* @return Number of bytes written (must equal len for success), or negative on error
*/
typedef int (*zcbor_stream_write_fn)(void *user_data, const uint8_t *data, size_t len);

/** Initialize encoding state for streaming mode.
*
* Bytes are written directly via the callback instead of buffering.
* In streaming mode, arrays and maps use indefinite-length encoding
* (0x9F/0xBF + items + 0xFF) to avoid backtracking.
*
* @param state_array Array of states (must have at least 2 elements for constant_state)
* @param n_states Number of states in array
* @param stream_write Callback function to write bytes
* @param stream_user_data User data passed to callback (e.g., UART device pointer)
* @param elem_count Starting element count (typically 1)
*/
void zcbor_new_encode_state_streaming(zcbor_state_t *state_array, size_t n_states,
zcbor_stream_write_fn stream_write, void *stream_user_data, size_t elem_count);

size_t zcbor_stream_bytes_written(const zcbor_state_t *state);

int zcbor_stream_entry_function(void *input, zcbor_state_t *states, size_t n_states,
zcbor_encoder_t func, zcbor_stream_write_fn stream_write, void *stream_user_data,
size_t elem_count, size_t *bytes_written_out);

static inline void zcbor_set_stream_providers(zcbor_state_t *state, const void *providers)
{
if (state && state->constant_state) {
state->constant_state->stream_providers = providers;
}
}

static inline const void *zcbor_get_stream_providers(const zcbor_state_t *state)
{
return (state && state->constant_state) ? state->constant_state->stream_providers : NULL;
}

#ifdef __cplusplus
}
#endif
Expand Down
44 changes: 36 additions & 8 deletions src/zcbor_common.c
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@
_Static_assert((sizeof(size_t) == sizeof(void *)),
"This code needs size_t to be the same length as pointers.");

_Static_assert((sizeof(zcbor_state_t) >= sizeof(struct zcbor_state_constant)),
"This code needs zcbor_state_t to be at least as large as zcbor_backups_t.");

bool zcbor_new_backup(zcbor_state_t *state, size_t new_elem_count)
{
ZCBOR_CHECK_ERROR();
Expand Down Expand Up @@ -150,14 +147,41 @@ void zcbor_new_state(zcbor_state_t *state_array, size_t n_states,
#endif
state_array[0].constant_state = NULL;

if (n_states < 2) {
if (n_states < (1 + ZCBOR_CONST_STATE_SLOTS)) {
return;
}

/* Use the last state as a struct zcbor_state_constant object. */
state_array[0].constant_state = (struct zcbor_state_constant *)&state_array[n_states - 1];
/* Store the constant state object in the tail of the state array.
*
* The state array layout is:
* [0] : live state
* [1 .. backups] : backups
* [.. optional flags ..] : map smart search scratch (decode only, if flags points into the array)
* [tail] : struct zcbor_state_constant (may span multiple slots)
*/
const size_t const_state_slots = ZCBOR_CONST_STATE_SLOTS;
const size_t const_state_idx = n_states - const_state_slots;

state_array[0].constant_state = (struct zcbor_state_constant *)&state_array[const_state_idx];
state_array[0].constant_state->backup_list = NULL;
state_array[0].constant_state->num_backups = n_states - 2;

/* Backups live in state_array[1..num_backups]. If flags points inside the array,
* it indicates the start of the scratch region, and backups stop just before it.
*/
size_t backup_end_idx = const_state_idx - 1;
const uint8_t *const base = (const uint8_t *)state_array;
const uint8_t *const tail = (const uint8_t *)&state_array[const_state_idx];
if (flags) {
const uint8_t *const f = flags;
if ((f >= base) && (f < tail)) {
size_t flags_idx = (size_t)(f - base) / sizeof(zcbor_state_t);
if (flags_idx > 0) {
backup_end_idx = flags_idx - 1;
}
}
}

state_array[0].constant_state->num_backups = (backup_end_idx >= 1) ? backup_end_idx : 0;
state_array[0].constant_state->current_backup = 0;
state_array[0].constant_state->error = ZCBOR_SUCCESS;
#ifdef ZCBOR_STOP_ON_ERROR
Expand All @@ -168,7 +192,11 @@ void zcbor_new_state(zcbor_state_t *state_array, size_t n_states,
#ifdef ZCBOR_MAP_SMART_SEARCH
state_array[0].constant_state->map_search_elem_state_end = flags + flags_bytes;
#endif
if (n_states > 2) {
state_array[0].constant_state->stream_write = NULL;
state_array[0].constant_state->stream_user_data = NULL;
state_array[0].constant_state->stream_bytes_written = 0;
state_array[0].constant_state->stream_providers = NULL;
if (state_array[0].constant_state->num_backups > 0) {
state_array[0].constant_state->backup_list = &state_array[1];
}
}
Expand Down
Loading