Skip to content
Open
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
12 changes: 10 additions & 2 deletions bakelite/generator/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,15 @@ def cli() -> None:
default=None,
help="Import path for runtime (no value=bakelite.proto, omit=bakelite_runtime)",
)
def gen(language: str, input_file: str, output_file: str, runtime_import: str | None) -> None:
@click.option(
"--unpacked",
is_flag=True,
default=False,
help="Generate aligned structs with memmove shuffle (for Cortex-M0, RISC-V, ESP32, PIC32)",
)
def gen(
language: str, input_file: str, output_file: str, runtime_import: str | None, unpacked: bool
) -> None:
"""Generate protocol code from a definition file."""
with open(input_file, encoding="utf-8") as f:
proto = f.read()
Expand All @@ -37,7 +45,7 @@ def gen(language: str, input_file: str, output_file: str, runtime_import: str |
import_path = runtime_import if runtime_import is not None else "bakelite_runtime"
generated_file = python.render(*proto_def, runtime_import=import_path)
elif language == "cpptiny":
generated_file = cpptiny.render(*proto_def)
generated_file = cpptiny.render(*proto_def, unpacked=unpacked)
else:
print(f"Unknown language: {language}")
sys.exit(1)
Expand Down
14 changes: 13 additions & 1 deletion bakelite/generator/cpptiny.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,19 @@ def render(
structs: list[ProtoStruct],
proto: Protocol | None,
comments: list[str],
*,
unpacked: bool = False,
) -> str:
"""Render a protocol definition to C++ source code."""
"""Render a protocol definition to C++ source code.

Args:
enums: Enum definitions from the protocol
structs: Struct definitions from the protocol
proto: Protocol definition (framing, CRC, message IDs)
comments: Top-level comments from the protocol file
unpacked: If True, generate aligned structs with memmove shuffle.
If False (default), generate packed structs for zero-copy.
"""
enums_types = {enum.name: enum for enum in enums}
structs_types = {struct.name: struct for struct in structs}

Expand Down Expand Up @@ -189,6 +200,7 @@ def _read_type(member: ProtoStructMember) -> str:
read_type=_read_type,
framer=framer,
message_ids=message_ids,
unpacked=unpacked,
)


Expand Down
96 changes: 58 additions & 38 deletions bakelite/generator/runtimes/cpptiny/cobs.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,60 +21,69 @@ class CobsFramer {
char *data;
};

// Single buffer access for zero-copy operations
// Returns pointer to message area (after COBS overhead, at type byte position)
char *buffer() {
return m_buffer + messageOffset();
}

size_t bufferSize() {
return BufferSize + 1; // +1 for type byte
}

// Legacy API for compatibility
char *readBuffer() {
return m_readBuffer;
return m_buffer + messageOffset();
}

size_t readBufferSize() {
return sizeof(m_readBuffer);
return bufferSize();
}

char *writeBuffer() {
return m_writePtr;
return m_buffer + messageOffset();
}

size_t writeBufferSize() {
return sizeof(m_writeBuffer) - overhead(BufferSize);
return bufferSize();
}

Result encodeFrame(const char *data, size_t length) {
assert(data);
assert(length <= BufferSize);

memcpy(m_writePtr, data, length);
char *msgStart = m_buffer + messageOffset();
memcpy(msgStart, data, length);
return encodeFrame(length);
}

Result encodeFrame(size_t length) {
assert(length <= BufferSize);
char *msgStart = m_buffer + messageOffset();

if(C::size() > 0) {
C crc;
crc.update(m_writePtr, length);
crc.update(msgStart, length);
auto crc_val = crc.value();
memcpy(m_writePtr + length, (void *)&crc_val, sizeof(crc_val));
memcpy(msgStart + length, (void *)&crc_val, sizeof(crc_val));
}

auto result = cobs_encode((void *)m_writeBuffer, sizeof(m_writeBuffer),
(void *)m_writePtr, length+C::size());
auto result = cobs_encode((void *)m_buffer, sizeof(m_buffer),
(void *)msgStart, length + C::size());
if(result.status != 0) {
return { 1, 0, nullptr };
}

m_writeBuffer[result.out_len] = 0;
m_buffer[result.out_len] = 0;

return { 0, result.out_len + 1, m_writeBuffer };
return { 0, result.out_len + 1, m_buffer };
}

