diff --git a/bakelite/generator/cli.py b/bakelite/generator/cli.py index 8fd6a78..5ee0cf4 100644 --- a/bakelite/generator/cli.py +++ b/bakelite/generator/cli.py @@ -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() @@ -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) diff --git a/bakelite/generator/cpptiny.py b/bakelite/generator/cpptiny.py index 5529fa3..945a981 100644 --- a/bakelite/generator/cpptiny.py +++ b/bakelite/generator/cpptiny.py @@ -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} @@ -189,6 +200,7 @@ def _read_type(member: ProtoStructMember) -> str: read_type=_read_type, framer=framer, message_ids=message_ids, + unpacked=unpacked, ) diff --git a/bakelite/generator/runtimes/cpptiny/cobs.h b/bakelite/generator/runtimes/cpptiny/cobs.h index 185e921..164541d 100644 --- a/bakelite/generator/runtimes/cpptiny/cobs.h +++ b/bakelite/generator/runtimes/cpptiny/cobs.h @@ -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 }; } @@ -85,17 +94,18 @@ 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) { @@ -103,37 +113,47 @@ class CobsFramer { // 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)) @@ -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; -} \ No newline at end of file +} diff --git a/bakelite/generator/templates/cpptiny.h.j2 b/bakelite/generator/templates/cpptiny.h.j2 index 365bbf4..24e48ed 100644 --- a/bakelite/generator/templates/cpptiny.h.j2 +++ b/bakelite/generator/templates/cpptiny.h.j2 @@ -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 }} @@ -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 }} @@ -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; @@ -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 + T& message() { + return *reinterpret_cast(m_framer.buffer() + 1); + } + + template + const T& message() const { + return *reinterpret_cast(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(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() + template + int send() { + return send(static_cast(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(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); diff --git a/bakelite/tests/generator/Makefile b/bakelite/tests/generator/Makefile index eb11de8..525d3be 100644 --- a/bakelite/tests/generator/Makefile +++ b/bakelite/tests/generator/Makefile @@ -6,7 +6,7 @@ ifeq ($(UNAME_S),Linux) FLAGS = -static-libasan endif ifeq ($(UNAME_S),Darwin) - FLAGS = -static-libsan + FLAGS = endif ifdef CI @@ -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 diff --git a/bakelite/tests/generator/test_cli.py b/bakelite/tests/generator/test_cli.py index 1c69f8c..6961b88 100644 --- a/bakelite/tests/generator/test_cli.py +++ b/bakelite/tests/generator/test_cli.py @@ -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) diff --git a/docs/cpptiny.md b/docs/cpptiny.md index c68275a..a331cd1 100644 --- a/docs/cpptiny.md +++ b/docs/cpptiny.md @@ -6,7 +6,8 @@ A compiler that can target the `C++14` standard is required. ## Features * Header only - * Small memory footprint + * Small memory footprint with single-buffer design + * Zero-copy message access (packed mode) * No heap memory allocation * Does not use the STL * Produces well-optimized assembly when compiler optimizations are enabled @@ -23,46 +24,61 @@ Use the generated code to implement a simple protocol. #include "proto.h" int main(int argc, char *arv[]) { - Serial port("/dev/ttyUSB0", 9600); // Magic, easy to use, portable serial port class.. :) + Serial port("/dev/ttyUSB0", 9600); - // Create an instance of our protocol. Use the Serial port for sending and receiving data. Protocol proto( []() { return port.read(); }, [](const char *data, size_t length) { return port.write(data, length); } ); - // Send a message - HelloMsg msg; + // Zero-copy send - write directly to the buffer + auto& msg = proto.message(); msg.code = 42; strcpy(msg.message, "Hello world!"); - proto.send(msg); + proto.send(); // Wait for a reply while(true) { - // Check and see if a new message has arrived Protocol::Message messageId = proto.poll(); switch(messageId) { - case Protocol::Message::NoMessage: // Nope, better luch next time + case Protocol::Message::NoMessage: break; - case Protocol::Message::ReplyMsg: // We received a reply! - //Decode message - ReplyMsg msg; - int ret = proto.decode(msg); - if(ret != 0) { - send_err("Decode Failed", ret); - return; - } - - cout << "Reply: " << msg.text << endl; + case Protocol::Message::ReplyMsg: + // Zero-copy receive - access message directly in buffer + const auto& reply = proto.message(); + cout << "Reply: " << reply.text << endl; + break; default: - send_err("Unkown message id:", messageId); break; + } } } ``` +## Platform Support +By default, Bakelite generates packed structs that enable zero-copy message access. +This requires platforms that support unaligned memory access. + +**Supported platforms (default packed mode):** +- x86/x64 (Intel, AMD) +- ARM Cortex-M3 and higher (ARMv7+) +- AVR (Arduino Uno, Nano, Mega) + +**Platforms requiring `--unpacked`:** +- ARM Cortex-M0/M0+ (no unaligned access) +- RISC-V +- ESP32 (Xtensa) +- PIC32 + +For platforms without unaligned access support, use the `--unpacked` flag: +```sh +$ bakelite gen -l cpptiny -i proto.bakelite -o proto.h --unpacked +``` + +A compile-time check will fail if packed code is used on an unsupported platform. + ## Runtime The code generated by Bakelite is broken into two parts. The runtime, generated with `bakelite runtime` @@ -136,15 +152,13 @@ cout << msg1.text << endl; ``` ### Memory Overhead -The read/write buffers account for the majority of the memory used by Bakelite. -Each buffer will use the `maxSize` bytes, plus the framing overhead. +Bakelite uses a single buffer for both sending and receiving, reducing memory usage by approximately 50% compared to dual-buffer implementations. -If we take an example protocol with a maxSize of 256 bytes, COBS framing, and CRC8, each buffer will use 261 bytes (256 data, 2 COBS overhead, 1 CRC, 1 message ID, and 1 null terminator). +For a protocol with a maxSize of 256 bytes, COBS framing, and CRC8, the buffer uses approximately 261 bytes (256 data + 2 COBS overhead + 1 CRC + 1 message ID + 1 null terminator). -The total size of the Protocol object on a 64bit AMD64 system would be 576 bytes (Two 261 byte buffers, and 54 bytes of additional overhead). -The same object compiled for an AVR system would use 552 bytes. +The total size of the Protocol object on a 64-bit system is approximately 290 bytes. On an AVR system, it's approximately 280 bytes. -If you are using a system where there isn't much RAM available, consider reducing your maxSize, and if needed, sending smaller messages. +If RAM is limited, reduce your maxSize or send smaller messages. ## API ### Type Mappings @@ -187,37 +201,55 @@ __arguments:__ * __write__ - A function with the signature int(const char *data, size_t length). When called, write length bytes to the output device. Return the number of bytes written. ##### poll() -> Protocol::MessageId -Call this function to wail for a message. -It will read any available data from the stream. +Reads available data and returns the message ID if a complete frame was received. __returns:__
-If a message is available, it's message ID will be returned. -If no message is available Protocol::MessageId::NoMessage is returned. +The message ID if available, or `Protocol::MessageId::NoMessage`. -##### decode(Struct &message, char *buffer = 0, size_t length = 0) -> int -Decodes a message and stores it in the message parameter. -If a message contains variable length value, then buffer and length need to be specified. -See [Memory Ownership](#memory_ownership) for more information. +##### message\() -> T& +Returns a reference to the message in the internal buffer. Use this for zero-copy access. + +**For receiving:** Call after `poll()` returns a message ID to access the received data. +**For sending:** Call before `send()` to write data directly to the buffer. + +```c++ +// Receiving +const auto& msg = proto.message(); + +// Sending +auto& msg = proto.message(); +msg.field = value; +proto.send(); +``` + +##### send\() -> int +Sends the message already written to the buffer via `message()`. Zero-copy. + +__returns:__
+0 on success. + +##### send(const Struct &message) -> int +Copy-based send. Serializes and sends a message. Works with variable-length fields. __arguments:__ * __message__ - Any struct with an assigned message-id. -* __buffer__ - A buffer used to write the contents of variable length fields. -* __length__ - Length of `buffer` in bytes. __returns:__
0 on success. -##### send(Struct message) -> int -Sends a message. The message is serialized, encoded as a frame, and sent to the stream. -You can pass any struct, as long as it was assigned a message ID in the protocol spec. +##### decode(Struct &message, char *buffer = 0, size_t length = 0) -> int +Copy-based decode. Use this for variable-length fields or when you need the data to persist. +See [Memory Ownership](#memory_ownership) for more information. __arguments:__ * __message__ - Any struct with an assigned message-id. +* __buffer__ - Buffer for variable-length field contents. +* __length__ - Length of `buffer` in bytes. __returns:__
-0 if successful. +0 on success. ### Struct A struct is generated for every struct defined in the protocol specification. diff --git a/examples/arduino/arduino.ino b/examples/arduino/arduino.ino index 780b4bc..3435769 100644 --- a/examples/arduino/arduino.ino +++ b/examples/arduino/arduino.ino @@ -6,7 +6,7 @@ Protocol proto( [](const char *data, size_t length) { return Serial.write(data, length); } ); -// keep track of how many responses we've set. +// keep track of how many responses we've sent. int numResponses = 0; void setup() { @@ -15,47 +15,51 @@ void setup() { // For boards that have a native USB port, wait for the serial device to be initialized. while(!Serial) {} - // Send a hello message, because, why not? - Ack ack; + // Send a hello message using zero-copy API + auto& ack = proto.message(); ack.code = 42; strcpy(ack.message, "Hello world!"); - proto.send(ack); + proto.send(); } // Send an error message to the PC for debugging. void send_err(const char *msg, uint8_t code) { - Ack ack; + auto& ack = proto.message(); ack.code = code; strcpy(ack.message, msg); - proto.send(ack); + proto.send(); } void loop() { // Check and see if a new message has arrived Protocol::Message messageId = proto.poll(); - + switch(messageId) { - case Protocol::Message::NoMessage: // Nope, better luch next time + case Protocol::Message::NoMessage: // Nope, better luck next time break; case Protocol::Message::TestMessage: // We received a test message! { - //Decode the test message - TestMessage msg; - int ret = proto.decode(msg); - if(ret != 0) { - send_err("Decode Failed", ret); - return; - } - + // Access the message directly in the buffer (zero-copy) + const auto& msg = proto.message(); + + // Copy data we need before preparing the response + // (response will overwrite the buffer) + uint8_t a = msg.a; + int32_t b = msg.b; + bool status = msg.status; + char msgText[16]; + strncpy(msgText, msg.message, sizeof(msgText)); + // Reply numResponses++; - - Ack ack; + + auto& ack = proto.message(); ack.code = numResponses; - snprintf(ack.message, sizeof(ack.message), "a=%d b=%d status=%s msg='%s'", (int)msg.a, (int)msg.b, msg.status ? "true" : "false", msg.message); - ret = proto.send(ack); + snprintf(ack.message, sizeof(ack.message), "a=%d b=%d status=%s msg='%s'", + (int)a, (int)b, status ? "true" : "false", msgText); + int ret = proto.send(); if(ret != 0) { send_err("Send failed", ret); return; @@ -63,7 +67,7 @@ void loop() { break; } default: // Just in case we get something unexpected... - send_err("Unkown message received!", (uint8_t)messageId); + send_err("Unknown message received!", (uint8_t)messageId); break; } } diff --git a/examples/arduino/bakelite.h b/examples/arduino/bakelite.h index 540b36f..b10ed68 100644 --- a/examples/arduino/bakelite.h +++ b/examples/arduino/bakelite.h @@ -545,60 +545,63 @@ 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); - 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 }; } @@ -609,17 +612,18 @@ 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) { @@ -627,37 +631,47 @@ class CobsFramer { // 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 overhead(size_t bufferSize) { - return cobsOverhead(BufferSize + C::size()) + C::size() + 1; + + constexpr static size_t messageOffset() { + return cobsOverhead(BufferSize + C::size()); } - char m_readBuffer[BufferSize + overhead(BufferSize)]; - char *m_readPos = m_readBuffer; - char m_writeBuffer[BufferSize + overhead(BufferSize)]; - char *m_writePtr = m_writeBuffer + cobsOverhead(BufferSize); + constexpr static size_t totalBufferSize() { + // COBS overhead + message + CRC + null terminator + return messageOffset() + BufferSize + C::size() + 1; + } + + 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)) @@ -875,6 +889,7 @@ static cobs_decode_result cobs_decode(void *dst_buf_ptr, size_t dst_buf_len, return result; } + } /* diff --git a/examples/arduino/proto.h b/examples/arduino/proto.h index 95f447d..e0c9c73 100644 --- a/examples/arduino/proto.h +++ b/examples/arduino/proto.h @@ -2,7 +2,18 @@ #include "bakelite.h" -struct TestMessage { +// 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."); +struct __attribute__((packed)) TestMessage { uint8_t a; int32_t b; bool status; @@ -47,7 +58,7 @@ struct TestMessage { -struct Ack { +struct __attribute__((packed)) Ack { uint8_t code; char message[64]; @@ -103,7 +114,7 @@ class ProtocolBase { if(result.length == 0) { return Message::NoMessage; } - + m_receivedMessage = (Message)result.data[0]; m_receivedFrameLength = result.length - 1; return m_receivedMessage; @@ -112,46 +123,89 @@ class ProtocolBase { return Message::NoMessage; } + // Zero-copy message access - returns reference to message in buffer + template + T& message() { + return *reinterpret_cast(m_framer.buffer() + 1); + } + + template + const T& message() const { + return *reinterpret_cast(m_framer.buffer() + 1); + } + + // Zero-copy send overloads for each message type + int send(const TestMessage*) { + m_framer.buffer()[0] = static_cast(Message::TestMessage); + size_t frameSize = sizeof(TestMessage) + 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; + } + + int send(const Ack*) { + m_framer.buffer()[0] = static_cast(Message::Ack); + size_t frameSize = sizeof(Ack) + 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; + } + + // Zero-copy send helper - use as: send() + template + int send() { + return send(static_cast(nullptr)); + } + // Copy-based send (works with variable-length fields, compatible with both modes) int send(const TestMessage &val) { - Bakelite::BufferStream outStream((char *)m_framer.writeBuffer() + 1, m_framer.writeBufferSize() - 1); - m_framer.writeBuffer()[0] = (char)Message::TestMessage; + Bakelite::BufferStream outStream(m_framer.buffer() + 1, m_framer.bufferSize() - 1); + m_framer.buffer()[0] = static_cast(Message::TestMessage); 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; } int send(const Ack &val) { - Bakelite::BufferStream outStream((char *)m_framer.writeBuffer() + 1, m_framer.writeBufferSize() - 1); - m_framer.writeBuffer()[0] = (char)Message::Ack; + Bakelite::BufferStream outStream(m_framer.buffer() + 1, m_framer.bufferSize() - 1); + m_framer.buffer()[0] = static_cast(Message::Ack); 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; } + // Copy-based decode (works with variable-length fields, compatible with both modes) int decode(TestMessage &val, char *buffer = nullptr, size_t length = 0) { if(m_receivedMessage != Message::TestMessage) { return -1; } Bakelite::BufferStream stream( - (char *)m_framer.readBuffer() + 1, m_receivedFrameLength, + m_framer.buffer() + 1, m_receivedFrameLength, buffer, length ); return val.unpack(stream); @@ -162,7 +216,7 @@ class ProtocolBase { return -1; } Bakelite::BufferStream stream( - (char *)m_framer.readBuffer() + 1, m_receivedFrameLength, + m_framer.buffer() + 1, m_receivedFrameLength, buffer, length ); return val.unpack(stream);