DecodeResult readFrameByte(char byte) {
*m_readPos = byte;
size_t length = (m_readPos - m_readBuffer) + 1;
size_t length = (m_readPos - m_buffer) + 1;
if(byte == 0) {
m_readPos = m_readBuffer;
m_readPos = m_buffer;
return decodeFrame(length);
}
else if(length == sizeof(m_readBuffer)) {
m_readPos = m_readBuffer;
else if(length == sizeof(m_buffer)) {
m_readPos = m_buffer;
return { CobsDecodeState::BufferOverrun, 0, nullptr };
}

Expand All @@ -85,55 +94,66 @@ class CobsFramer {
private:
DecodeResult decodeFrame(size_t length) {
if(length == 1) {
return { CobsDecodeState::DecodeFailure, 0, nullptr };
return { CobsDecodeState::DecodeFailure, 0, nullptr };
}

length--; // Discard null byte
length--; // Discard null byte

auto result = cobs_decode((void *)m_readBuffer, sizeof(m_readBuffer), (void *)m_readBuffer, length);
// Decode in-place at buffer start
auto result = cobs_decode((void *)m_buffer, sizeof(m_buffer), (void *)m_buffer, length);
if(result.status != 0) {
return { CobsDecodeState::DecodeFailure, 0, nullptr };
}

// length of the decoded data without CRC
// Length of decoded data without CRC
length = result.out_len - C::size();

if(C::size() > 0) {
C crc;

// Get the CRC from the end of the frame
auto crc_val = crc.value();
memcpy(&crc_val, m_readBuffer + length, sizeof(crc_val));
memcpy(&crc_val, m_buffer + length, sizeof(crc_val));

crc.update(m_readBuffer, length);
crc.update(m_buffer, length);
if(crc_val != crc.value()) {
return { CobsDecodeState::CrcFailure, 0, nullptr };
}
}

return { CobsDecodeState::Decoded, length, m_readBuffer };
// Move decoded data to message offset position for consistent buffer layout
size_t offset = messageOffset();
if(offset > 0) {
memmove(m_buffer + offset, m_buffer, length);
}

return { CobsDecodeState::Decoded, length, m_buffer + offset };
}

constexpr static size_t cobsOverhead(size_t bufferSize) {
return (bufferSize + 253u)/254u;
return (bufferSize + 253u) / 254u;
}

constexpr static size_t messageOffset() {
return cobsOverhead(BufferSize + C::size());
}
constexpr static size_t overhead(size_t bufferSize) {
return cobsOverhead(BufferSize + C::size()) + C::size() + 1;

constexpr static size_t totalBufferSize() {
// COBS overhead + message + CRC + null terminator
return messageOffset() + BufferSize + C::size() + 1;
}

char m_readBuffer[BufferSize + overhead(BufferSize)];
char *m_readPos = m_readBuffer;
char m_writeBuffer[BufferSize + overhead(BufferSize)];
char *m_writePtr = m_writeBuffer + cobsOverhead(BufferSize);
char m_buffer[totalBufferSize()];
char *m_readPos = m_buffer;
};

/***************
* The below COBS function are Copyright (c) 2010 Craig McQueen
* And licensed under the MIT license, which can be found at the end of this file.
*
*
* Source: https://github.com/cmcqueen/cobs-c
* Commit: f4b812953e19bcece1a994d33f370652dba2bf1b
***************/
* Commit: f4b812953e19bcece1a994d33f370652dba2bf1b
***************/

#define COBS_ENCODE_DST_BUF_LEN_MAX(SRC_LEN) ((SRC_LEN) + (((SRC_LEN) + 253u)/254u))
#define COBS_DECODE_DST_BUF_LEN_MAX(SRC_LEN) (((SRC_LEN) == 0) ? 0u : ((SRC_LEN) - 1u))
Expand Down Expand Up @@ -350,4 +370,4 @@ static cobs_decode_result cobs_decode(void *dst_buf_ptr, size_t dst_buf_len,
result.out_len = dst_write_ptr - dst_buf_start_ptr;

return result;
}
}
80 changes: 67 additions & 13 deletions bakelite/generator/templates/cpptiny.h.j2
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@

#include "bakelite.h"

% if not unpacked
// Platform check for packed struct support (unaligned access required)
#if defined(__AVR__) || (defined(__ARM_ARCH) && __ARM_ARCH >= 7) || \
defined(__x86_64__) || defined(__i386__) || defined(_M_X64) || defined(_M_IX86)
#define BAKELITE_UNALIGNED_OK 1
#else
#define BAKELITE_UNALIGNED_OK 0
#endif

static_assert(BAKELITE_UNALIGNED_OK,
"This code requires unaligned memory access. Regenerate with --unpacked for "
"Cortex-M0, RISC-V, ESP32, PIC32, or other platforms without unaligned access support.");
% endif

% for enum in enums
% if enum.comment
// {{ enum.comment }}
Expand All @@ -22,7 +36,11 @@ enum class {{ enum.name }}: {{map_type(enum.type)}} {
% if struct.comment
// {{ struct.comment }}
% endif
% if unpacked
struct {{ struct.name }} {
% else
struct __attribute__((packed)) {{ struct.name }} {
% endif
% for member in struct.members:
% if member.comment
// {{ member.comment }}
Expand Down Expand Up @@ -84,7 +102,7 @@ public:
if(result.length == 0) {
return Message::NoMessage;
}

m_receivedMessage = (Message)result.data[0];
m_receivedFrameLength = result.length - 1;
return m_receivedMessage;
Expand All @@ -93,33 +111,69 @@ public:
return Message::NoMessage;
}

% for message in message_ids:
int send(const {{message[0]}} &val) {
Bakelite::BufferStream outStream((char *)m_framer.writeBuffer() + 1, m_framer.writeBufferSize() - 1);
m_framer.writeBuffer()[0] = (char)Message::{{message[0]}};
% if not unpacked
// Zero-copy message access - returns reference to message in buffer
template<typename T>
T& message() {
return *reinterpret_cast<T*>(m_framer.buffer() + 1);
}

template<typename T>
const T& message() const {
return *reinterpret_cast<const T*>(m_framer.buffer() + 1);
}

// Zero-copy send overloads for each message type
% for msg in message_ids:
int send(const {{msg[0]}}*) {
m_framer.buffer()[0] = static_cast<char>(Message::{{msg[0]}});
size_t frameSize = sizeof({{msg[0]}}) + 1;
auto result = m_framer.encodeFrame(frameSize);

if(result.status != 0) {
return result.status;
}

size_t ret = (*m_writeFn)(result.data, result.length);
return ret == result.length ? 0 : -1;
}
{{""}}
% endfor
// Zero-copy send helper - use as: send<MsgType>()
template<typename T>
int send() {
return send(static_cast<const T*>(nullptr));
}
% endif

// Copy-based send (works with variable-length fields, compatible with both modes)
% for msg in message_ids:
int send(const {{msg[0]}} &val) {
Bakelite::BufferStream outStream(m_framer.buffer() + 1, m_framer.bufferSize() - 1);
m_framer.buffer()[0] = static_cast<char>(Message::{{msg[0]}});
size_t startPos = outStream.pos();
val.pack(outStream);
// Input fame size is the difference in stream position, plus the message byte
size_t frameSize = ((outStream.pos() - startPos)) + 1;
size_t frameSize = (outStream.pos() - startPos) + 1;
auto result = m_framer.encodeFrame(frameSize);

if(result.status != 0) {
return result.status;
}
int ret = (*m_writeFn)((const char *)result.data, result.length);

size_t ret = (*m_writeFn)(result.data, result.length);
return ret == result.length ? 0 : -1;
}
{{""}}
% endfor

% for message in message_ids:
int decode({{message[0]}} &val, char *buffer = nullptr, size_t length = 0) {
if(m_receivedMessage != Message::{{message[0]}}) {
// Copy-based decode (works with variable-length fields, compatible with both modes)
% for msg in message_ids:
int decode({{msg[0]}} &val, char *buffer = nullptr, size_t length = 0) {
if(m_receivedMessage != Message::{{msg[0]}}) {
return -1;
}
Bakelite::BufferStream stream(
(char *)m_framer.readBuffer() + 1, m_receivedFrameLength,
m_framer.buffer() + 1, m_receivedFrameLength,
buffer, length
);
return val.unpack(stream);
Expand Down
8 changes: 4 additions & 4 deletions bakelite/tests/generator/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ ifeq ($(UNAME_S),Linux)
FLAGS = -static-libasan
endif
ifeq ($(UNAME_S),Darwin)
FLAGS = -static-libsan
FLAGS =
endif

ifdef CI
Expand All @@ -23,12 +23,12 @@ cpptiny: cpptiny-serialization.cpp cpptiny-framing.cpp cpptiny-protocol.cpp bake

.PHONY: struct.h
struct.h: struct.bakelite
poetry run bakelite gen -l cpptiny -i struct.bakelite -o struct.h
pixi run -- bakelite gen -l cpptiny -i struct.bakelite -o struct.h

.PHONY: proto.h
proto.h: proto.bakelite
poetry run bakelite gen -l cpptiny -i proto.bakelite -o proto.h
pixi run -- bakelite gen -l cpptiny -i proto.bakelite -o proto.h

.PHONY: bakelite.h
bakelite.h: ${INCLUDEPATH}/serializer.h ${INCLUDEPATH}/cobs.h ${INCLUDEPATH}/crc.h ${INCLUDEPATH}/declarations.h
poetry run bakelite runtime -l cpptiny -o bakelite.h
pixi run -- bakelite runtime -l cpptiny -o bakelite.h
3 changes: 2 additions & 1 deletion bakelite/tests/generator/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ def generates_cpptiny_code(expect):
expect(result.exit_code) == 0
with open(output_file) as f:
content = f.read()
expect("struct TestStruct" in content) == True
expect("TestStruct" in content) == True
expect("struct" in content) == True
finally:
os.unlink(output_file)

Expand Down
Loading
Loading