From 4be7352ab3c077b0b4fbd4e04b0d6c91d795ef6a Mon Sep 17 00:00:00 2001 From: Emma Powers Date: Sat, 29 Nov 2025 14:50:08 -0500 Subject: [PATCH 1/6] Add Python runtime generation and improve template - Add 'bakelite runtime -l python' command to generate runtime files - Support --runtime-import flag for custom import paths - Rewrite Python template for black/ruff/mypy compliance - Use BLANK_LINE variable for clearer template formatting - Generated code uses temporary sys.path modification for imports - Update arduino example with new runtime generation --- bakelite/generator/cli.py | 58 ++++--- bakelite/generator/python.py | 72 +++++---- bakelite/generator/templates/python.py.j2 | 181 +++++++++++++++------- examples/arduino/Makefile | 2 +- examples/arduino/proto.py | 117 ++++++++++---- 5 files changed, 288 insertions(+), 142 deletions(-) diff --git a/bakelite/generator/cli.py b/bakelite/generator/cli.py index 0e521c7..e0e1bad 100644 --- a/bakelite/generator/cli.py +++ b/bakelite/generator/cli.py @@ -1,6 +1,7 @@ -# pylint: disable=redefined-builtin +"""Command-line interface for bakelite code generation.""" import sys +from pathlib import Path import click @@ -9,50 +10,65 @@ @click.group() def cli() -> None: - pass + """Bakelite protocol code generator.""" @cli.command() -@click.option("--language", "-l", required=True) -@click.option("--input", "-i", required=True) -@click.option("--output", "-o", required=True) -def gen(language: str, input: str, output: str) -> None: - with open(input, "r", encoding="utf-8") as f: +@click.option("--language", "-l", required=True, help="Target language (python, cpptiny)") +@click.option("--input", "-i", "input_file", required=True, help="Input protocol file") +@click.option("--output", "-o", "output_file", required=True, help="Output file") +@click.option( + "--runtime-import", + "runtime_import", + is_flag=False, + flag_value="bakelite.proto", + default=None, + help="Import path for runtime. No value=bakelite.proto, omit=runtime", +) +def gen(language: str, input_file: str, output_file: str, runtime_import: str | None) -> None: + """Generate protocol code from a definition file.""" + with open(input_file, encoding="utf-8") as f: proto = f.read() proto_def = parse(proto) if language == "python": - generated_file = python.render(*proto_def) + # Default to "bakelite_runtime" (relative import) if not specified + 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) else: print(f"Unknown language: {language}") sys.exit(1) - with open(output, "w", encoding="utf-8") as f: + with open(output_file, "w", encoding="utf-8") as f: f.write(generated_file) @cli.command() -@click.option("--language", "-l", required=True) -@click.option("--output", "-o", required=True) -def runtime(language: str, output: str) -> None: - runtime_func = None - +@click.option("--language", "-l", required=True, help="Target language (python, cpptiny)") +@click.option("--output", "-o", "output_path", default=".", help="Output directory") +@click.option("--name", default="bakelite_runtime", help="Runtime folder name (python only)") +def runtime(language: str, output_path: str, name: str) -> None: + """Generate runtime support code.""" if language == "cpptiny": - runtime_func = cpptiny.runtime + generated_file = cpptiny.runtime() + with open(output_path, "w", encoding="utf-8") as f: + f.write(generated_file) + elif language == "python": + runtime_dir = Path(output_path) / name + runtime_dir.mkdir(parents=True, exist_ok=True) + for filename, content in python.runtime().items(): + (runtime_dir / filename).write_text(content) + print(f"Generated Python runtime in {runtime_dir}") else: - print(f"Unkown language: {language}") + print(f"Unknown language: {language}") sys.exit(1) - generated_file = runtime_func() - - with open(output, "w", encoding="utf-8") as f: - f.write(generated_file) - def main() -> None: + """Main entry point.""" cli() diff --git a/bakelite/generator/python.py b/bakelite/generator/python.py index 7ba67b2..8194dfd 100644 --- a/bakelite/generator/python.py +++ b/bakelite/generator/python.py @@ -1,10 +1,21 @@ -from __future__ import annotations +"""Python code generator for bakelite protocols.""" + +from importlib import resources from jinja2 import Environment, PackageLoader -from .types import * +from .types import Protocol, ProtoEnum, ProtoStruct, ProtoStructMember from .util import to_camel_case +RUNTIME_FILES = [ + "__init__.py", + "types.py", + "serialization.py", + "runtime.py", + "framing.py", + "crc.py", +] + env = Environment( loader=PackageLoader("bakelite.generator", "templates"), trim_blocks=True, @@ -16,38 +27,32 @@ template = env.get_template("python.py.j2") +PRIMITIVE_TYPE_MAP = { + "bool": "bool", + "int8": "int", + "int16": "int", + "int32": "int", + "int64": "int", + "uint8": "int", + "uint16": "int", + "uint32": "int", + "uint64": "int", + "float32": "float", + "float64": "float", + "bytes": "bytes", + "string": "str", +} -def _map_type(member: ProtoStructMember) -> str: - prim_types = { - "bool": "bool", - "int8": "int", - "int16": "int", - "int32": "int", - "int64": "int", - "uint8": "int", - "uint16": "int", - "uint32": "int", - "uint64": "int", - "float32": "float", - "float64": "float", - "bytes": "bytes", - "string": "str", - } - if member.type.name in prim_types: - type_name = prim_types[member.type.name] - else: - type_name = member.type.name +def _map_type(member: ProtoStructMember) -> str: + """Map a proto type to a Python type annotation.""" + type_name = PRIMITIVE_TYPE_MAP.get(member.type.name, member.type.name) if member.array_size is not None: - return f"List[{type_name}]" + return f"list[{type_name}]" return type_name -def _to_desc(dclass: ProtoEnum | ProtoStruct) -> str: - return dclass.to_json() - - def render( enums: list[ProtoEnum], structs: list[ProtoStruct], @@ -55,14 +60,23 @@ def render( comments: list[str], runtime_import: str = "bakelite_runtime", ) -> str: - + """Render a protocol definition to Python source code.""" return template.render( enums=enums, structs=structs, proto=proto, comments=comments, map_type=_map_type, - to_desc=_to_desc, to_camel_case=to_camel_case, runtime_import=runtime_import, + BLANK_LINE="", ) + + +def runtime() -> dict[str, str]: + """Return the Python runtime files as a dict of filename -> content.""" + result: dict[str, str] = {} + for filename in RUNTIME_FILES: + content = resources.files("bakelite.proto").joinpath(filename).read_text() + result[filename] = content + return result diff --git a/bakelite/generator/templates/python.py.j2 b/bakelite/generator/templates/python.py.j2 index 999a82c..413e852 100644 --- a/bakelite/generator/templates/python.py.j2 +++ b/bakelite/generator/templates/python.py.j2 @@ -1,70 +1,135 @@ +"""Generated protocol definitions.""" +% if runtime_import == "bakelite_runtime" +{{ BLANK_LINE }} +import sys from dataclasses import dataclass +% if enums from enum import Enum -from bakelite.proto.serialization import struct, enum -from bakelite.proto.runtime import Registry, ProtocolBase -from typing import Any, List - -{{""}} -{{""}} - +% endif +from pathlib import Path +% if proto +from typing import Any +% endif +{{ BLANK_LINE }} +_runtime_path = str(Path(__file__).parent) +_added_to_path = _runtime_path not in sys.path +if _added_to_path: + sys.path.insert(0, _runtime_path) +try: + from bakelite_runtime.runtime import ProtocolBase, Registry + from bakelite_runtime.serialization import {{ "enum, " if enums }}struct + from bakelite_runtime.types import ( +% if enums + ProtoEnumDescriptor, +% endif +% if proto + ProtocolDescriptor, + ProtoMessageId, +% endif + ProtoStruct, + ProtoStructMember, + ProtoType, + ) +finally: + if _added_to_path: + sys.path.remove(_runtime_path) +% else +{{ BLANK_LINE }} +from dataclasses import dataclass +% if enums +from enum import Enum +% endif +% if proto +from typing import Any +% endif +{{ BLANK_LINE }} +from {{ runtime_import }}.runtime import ProtocolBase, Registry +from {{ runtime_import }}.serialization import {{ "enum, " if enums }}struct +from {{ runtime_import }}.types import ( +% if enums + ProtoEnumDescriptor, +% endif +% if proto + ProtocolDescriptor, + ProtoMessageId, +% endif + ProtoStruct, + ProtoStructMember, + ProtoType, +) +% endif % for comment in comments: #{{ comment }} % endfor - -{{""}} - +{{ BLANK_LINE }} registry = Registry() - -{{""}} -{{""}} - -% for enum in enums -% if enum.comment -# {{ enum.comment }} +% for e in enums +{{ BLANK_LINE }} +{{ BLANK_LINE }} +% if e.comment +# {{ e.comment }} % endif -@enum(registry, r'''{{to_desc(enum)}}''') -class {{ enum.name }}(Enum): - % for value in enum.values: - % if value.comment - #{{ value.comment }} - % endif - {{ value.name }} = {{ value.value }} - % endfor - {{""}} - {{""}} +@enum( + registry, + ProtoEnumDescriptor( + name="{{ e.name }}", + type=ProtoType(name="{{ e.type.name }}"{{ ', size=' + (e.type.size | string) if e.type.size is not none }}), + ), +) +class {{ e.name }}(Enum): + % for value in e.values: + % if value.comment + #{{ value.comment }} + % endif + {{ value.name }} = {{ value.value }} + % endfor % endfor - -% for struct in structs -% if struct.comment -# {{ struct.comment }} +% for s in structs +{{ BLANK_LINE }} +{{ BLANK_LINE }} +% if s.comment +# {{ s.comment }} % endif -@struct(registry, r'''{{to_desc(struct)}}''') +@struct( + registry, + ProtoStruct( + name="{{ s.name }}", + members=( + % for member in s.members: + ProtoStructMember( + type=ProtoType(name="{{ member.type.name }}"{{ ', size=' + (member.type.size | string) if member.type.size is not none }}), + name="{{ member.name }}",{{ ' array_size=' + (member.array_size | string) + ',' if member.array_size is not none }} + ), + % endfor + ), + ), +) @dataclass -class {{ struct.name }}: - % for member in struct.members: - % if member.comment - #{{ member.comment }} - % endif - {{ member.name }}: {{map_type(member)}}{{ ' = ' + member.value if member.value }} - % endfor - {{""}} - {{""}} +class {{ s.name }}: + % for member in s.members: + % if member.comment + #{{ member.comment }} + % endif + {{ member.name }}: {{map_type(member)}}{{ ' = ' + member.value if member.value }} + % endfor % endfor - -{{""}} -{{""}} - -%if proto - +% if proto +{{ BLANK_LINE }} +{{ BLANK_LINE }} class Protocol(ProtocolBase): - def __init__(self, **kwargs: Any) -> None: - super().__init__( - % for option in proto.options: - {{option.name}}="{{option.value}}", - % endfor - registry=registry, - desc=r'''{{to_desc(proto)}}''', - **kwargs - ) - -%endif \ No newline at end of file + def __init__(self, **kwargs: Any) -> None: + super().__init__( + % for option in proto.options: + {{option.name}}="{{option.value}}", + % endfor + registry=registry, + desc=ProtocolDescriptor( + message_ids=( + % for msg in proto.message_ids: + ProtoMessageId(name="{{ msg.name }}", number={{ msg.number }}), + % endfor + ), + ), + **kwargs, + ) +% endif diff --git a/examples/arduino/Makefile b/examples/arduino/Makefile index 6cb01bf..a856573 100644 --- a/examples/arduino/Makefile +++ b/examples/arduino/Makefile @@ -3,7 +3,7 @@ gen: proto.h proto.py proto.h: proto.bakelite bakelite.h bakelite gen -l cpptiny -i proto.bakelite -o proto.h -proto.py: proto.bakelite +proto.py: proto.bakelite bakelite_runtime bakelite gen -l python -i proto.bakelite -o proto.py bakelite.h: diff --git a/examples/arduino/proto.py b/examples/arduino/proto.py index 2a2aeb8..085091e 100644 --- a/examples/arduino/proto.py +++ b/examples/arduino/proto.py @@ -1,46 +1,97 @@ -from dataclasses import dataclass -from enum import Enum -from bakelite.proto.serialization import struct, enum -from bakelite.proto.runtime import Registry, ProtocolBase -from typing import Any, List - - - +"""Generated protocol definitions.""" +import sys +from dataclasses import dataclass +from pathlib import Path +from typing import Any +_runtime_path = str(Path(__file__).parent) +_added_to_path = _runtime_path not in sys.path +if _added_to_path: + sys.path.insert(0, _runtime_path) +try: + from bakelite_runtime.runtime import ProtocolBase, Registry + from bakelite_runtime.serialization import struct + from bakelite_runtime.types import ( + ProtocolDescriptor, + ProtoMessageId, + ProtoStruct, + ProtoStructMember, + ProtoType, + ) +finally: + if _added_to_path: + sys.path.remove(_runtime_path) registry = Registry() - - -@struct(registry, r'''{"members": [{"type": {"name": "uint8", "size": null}, "name": "a", "value": null, "comment": null, "annotations": [], "arraySize": null}, {"type": {"name": "int32", "size": null}, "name": "b", "value": null, "comment": null, "annotations": [], "arraySize": null}, {"type": {"name": "bool", "size": null}, "name": "status", "value": null, "comment": null, "annotations": [], "arraySize": null}, {"type": {"name": "string", "size": 16}, "name": "message", "value": null, "comment": null, "annotations": [], "arraySize": null}], "name": "TestMessage", "comment": null, "annotations": []}''') +@struct( + registry, + ProtoStruct( + name="TestMessage", + members=( + ProtoStructMember( + type=ProtoType(name="uint8", size=0), + name="a", + ), + ProtoStructMember( + type=ProtoType(name="int32", size=0), + name="b", + ), + ProtoStructMember( + type=ProtoType(name="bool", size=0), + name="status", + ), + ProtoStructMember( + type=ProtoType(name="string", size=16), + name="message", + ), + ), + ), +) @dataclass class TestMessage: - a: int - b: int - status: bool - message: str - - -@struct(registry, r'''{"members": [{"type": {"name": "uint8", "size": null}, "name": "code", "value": null, "comment": null, "annotations": [], "arraySize": null}, {"type": {"name": "string", "size": 64}, "name": "message", "value": null, "comment": null, "annotations": [], "arraySize": null}], "name": "Ack", "comment": null, "annotations": []}''') + a: int + b: int + status: bool + message: str + + +@struct( + registry, + ProtoStruct( + name="Ack", + members=( + ProtoStructMember( + type=ProtoType(name="uint8", size=0), + name="code", + ), + ProtoStructMember( + type=ProtoType(name="string", size=64), + name="message", + ), + ), + ), +) @dataclass class Ack: - code: int - message: str - - - + code: int + message: str class Protocol(ProtocolBase): - def __init__(self, **kwargs: Any) -> None: - super().__init__( - maxLength="70", - framing="COBS", - crc="CRC8", - registry=registry, - desc=r'''{"options": [{"name": "maxLength", "value": "70", "comment": null, "annotations": []}, {"name": "framing", "value": "COBS", "comment": null, "annotations": []}, {"name": "crc", "value": "CRC8", "comment": null, "annotations": []}], "message_ids": [{"name": "TestMessage", "number": 1, "comment": null, "annotations": []}, {"name": "Ack", "number": 2, "comment": null, "annotations": []}], "comment": null, "annotations": []}''', - **kwargs - ) - + def __init__(self, **kwargs: Any) -> None: + super().__init__( + maxLength="70", + framing="COBS", + crc="CRC8", + registry=registry, + desc=ProtocolDescriptor( + message_ids=( + ProtoMessageId(name="TestMessage", number=1), + ProtoMessageId(name="Ack", number=2), + ), + ), + **kwargs, + ) From b1da7a3bc7fe77fd185702465c76ff76ff06ee54 Mon Sep 17 00:00:00 2001 From: Emma Powers Date: Sat, 29 Nov 2025 14:50:31 -0500 Subject: [PATCH 2/6] Add async support to protocol runtime - Add async_= parameter to send(), poll(), and messages() methods - Use @overload decorators for proper type hints - Support both BufferedIOBase (sync) and StreamReader/StreamWriter (async) - Add messages() as the primary iterator interface for consuming streams --- bakelite/proto/runtime.py | 215 ++++++++++++++++++++++++++++++++------ 1 file changed, 185 insertions(+), 30 deletions(-) diff --git a/bakelite/proto/runtime.py b/bakelite/proto/runtime.py index b5e83c1..197dfdc 100644 --- a/bakelite/proto/runtime.py +++ b/bakelite/proto/runtime.py @@ -1,68 +1,107 @@ +"""Runtime support for bakelite protocol communication.""" + import struct +from asyncio import StreamReader, StreamWriter +from collections.abc import AsyncIterator, Coroutine, Iterator from dataclasses import is_dataclass from enum import Enum from io import BufferedIOBase, BytesIO -from typing import Any, Dict, Optional, Union +from typing import Any, Literal, overload -from ..generator.types import Protocol from .framing import CrcSize, Framer +from .types import ProtocolDescriptor class ProtocolError(RuntimeError): - pass + """Raised when protocol operations fail.""" class Registry: + """Registry of protocol types (structs and enums).""" + def __init__(self) -> None: - self.types: Dict[str, Any] = {} + self.types: dict[str, Any] = {} def register(self, name: str, cls: Any) -> None: + """Register a type by name.""" self.types[name] = cls def get(self, name: str) -> Any: + """Get a type by name.""" return self.types[name] def is_enum(self, name: str) -> bool: + """Check if a registered type is an enum.""" return issubclass(self.types[name], Enum) def is_struct(self, name: str) -> bool: + """Check if a registered type is a struct (dataclass).""" return is_dataclass(self.types[name]) class ProtocolBase: - _stream: BufferedIOBase + """Base class for generated protocol classes. + + Supports both synchronous and asynchronous I/O. For sync operations, + pass a BufferedIOBase stream. For async operations, pass a tuple of + (StreamReader, StreamWriter). + + Example (sync): + proto = Protocol(stream=serial_port) + proto.send(MyMessage(data=42)) + for msg in proto.messages(): + handle(msg) + + Example (async): + reader, writer = await asyncio.open_connection(host, port) + proto = Protocol(stream=(reader, writer)) + await proto.send(MyMessage(data=42), async_=True) + async for msg in proto.messages(async_=True): + await handle(msg) + """ + + _stream: BufferedIOBase | None + _async_reader: StreamReader | None + _async_writer: StreamWriter | None _registry: Registry - _desc: Protocol - _options: Dict[str, Any] + _desc: ProtocolDescriptor + _options: dict[str, Any] def __init__( self, *, - stream: BufferedIOBase, + stream: BufferedIOBase | tuple[StreamReader, StreamWriter], registry: Registry, - desc: Union[str, bytes, bytearray], + desc: ProtocolDescriptor, crc: str = "CRC8", - framer: Optional[Framer] = None, + framer: Framer | None = None, **kwargs: Any, ) -> None: - self._stream = stream + if isinstance(stream, tuple): + self._stream = None + self._async_reader, self._async_writer = stream + else: + self._stream = stream + self._async_reader = None + self._async_writer = None + self._registry = registry - self._desc = Protocol.from_json(desc) + self._desc = desc self._options = kwargs - self._ids = {id.number: id.name for id in self._desc.message_ids} - self._messages = {id.name: id.number for id in self._desc.message_ids} + self._ids = {mid.number: mid.name for mid in self._desc.message_ids} + self._messages = {mid.name: mid.number for mid in self._desc.message_ids} crc_size = CrcSize.NO_CRC - crc = crc.lower() + crc_lower = crc.lower() - if crc == "none": + if crc_lower == "none": crc_size = CrcSize.NO_CRC - elif crc == "crc8": + elif crc_lower == "crc8": crc_size = CrcSize.CRC8 - elif crc == "crc16": + elif crc_lower == "crc16": crc_size = CrcSize.CRC16 - elif crc == "crc32": + elif crc_lower == "crc32": crc_size = CrcSize.CRC32 else: raise RuntimeError(f"Unknown CRC type {crc}") @@ -72,7 +111,8 @@ def __init__( else: self._framer = framer - def send(self, message: Any) -> None: + def _encode_message(self, message: Any) -> bytes: + """Encode a message to bytes (shared by sync and async).""" if not getattr(message, "_desc", None): raise ProtocolError(f"{type(message)} is not a message type") @@ -84,23 +124,138 @@ def send(self, message: Any) -> None: stream = BytesIO() stream.write(struct.pack("=B", msg_id)) message.pack(stream) - frame = self._framer.encode_frame(stream.getvalue()) + return self._framer.encode_frame(stream.getvalue()) + + def _decode_frame(self, frame: bytes) -> Any: + """Decode a frame to a message (shared by sync and async).""" + msg_id = frame[0] + msg = frame[1:] + + if msg_id in self._ids: + message_type = self._registry.get(self._ids[msg_id]) + return message_type.unpack(BytesIO(msg)) + raise ProtocolError(f"Received unknown message id {msg_id}") + @overload + def send(self, message: Any, *, async_: Literal[False] = False) -> None: ... + + @overload + def send(self, message: Any, *, async_: Literal[True]) -> Coroutine[Any, Any, None]: ... + + def send(self, message: Any, *, async_: bool = False) -> None | Coroutine[Any, Any, None]: + """Send a message over the protocol stream. + + Args: + message: The message to send. + async_: If True, returns a coroutine for async sending. + + Returns: + None for sync, or a coroutine for async. + """ + if async_: + return self._send_async(message) + + if self._stream is None: + raise ProtocolError("Sync send requires a BufferedIOBase stream") + frame = self._encode_message(message) self._stream.write(frame) + return None + + async def _send_async(self, message: Any) -> None: + """Async implementation of send.""" + if self._async_writer is None: + raise ProtocolError("Async send requires a StreamWriter") + frame = self._encode_message(message) + self._async_writer.write(frame) + await self._async_writer.drain() + + @overload + def poll(self, *, async_: Literal[False] = False) -> Any | None: ... + + @overload + def poll(self, *, async_: Literal[True]) -> Coroutine[Any, Any, Any | None]: ... - def poll(self) -> Any: + def poll(self, *, async_: bool = False) -> Any | None | Coroutine[Any, Any, Any | None]: + """Poll for incoming messages. + + Args: + async_: If True, returns a coroutine for async polling. + + Returns: + A message if one is available, None otherwise. + For async, returns a coroutine. + """ + if async_: + return self._poll_async() + + if self._stream is None: + raise ProtocolError("Sync poll requires a BufferedIOBase stream") data = self._stream.read() - self._framer.append_buffer(data) + if data: + self._framer.append_buffer(data) frame = self._framer.decode_frame() - if frame: - msg_id = frame[0] - msg = frame[1:] + return self._decode_frame(frame) + return None - if msg_id in self._ids: - message_type = self._registry.get(self._ids[msg_id]) - return message_type.unpack(BytesIO(msg)) - raise ProtocolError(f"Received unknown message id {msg_id}") + async def _poll_async(self) -> Any | None: + """Async implementation of poll.""" + if self._async_reader is None: + raise ProtocolError("Async poll requires a StreamReader") + # Read available data (non-blocking read of up to 4096 bytes) + try: + data = await self._async_reader.read(4096) + if data: + self._framer.append_buffer(data) + except Exception: + pass + + frame = self._framer.decode_frame() + if frame: + return self._decode_frame(frame) return None + + @overload + def messages(self, *, async_: Literal[False] = False) -> Iterator[Any]: ... + + @overload + def messages(self, *, async_: Literal[True]) -> AsyncIterator[Any]: ... + + def messages(self, *, async_: bool = False) -> Iterator[Any] | AsyncIterator[Any]: + """Iterate over incoming messages. + + This is the primary interface for consuming protocol streams. + + Args: + async_: If True, returns an async iterator. + + Returns: + An iterator (sync) or async iterator yielding messages. + + Example (sync): + for msg in protocol.messages(): + handle(msg) + + Example (async): + async for msg in protocol.messages(async_=True): + await handle(msg) + """ + if async_: + return self._messages_async() + return self._messages_sync() + + def _messages_sync(self) -> Iterator[Any]: + """Sync iterator over messages.""" + while True: + msg = self.poll() + if msg is not None: + yield msg + + async def _messages_async(self) -> AsyncIterator[Any]: + """Async iterator over messages.""" + while True: + msg = await self.poll(async_=True) + if msg is not None: + yield msg From afb3f51e9a32c2b78d7c07a1fcccd398ae31d968 Mon Sep 17 00:00:00 2001 From: Emma Powers Date: Sat, 29 Nov 2025 18:47:58 -0500 Subject: [PATCH 3/6] Optimize Python codegen with batched struct.pack/unpack - Replace decorator-based API with dataclass base classes (Struct, BakeliteEnum) - Generate inline pack/unpack code instead of runtime interpretation - Batch consecutive primitive fields into single struct.pack/unpack calls - Simplify ProtocolBase to use class attributes for message mapping --- bakelite/generator/python.py | 372 ++++++++++++++++++- bakelite/generator/templates/python.py.j2 | 135 ++++--- bakelite/proto/runtime.py | 112 ++---- bakelite/proto/serialization.py | 394 +++++---------------- bakelite/tests/proto/test_serialization.py | 205 ++++------- examples/arduino/bakelite.h | 4 + examples/arduino/proto.py | 140 ++++---- 7 files changed, 694 insertions(+), 668 deletions(-) diff --git a/bakelite/generator/python.py b/bakelite/generator/python.py index 8194dfd..9bc7826 100644 --- a/bakelite/generator/python.py +++ b/bakelite/generator/python.py @@ -4,7 +4,7 @@ from jinja2 import Environment, PackageLoader -from .types import Protocol, ProtoEnum, ProtoStruct, ProtoStructMember +from .types import Protocol, ProtoEnum, ProtoStruct, ProtoStructMember, ProtoType from .util import to_camel_case RUNTIME_FILES = [ @@ -27,6 +27,7 @@ template = env.get_template("python.py.j2") +# Map bakelite types to Python type annotations PRIMITIVE_TYPE_MAP = { "bool": "bool", "int8": "int", @@ -43,6 +44,38 @@ "string": "str", } +# Map bakelite types to struct format characters +FORMAT_CHARS = { + "bool": "?", + "int8": "b", + "uint8": "B", + "int16": "h", + "uint16": "H", + "int32": "i", + "uint32": "I", + "int64": "q", + "uint64": "Q", + "float32": "f", + "float64": "d", +} + +# Size in bytes for each type +TYPE_SIZES = { + "bool": 1, + "int8": 1, + "uint8": 1, + "int16": 2, + "uint16": 2, + "int32": 4, + "uint32": 4, + "int64": 8, + "uint64": 8, + "float32": 4, + "float64": 8, +} + +PRIMITIVES = frozenset(PRIMITIVE_TYPE_MAP.keys()) + def _map_type(member: ProtoStructMember) -> str: """Map a proto type to a Python type annotation.""" @@ -53,6 +86,334 @@ def _map_type(member: ProtoStructMember) -> str: return type_name +def _is_primitive(t: ProtoType) -> bool: + """Check if a type is a primitive type.""" + return t.name in PRIMITIVES + + +def _is_numeric_primitive(t: ProtoType) -> bool: + """Check if a type is a numeric primitive (can use struct.pack).""" + return t.name in FORMAT_CHARS + + +def _format_char(t: ProtoType) -> str: + """Get the struct format character for a type.""" + return FORMAT_CHARS.get(t.name, "") + + +def _type_size(t: ProtoType) -> int: + """Get the size in bytes of a type.""" + return TYPE_SIZES.get(t.name, 0) + + +def _gen_pack_primitive(member: ProtoStructMember, indent: str = "") -> str: + """Generate pack code for a primitive field.""" + t = member.type + name = member.name + + if t.name in FORMAT_CHARS: + fmt = FORMAT_CHARS[t.name] + return f'{indent}_buf.extend(_struct.pack("={fmt}", self.{name}))' + + if t.name == "bytes": + if t.size is None or t.size == 0: + # Variable length bytes + return ( + f"{indent}if len(self.{name}) > 255:\n" + f'{indent} raise SerializationError("{name} exceeds 255 bytes")\n' + f'{indent}_buf.extend(_struct.pack("=B", len(self.{name})))\n' + f"{indent}_buf.extend(self.{name})" + ) + # Fixed length bytes + return ( + f"{indent}if len(self.{name}) > {t.size}:\n" + f'{indent} raise SerializationError("{name} exceeds {t.size} bytes")\n' + f"{indent}_buf.extend(self.{name})\n" + f'{indent}_buf.extend(b"\\x00" * ({t.size} - len(self.{name})))' + ) + + if t.name == "string": + if t.size is None or t.size == 0: + # Variable length string (null terminated) + return ( + f'{indent}_enc_{name} = self.{name}.encode("ascii")\n' + f"{indent}_buf.extend(_enc_{name})\n" + f"{indent}_buf.append(0)" + ) + # Fixed length string + return ( + f'{indent}_enc_{name} = self.{name}.encode("ascii")\n' + f"{indent}if len(_enc_{name}) >= {t.size}:\n" + f'{indent} raise SerializationError("{name} exceeds {t.size - 1} chars")\n' + f"{indent}_buf.extend(_enc_{name})\n" + f'{indent}_buf.extend(b"\\x00" * ({t.size} - len(_enc_{name})))' + ) + + raise ValueError(f"Unknown primitive type: {t.name}") + + +def _gen_unpack_primitive(member: ProtoStructMember, indent: str = "") -> str: + """Generate unpack code for a primitive field.""" + t = member.type + name = member.name + + if t.name in FORMAT_CHARS: + fmt = FORMAT_CHARS[t.name] + size = TYPE_SIZES[t.name] + return ( + f'{indent}{name} = _struct.unpack_from("={fmt}", _data, _o)[0]\n' + f"{indent}_o += {size}" + ) + + if t.name == "bytes": + if t.size is None or t.size == 0: + # Variable length bytes + return ( + f'{indent}_len_{name} = _struct.unpack_from("=B", _data, _o)[0]\n' + f"{indent}_o += 1\n" + f"{indent}{name} = bytes(_data[_o:_o + _len_{name}])\n" + f"{indent}_o += _len_{name}" + ) + # Fixed length bytes + return f"{indent}{name} = bytes(_data[_o:_o + {t.size}])\n" f"{indent}_o += {t.size}" + + if t.name == "string": + if t.size is None or t.size == 0: + # Variable length string (null terminated) + return ( + f'{indent}_end_{name} = _data.find(b"\\x00", _o)\n' + f"{indent}if _end_{name} < 0:\n" + f'{indent} raise SerializationError("unterminated string {name}")\n' + f'{indent}{name} = bytes(_data[_o:_end_{name}]).decode("ascii")\n' + f"{indent}_o = _end_{name} + 1" + ) + # Fixed length string + return ( + f"{indent}_raw_{name} = _data[_o:_o + {t.size}]\n" + f'{indent}_null_{name} = _raw_{name}.find(b"\\x00")\n' + f'{indent}{name} = bytes(_raw_{name}[:_null_{name} if _null_{name} >= 0 else {t.size}]).decode("ascii")\n' + f"{indent}_o += {t.size}" + ) + + raise ValueError(f"Unknown primitive type: {t.name}") + + +def _gen_pack_field( + member: ProtoStructMember, enums: list[ProtoEnum], structs: list[ProtoStruct] +) -> str: + """Generate pack code for a struct field.""" + t = member.type + name = member.name + lines: list[str] = [] + + # Check if it's an array + if member.array_size is not None: + if member.array_size == 0: + # Variable length array - write length prefix + lines.append(f"if len(self.{name}) > 255:") + lines.append(f' raise SerializationError("{name} array exceeds 255 elements")') + lines.append(f'_buf.extend(_struct.pack("=B", len(self.{name})))') + + # Check if it's a primitive array that can be batched + if t.name in FORMAT_CHARS: + fmt = FORMAT_CHARS[t.name] + if member.array_size == 0: + # Variable length - use f-string format + lines.append( + f'_buf.extend(_struct.pack(f"={{len(self.{name})}}{fmt}", *self.{name}))' + ) + else: + # Fixed length - use literal format + lines.append(f"if len(self.{name}) != {member.array_size}:") + lines.append( + f' raise SerializationError("{name} must have {member.array_size} elements")' + ) + lines.append( + f'_buf.extend(_struct.pack("={member.array_size}{fmt}", *self.{name}))' + ) + else: + # Non-primitive array - loop + lines.append(f"for _item in self.{name}:") + if _is_primitive(t): + inner = _gen_pack_primitive( + ProtoStructMember( + type=t, + name="_item", + value=None, + comment=None, + annotations=[], + array_size=None, + ), + indent="", + ) + # Replace self._item with _item + inner = inner.replace("self._item", "_item") + # Indent each line properly + for line in inner.split("\n"): + lines.append(" " + line) + elif _is_enum(t, enums): + lines.append(" _buf.extend(_item.pack())") + else: + lines.append(" _buf.extend(_item.pack())") + return "\n".join(lines) + + # Single value + if _is_primitive(t): + return _gen_pack_primitive(member) + if _is_enum(t, enums): + return f"_buf.extend(self.{name}.pack())" + # Nested struct + return f"_buf.extend(self.{name}.pack())" + + +def _gen_unpack_field( + member: ProtoStructMember, enums: list[ProtoEnum], structs: list[ProtoStruct] +) -> str: + """Generate unpack code for a struct field.""" + t = member.type + name = member.name + lines: list[str] = [] + + # Check if it's an array + if member.array_size is not None: + if member.array_size == 0: + # Variable length array - read length prefix + lines.append(f'_len_{name} = _struct.unpack_from("=B", _data, _o)[0]') + lines.append("_o += 1") + + # Check if it's a primitive array that can be batched + if t.name in FORMAT_CHARS: + fmt = FORMAT_CHARS[t.name] + size = TYPE_SIZES[t.name] + if member.array_size == 0: + # Variable length + lines.append( + f'{name} = list(_struct.unpack_from(f"={{_len_{name}}}{fmt}", _data, _o))' + ) + lines.append(f"_o += _len_{name} * {size}") + else: + # Fixed length + lines.append( + f'{name} = list(_struct.unpack_from("={member.array_size}{fmt}", _data, _o))' + ) + lines.append(f"_o += {member.array_size * size}") + else: + # Non-primitive array - loop + arr_len = f"_len_{name}" if member.array_size == 0 else str(member.array_size) + lines.append(f"{name} = []") + lines.append(f"for _ in range({arr_len}):") + if _is_primitive(t): + # For primitive non-numeric types (bytes/string in arrays is unusual) + inner = _gen_unpack_primitive( + ProtoStructMember( + type=t, + name="_item", + value=None, + comment=None, + annotations=[], + array_size=None, + ), + indent="", + ) + # Indent each line properly + for line in inner.split("\n"): + lines.append(" " + line) + lines.append(f" {name}.append(_item)") + elif _is_enum(t, enums): + lines.append(f" _item, _n = {t.name}.unpack(_data, _o)") + lines.append(" _o += _n") + lines.append(f" {name}.append(_item)") + else: + lines.append(f" _item, _n = {t.name}.unpack(_data, _o)") + lines.append(" _o += _n") + lines.append(f" {name}.append(_item)") + return "\n".join(lines) + + # Single value + if _is_primitive(t): + return _gen_unpack_primitive(member) + if _is_enum(t, enums): + return f"{name}, _n = {t.name}.unpack(_data, _o)\n_o += _n" + # Nested struct + return f"{name}, _n = {t.name}.unpack(_data, _o)\n_o += _n" + + +def _is_enum(t: ProtoType, enums: list[ProtoEnum]) -> bool: + """Check if a type is an enum.""" + return any(e.name == t.name for e in enums) + + +def _is_struct(t: ProtoType, structs: list[ProtoStruct]) -> bool: + """Check if a type is a struct.""" + return any(s.name == t.name for s in structs) + + +def _can_batch(member: ProtoStructMember, enums: list[ProtoEnum]) -> bool: + """Check if member can be batched with other primitives.""" + if member.array_size is not None: + return False + if member.type.name in ("bytes", "string"): + return False + if _is_enum(member.type, enums): + return False + return member.type.name in FORMAT_CHARS + + +def _batch_members( + members: list[ProtoStructMember], enums: list[ProtoEnum] +) -> list[tuple[str, list[ProtoStructMember]]]: + """Group members into batches for pack/unpack optimization. + + Returns list of (batch_type, members) where batch_type is "primitive" or "single". + """ + batches: list[tuple[str, list[ProtoStructMember]]] = [] + current: list[ProtoStructMember] = [] + + for member in members: + if _can_batch(member, enums): + current.append(member) + else: + if current: + batches.append(("primitive", current)) + current = [] + batches.append(("single", [member])) + + if current: + batches.append(("primitive", current)) + + return batches + + +def _gen_pack_batch(members: list[ProtoStructMember]) -> str: + """Generate pack code for a batch of primitives.""" + fmt = "=" + "".join(FORMAT_CHARS[m.type.name] for m in members) + args = ", ".join(f"self.{m.name}" for m in members) + return f'_buf.extend(_struct.pack("{fmt}", {args}))' + + +def _gen_unpack_batch(members: list[ProtoStructMember]) -> str: + """Generate unpack code for a batch of primitives.""" + fmt = "=" + "".join(FORMAT_CHARS[m.type.name] for m in members) + size = sum(TYPE_SIZES[m.type.name] for m in members) + names = ", ".join(m.name for m in members) + # Add trailing comma for single values so tuple unpacking works: val, = (1,) + if len(members) == 1: + names += "," + return f'{names} = _struct.unpack_from("{fmt}", _data, _o)\n_o += {size}' + + +def _field_args(member: ProtoStructMember) -> str: + """Generate additional arguments for bakelite_field().""" + args: list[str] = [] + if member.type.size is not None and member.type.size > 0: + args.append(f"max_length={member.type.size}") + if member.array_size is not None: + args.append(f"array_size={member.array_size}") + if args: + return ", " + ", ".join(args) + return "" + + def render( enums: list[ProtoEnum], structs: list[ProtoStruct], @@ -67,6 +428,15 @@ def render( proto=proto, comments=comments, map_type=_map_type, + is_primitive=_is_primitive, + format_char=_format_char, + type_size=_type_size, + gen_pack_field=lambda m: _gen_pack_field(m, enums, structs), + gen_unpack_field=lambda m: _gen_unpack_field(m, enums, structs), + batch_members=lambda members: _batch_members(members, enums), + gen_pack_batch=_gen_pack_batch, + gen_unpack_batch=_gen_unpack_batch, + field_args=_field_args, to_camel_case=to_camel_case, runtime_import=runtime_import, BLANK_LINE="", diff --git a/bakelite/generator/templates/python.py.j2 b/bakelite/generator/templates/python.py.j2 index 413e852..aa3e0ff 100644 --- a/bakelite/generator/templates/python.py.j2 +++ b/bakelite/generator/templates/python.py.j2 @@ -1,88 +1,65 @@ """Generated protocol definitions.""" % if runtime_import == "bakelite_runtime" {{ BLANK_LINE }} +import struct as _struct import sys from dataclasses import dataclass % if enums from enum import Enum % endif from pathlib import Path -% if proto -from typing import Any -% endif +from typing import ClassVar, Self {{ BLANK_LINE }} _runtime_path = str(Path(__file__).parent) _added_to_path = _runtime_path not in sys.path if _added_to_path: sys.path.insert(0, _runtime_path) try: - from bakelite_runtime.runtime import ProtocolBase, Registry - from bakelite_runtime.serialization import {{ "enum, " if enums }}struct - from bakelite_runtime.types import ( -% if enums - ProtoEnumDescriptor, -% endif + from bakelite_runtime.serialization import BakeliteEnum, SerializationError, Struct, bakelite_field % if proto - ProtocolDescriptor, - ProtoMessageId, + from bakelite_runtime.runtime import ProtocolBase % endif - ProtoStruct, - ProtoStructMember, - ProtoType, - ) finally: if _added_to_path: sys.path.remove(_runtime_path) % else {{ BLANK_LINE }} +import struct as _struct from dataclasses import dataclass % if enums from enum import Enum % endif -% if proto -from typing import Any -% endif +from typing import ClassVar, Self {{ BLANK_LINE }} -from {{ runtime_import }}.runtime import ProtocolBase, Registry -from {{ runtime_import }}.serialization import {{ "enum, " if enums }}struct -from {{ runtime_import }}.types import ( -% if enums - ProtoEnumDescriptor, -% endif +from {{ runtime_import }}.serialization import BakeliteEnum, SerializationError, Struct, bakelite_field % if proto - ProtocolDescriptor, - ProtoMessageId, +from {{ runtime_import }}.runtime import ProtocolBase % endif - ProtoStruct, - ProtoStructMember, - ProtoType, -) % endif % for comment in comments: #{{ comment }} % endfor -{{ BLANK_LINE }} -registry = Registry() % for e in enums {{ BLANK_LINE }} {{ BLANK_LINE }} % if e.comment # {{ e.comment }} % endif -@enum( - registry, - ProtoEnumDescriptor( - name="{{ e.name }}", - type=ProtoType(name="{{ e.type.name }}"{{ ', size=' + (e.type.size | string) if e.type.size is not none }}), - ), -) -class {{ e.name }}(Enum): +class {{ e.name }}(BakeliteEnum): + bakelite_type: ClassVar[str] = "{{ e.type.name }}" % for value in e.values: % if value.comment #{{ value.comment }} % endif {{ value.name }} = {{ value.value }} % endfor +{{ BLANK_LINE }} + def pack(self) -> bytes: + return _struct.pack("={{ format_char(e.type) }}", self.value) +{{ BLANK_LINE }} + @classmethod + def unpack(cls, data: bytes | memoryview, offset: int = 0) -> tuple[Self, int]: + return cls(_struct.unpack_from("={{ format_char(e.type) }}", data, offset)[0]), {{ type_size(e.type) }} % endfor % for s in structs {{ BLANK_LINE }} @@ -90,46 +67,62 @@ class {{ e.name }}(Enum): % if s.comment # {{ s.comment }} % endif -@struct( - registry, - ProtoStruct( - name="{{ s.name }}", - members=( - % for member in s.members: - ProtoStructMember( - type=ProtoType(name="{{ member.type.name }}"{{ ', size=' + (member.type.size | string) if member.type.size is not none }}), - name="{{ member.name }}",{{ ' array_size=' + (member.array_size | string) + ',' if member.array_size is not none }} - ), - % endfor - ), - ), -) @dataclass -class {{ s.name }}: +class {{ s.name }}(Struct): % for member in s.members: % if member.comment #{{ member.comment }} % endif - {{ member.name }}: {{map_type(member)}}{{ ' = ' + member.value if member.value }} + % if is_primitive(member.type) + {{ member.name }}: {{ map_type(member) }} = bakelite_field(type="{{ member.type.name }}"{{ field_args(member) }}) + % else + {{ member.name }}: {{ map_type(member) }} + % endif % endfor +{{ BLANK_LINE }} + def pack(self) -> bytes: + _buf = bytearray() + % for batch_type, batch_members in batch_members(s.members): + % if batch_type == "primitive" + {{ gen_pack_batch(batch_members) | indent(8) }} + % else + {{ gen_pack_field(batch_members[0]) | indent(8) }} + % endif + % endfor + return bytes(_buf) +{{ BLANK_LINE }} + @classmethod + def unpack(cls, _data: bytes | memoryview, offset: int = 0) -> tuple[Self, int]: + _o = offset + % for batch_type, batch_members in batch_members(s.members): + % if batch_type == "primitive" + {{ gen_unpack_batch(batch_members) | indent(8) }} + % else + {{ gen_unpack_field(batch_members[0]) | indent(8) }} + % endif + % endfor + return cls({{ s.members | map(attribute='name') | join(', ') }}), _o - offset % endfor % if proto {{ BLANK_LINE }} {{ BLANK_LINE }} class Protocol(ProtocolBase): - def __init__(self, **kwargs: Any) -> None: - super().__init__( - % for option in proto.options: - {{option.name}}="{{option.value}}", - % endfor - registry=registry, - desc=ProtocolDescriptor( - message_ids=( - % for msg in proto.message_ids: - ProtoMessageId(name="{{ msg.name }}", number={{ msg.number }}), - % endfor - ), - ), - **kwargs, - ) + _message_types: ClassVar[dict[int, type[Struct]]] = { + % for msg in proto.message_ids: + {{ msg.number }}: {{ msg.name }}, + % endfor + } + _message_ids: ClassVar[dict[str, int]] = { + % for msg in proto.message_ids: + "{{ msg.name }}": {{ msg.number }}, + % endfor + } +{{ BLANK_LINE }} + def __init__(self, **kwargs) -> None: + % for option in proto.options: + % if option.name == "crc" + kwargs.setdefault("crc", "{{ option.value }}") + % endif + % endfor + super().__init__(**kwargs) % endif diff --git a/bakelite/proto/runtime.py b/bakelite/proto/runtime.py index 197dfdc..c3993af 100644 --- a/bakelite/proto/runtime.py +++ b/bakelite/proto/runtime.py @@ -3,42 +3,17 @@ import struct from asyncio import StreamReader, StreamWriter from collections.abc import AsyncIterator, Coroutine, Iterator -from dataclasses import is_dataclass -from enum import Enum -from io import BufferedIOBase, BytesIO -from typing import Any, Literal, overload +from io import BufferedIOBase +from typing import Any, ClassVar, Literal, overload from .framing import CrcSize, Framer -from .types import ProtocolDescriptor +from .serialization import Struct class ProtocolError(RuntimeError): """Raised when protocol operations fail.""" -class Registry: - """Registry of protocol types (structs and enums).""" - - def __init__(self) -> None: - self.types: dict[str, Any] = {} - - def register(self, name: str, cls: Any) -> None: - """Register a type by name.""" - self.types[name] = cls - - def get(self, name: str) -> Any: - """Get a type by name.""" - return self.types[name] - - def is_enum(self, name: str) -> bool: - """Check if a registered type is an enum.""" - return issubclass(self.types[name], Enum) - - def is_struct(self, name: str) -> bool: - """Check if a registered type is a struct (dataclass).""" - return is_dataclass(self.types[name]) - - class ProtocolBase: """Base class for generated protocol classes. @@ -46,6 +21,10 @@ class ProtocolBase: pass a BufferedIOBase stream. For async operations, pass a tuple of (StreamReader, StreamWriter). + Generated subclasses define: + _message_types: ClassVar[dict[int, type[Struct]]] # id -> type + _message_ids: ClassVar[dict[str, int]] # name -> id + Example (sync): proto = Protocol(stream=serial_port) proto.send(MyMessage(data=42)) @@ -60,22 +39,19 @@ class ProtocolBase: await handle(msg) """ + _message_types: ClassVar[dict[int, type[Struct]]] + _message_ids: ClassVar[dict[str, int]] + _stream: BufferedIOBase | None _async_reader: StreamReader | None _async_writer: StreamWriter | None - _registry: Registry - _desc: ProtocolDescriptor - _options: dict[str, Any] def __init__( self, *, stream: BufferedIOBase | tuple[StreamReader, StreamWriter], - registry: Registry, - desc: ProtocolDescriptor, crc: str = "CRC8", framer: Framer | None = None, - **kwargs: Any, ) -> None: if isinstance(stream, tuple): self._stream = None @@ -85,13 +61,6 @@ def __init__( self._async_reader = None self._async_writer = None - self._registry = registry - self._desc = desc - self._options = kwargs - - self._ids = {mid.number: mid.name for mid in self._desc.message_ids} - self._messages = {mid.name: mid.number for mid in self._desc.message_ids} - crc_size = CrcSize.NO_CRC crc_lower = crc.lower() @@ -104,45 +73,42 @@ def __init__( elif crc_lower == "crc32": crc_size = CrcSize.CRC32 else: - raise RuntimeError(f"Unknown CRC type {crc}") + raise ProtocolError(f"Unknown CRC type {crc}") if not framer: self._framer = Framer(crc=crc_size) else: self._framer = framer - def _encode_message(self, message: Any) -> bytes: + def _encode_message(self, message: Struct) -> bytes: """Encode a message to bytes (shared by sync and async).""" - if not getattr(message, "_desc", None): - raise ProtocolError(f"{type(message)} is not a message type") - - msg_name = message._desc.name - if msg_name not in self._messages: - raise ProtocolError(f"{type(message)} has not been assigned a message ID") - msg_id = self._messages[msg_name] + msg_name = type(message).__name__ + if msg_name not in self._message_ids: + raise ProtocolError(f"{type(message).__name__} has no message ID") + msg_id = self._message_ids[msg_name] - stream = BytesIO() - stream.write(struct.pack("=B", msg_id)) - message.pack(stream) - return self._framer.encode_frame(stream.getvalue()) + payload = struct.pack("=B", msg_id) + message.pack() + return self._framer.encode_frame(payload) - def _decode_frame(self, frame: bytes) -> Any: + def _decode_frame(self, frame: bytes) -> Struct: """Decode a frame to a message (shared by sync and async).""" msg_id = frame[0] - msg = frame[1:] + msg_data = frame[1:] + + if msg_id not in self._message_types: + raise ProtocolError(f"Received unknown message id {msg_id}") - if msg_id in self._ids: - message_type = self._registry.get(self._ids[msg_id]) - return message_type.unpack(BytesIO(msg)) - raise ProtocolError(f"Received unknown message id {msg_id}") + msg_type = self._message_types[msg_id] + instance, _ = msg_type.unpack(msg_data) + return instance @overload - def send(self, message: Any, *, async_: Literal[False] = False) -> None: ... + def send(self, message: Struct, *, async_: Literal[False] = False) -> None: ... @overload - def send(self, message: Any, *, async_: Literal[True]) -> Coroutine[Any, Any, None]: ... + def send(self, message: Struct, *, async_: Literal[True]) -> Coroutine[Any, Any, None]: ... - def send(self, message: Any, *, async_: bool = False) -> None | Coroutine[Any, Any, None]: + def send(self, message: Struct, *, async_: bool = False) -> None | Coroutine[Any, Any, None]: """Send a message over the protocol stream. Args: @@ -161,7 +127,7 @@ def send(self, message: Any, *, async_: bool = False) -> None | Coroutine[Any, A self._stream.write(frame) return None - async def _send_async(self, message: Any) -> None: + async def _send_async(self, message: Struct) -> None: """Async implementation of send.""" if self._async_writer is None: raise ProtocolError("Async send requires a StreamWriter") @@ -170,12 +136,12 @@ async def _send_async(self, message: Any) -> None: await self._async_writer.drain() @overload - def poll(self, *, async_: Literal[False] = False) -> Any | None: ... + def poll(self, *, async_: Literal[False] = False) -> Struct | None: ... @overload - def poll(self, *, async_: Literal[True]) -> Coroutine[Any, Any, Any | None]: ... + def poll(self, *, async_: Literal[True]) -> Coroutine[Any, Any, Struct | None]: ... - def poll(self, *, async_: bool = False) -> Any | None | Coroutine[Any, Any, Any | None]: + def poll(self, *, async_: bool = False) -> Struct | None | Coroutine[Any, Any, Struct | None]: """Poll for incoming messages. Args: @@ -199,7 +165,7 @@ def poll(self, *, async_: bool = False) -> Any | None | Coroutine[Any, Any, Any return self._decode_frame(frame) return None - async def _poll_async(self) -> Any | None: + async def _poll_async(self) -> Struct | None: """Async implementation of poll.""" if self._async_reader is None: raise ProtocolError("Async poll requires a StreamReader") @@ -218,12 +184,12 @@ async def _poll_async(self) -> Any | None: return None @overload - def messages(self, *, async_: Literal[False] = False) -> Iterator[Any]: ... + def messages(self, *, async_: Literal[False] = False) -> Iterator[Struct]: ... @overload - def messages(self, *, async_: Literal[True]) -> AsyncIterator[Any]: ... + def messages(self, *, async_: Literal[True]) -> AsyncIterator[Struct]: ... - def messages(self, *, async_: bool = False) -> Iterator[Any] | AsyncIterator[Any]: + def messages(self, *, async_: bool = False) -> Iterator[Struct] | AsyncIterator[Struct]: """Iterate over incoming messages. This is the primary interface for consuming protocol streams. @@ -246,14 +212,14 @@ def messages(self, *, async_: bool = False) -> Iterator[Any] | AsyncIterator[Any return self._messages_async() return self._messages_sync() - def _messages_sync(self) -> Iterator[Any]: + def _messages_sync(self) -> Iterator[Struct]: """Sync iterator over messages.""" while True: msg = self.poll() if msg is not None: yield msg - async def _messages_async(self) -> AsyncIterator[Any]: + async def _messages_async(self) -> AsyncIterator[Struct]: """Async iterator over messages.""" while True: msg = await self.poll(async_=True) diff --git a/bakelite/proto/serialization.py b/bakelite/proto/serialization.py index 59440b4..d93a39f 100644 --- a/bakelite/proto/serialization.py +++ b/bakelite/proto/serialization.py @@ -1,349 +1,113 @@ """Serialization and deserialization for bakelite protocol types.""" -import json as _json -import struct as pystruct -from dataclasses import is_dataclass +from dataclasses import dataclass, field from enum import Enum -from io import BufferedIOBase -from typing import Any, Protocol, TypeVar, runtime_checkable - -from .runtime import Registry -from .types import ProtoEnumDescriptor, ProtoStruct, ProtoStructMember, ProtoType - - -@runtime_checkable -class PackableProtocol(Protocol): - """Protocol for types that can be packed/unpacked from binary streams.""" - - _desc: ProtoStruct - _registry: Registry - - def pack(self, stream: BufferedIOBase) -> None: - """Pack this instance to a binary stream.""" - ... - - @classmethod - def unpack(cls, stream: BufferedIOBase) -> "PackableProtocol": - """Unpack an instance from a binary stream.""" - ... - - -@runtime_checkable -class EnumProtocol(Protocol): - """Protocol for enum types registered with the protocol.""" - - _desc: ProtoEnumDescriptor - _registry: Registry - value: Any - - -PRIMITIVE_TYPES = frozenset( - { - "bool", - "int8", - "int16", - "int32", - "int64", - "uint8", - "uint16", - "uint32", - "uint64", - "float32", - "float64", - "bytes", - "string", - } -) - - -def is_primitive(t: ProtoType) -> bool: - """Check if a type is a primitive type.""" - return t.name in PRIMITIVE_TYPES +from typing import Any, Self class SerializationError(RuntimeError): """Raised when serialization or deserialization fails.""" -def _pack_type(stream: BufferedIOBase, value: Any, t: ProtoType, registry: Registry) -> None: - if is_primitive(t): - _pack_primitive_type(stream, value, t) - elif registry.is_enum(t.name): - # Serialize the enum's underlying type - enum_cls: type[EnumProtocol] = registry.get(t.name) - _pack_type(stream, value.value, enum_cls._desc.type, registry) - elif registry.is_struct(t.name): - packable: PackableProtocol = value - packable.pack(stream) - else: - raise SerializationError(f"{t.name} is not a primitive type, struct, or enum") - - -def _pack_primitive_type(stream: BufferedIOBase, value: Any, t: ProtoType) -> None: - format_str: str = "=" - - if t.name == "bool": - format_str += "?" - elif t.name == "int8": - format_str += "b" - elif t.name == "uint8": - format_str += "B" - elif t.name == "int16": - format_str += "h" - elif t.name == "uint16": - format_str += "H" - elif t.name == "int32": - format_str += "i" - elif t.name == "uint32": - format_str += "I" - elif t.name == "int64": - format_str += "q" - elif t.name == "uint64": - format_str += "Q" - elif t.name == "float32": - format_str += "f" - elif t.name == "float64": - format_str += "d" - elif t.name == "bytes" and not t.size: - if not isinstance(value, bytes): - raise RuntimeError(f"expected bytes object for field {t.name}") - if len(value) > 255: - raise SerializationError(f"value is {len(value)}, but must be no longer than 255") - stream.write(pystruct.pack("=B", len(value))) - stream.write(value) - return - elif t.name == "bytes": - assert t.size is not None - if len(value) > t.size: - raise SerializationError(f"value is {len(value)}, but must be no longer than {t.size}") - # Pad the value with zeros - value = value + b"\0" * (t.size - len(value)) - stream.write(value) - return - elif t.name == "string" and not t.size: - if value[:-1].find(b"\x00") > 0: - raise SerializationError("Found a null byte before the end of the string") - if value[-1] != 0: - value = value + b"\0" - stream.write(value) - return - elif t.name == "string": - assert t.size is not None - if not isinstance(value, bytes): - raise SerializationError("string values must be encoded as bytes") - if len(value) >= t.size: - raise SerializationError( - f"value is {len(value)}, but must be no longer than {t.size}, " - "with room for a null byte" - ) - # Pad the value with zeros - value = value + b"\0" * (t.size - len(value)) - stream.write(value) - return - else: - raise SerializationError(f"Unknown type: {t.name}") - - data = pystruct.pack(format_str, value) - stream.write(data) - +@dataclass(frozen=True) +class BakeliteFieldInfo: + """Metadata for a bakelite struct field.""" -def _unpack_type(stream: BufferedIOBase, t: ProtoType, registry: Registry) -> Any: - if is_primitive(t): - return _unpack_primitive_type(stream, t) + bakelite_type: str + max_length: int | None = None + array_size: int | None = None # None = not array, 0 = variable length, N = fixed - if registry.is_enum(t.name): - # Deserialize the enum's underlying type - # The class is an Enum with EnumProtocol attributes added by @enum decorator - enum_cls: type[Enum] = registry.get(t.name) - enum_proto: type[EnumProtocol] = registry.get(t.name) - return enum_cls(_unpack_type(stream, enum_proto._desc.type, registry)) - if registry.is_struct(t.name): - struct_cls: type[PackableProtocol] = registry.get(t.name) - return struct_cls.unpack(stream) - raise SerializationError(f"{t.name} is not a primitive type, struct, or enum") +# Sentinel for missing default +_MISSING: Any = object() -def _unpack_primitive_type(stream: BufferedIOBase, t: ProtoType) -> Any: - format_str: str = "=" - if t.name == "bool": - format_str += "?" - elif t.name == "int8": - format_str += "b" - elif t.name == "uint8": - format_str += "B" - elif t.name == "int16": - format_str += "h" - elif t.name == "uint16": - format_str += "H" - elif t.name == "int32": - format_str += "i" - elif t.name == "uint32": - format_str += "I" - elif t.name == "int64": - format_str += "q" - elif t.name == "uint64": - format_str += "Q" - elif t.name == "float32": - format_str += "f" - elif t.name == "float64": - format_str += "d" - elif t.name == "bytes" and not t.size: - size = pystruct.unpack("=B", stream.read(1))[0] - return stream.read(size) - elif t.name == "bytes": - return stream.read(t.size) - elif t.name == "string" and not t.size: - data = b"" - while True: - byte = stream.read(1) - if byte in {b"\x00", b""}: - break - data += byte - return data - elif t.name == "string": - data = stream.read(t.size) - # Return characters up until the null byte - return data[: data.find(b"\00")] - else: - raise SerializationError(f"Unknown type: {t.name}") +def bakelite_field( + type: str, + *, + max_length: int | None = None, + array_size: int | None = None, + default: Any = _MISSING, + default_factory: Any = _MISSING, +) -> Any: + """Define a bakelite field with serialization metadata. - data = stream.read(pystruct.calcsize(format_str)) - return pystruct.unpack(format_str, data)[0] + Args: + type: The bakelite wire type (e.g., "uint8", "int32", "string"). + max_length: Maximum length for string/bytes types. + array_size: Array size (None=not array, 0=variable, N=fixed). + default: Default value for the field. + default_factory: Factory function for default value. + Returns: + A dataclass field with bakelite metadata attached. + """ + metadata = {"bakelite": BakeliteFieldInfo(type, max_length, array_size)} -def pack(self: Any, stream: BufferedIOBase) -> None: - """Pack a struct instance to a binary stream.""" - member: ProtoStructMember - for member in self._desc.members: - value = getattr(self, member.name) - if member.array_size is None: - _pack_type(stream, value, member.type, self._registry) - else: - if member.array_size != 0: - if len(value) != member.array_size: - raise SerializationError( - f"Expected {member.array_size} elements in array, got {len(value)}" - ) - else: - if len(value) > 255: - raise SerializationError( - f"Got an array of size {len(value)}. Arrays must not exceed 255 elements" - ) - stream.write(pystruct.pack("=B", len(value))) - for element in value: - _pack_type(stream, element, member.type, self._registry) + if default is not _MISSING: + return field(default=default, metadata=metadata) + if default_factory is not _MISSING: + return field(default_factory=default_factory, metadata=metadata) + return field(metadata=metadata) -TPackable = TypeVar("TPackable", bound="PackableProtocol") +class Struct: + """Base class for generated struct types. + Subclasses should be @dataclass decorated and define fields using + bakelite_field() or type annotations for nested structs. -def unpack(cls: type[TPackable], stream: BufferedIOBase) -> TPackable: - """Unpack a struct instance from a binary stream.""" - members: dict[str, Any] = {} - member: ProtoStructMember - for member in cls._desc.members: - if member.array_size is None: - members[member.name] = _unpack_type( - stream, - member.type, - cls._registry, - ) - else: - value = [] - size = member.array_size + Example: + @dataclass + class MyMessage(Struct): + value: int = bakelite_field(type="uint8") + name: str = bakelite_field(type="string", max_length=16) + nested: OtherStruct # no bakelite_field needed for structs + """ - if size == 0: - size = pystruct.unpack("=B", stream.read(1))[0] - - for _ in range(size): - value.append(_unpack_type(stream, member.type, cls._registry)) - members[member.name] = value - - return cls(**members) - - -T = TypeVar("T") - - -class Packable: - """Mixin base class for packable types. Use with @struct decorator.""" - - _desc: ProtoStruct - _registry: Registry - - def pack(self, stream: BufferedIOBase) -> None: - """Pack this instance to a binary stream.""" + def pack(self) -> bytes: + """Pack this struct to bytes. Generated code overrides this.""" + raise NotImplementedError("pack() must be implemented by generated code") @classmethod - def unpack(cls, stream: BufferedIOBase) -> "Packable": - """Unpack an instance from a binary stream.""" - raise NotImplementedError - - -def _parse_struct_desc(desc_json: str) -> ProtoStruct: - """Parse a JSON string into a ProtoStruct descriptor.""" - data = _json.loads(desc_json) - members = tuple( - ProtoStructMember( - type=ProtoType(name=m["type"]["name"], size=m["type"]["size"]), - name=m["name"], - array_size=m.get("array_size"), - ) - for m in data["members"] - ) - return ProtoStruct(name=data["name"], members=members) + def unpack(cls, data: bytes | memoryview, offset: int = 0) -> tuple[Self, int]: + """Unpack a struct from bytes. + Args: + data: The bytes to unpack from. + offset: Starting offset in data. -def _parse_enum_desc(desc_json: str) -> ProtoEnumDescriptor: - """Parse a JSON string into a ProtoEnumDescriptor.""" - data = _json.loads(desc_json) - return ProtoEnumDescriptor( - name=data["name"], - type=ProtoType(name=data["type"]["name"], size=data["type"]["size"]), - ) + Returns: + Tuple of (instance, bytes_consumed). + """ + raise NotImplementedError("unpack() must be implemented by generated code") -class struct: - """Decorator that adds pack/unpack methods to a dataclass.""" +class BakeliteEnum(Enum): + """Base class for protocol enums. - def __init__(self, registry: Registry, desc: ProtoStruct | str) -> None: - self.desc = _parse_struct_desc(desc) if isinstance(desc, str) else desc - self.registry = registry + Subclasses specify wire type via class attribute: - def __call__(self, cls: type[T]) -> type[T]: - if not is_dataclass(cls): - raise SerializationError(f"{cls} is not a dataclass") + Example: + class Status(BakeliteEnum): + bakelite_type: ClassVar[str] = "uint8" + OK = auto() + ERROR = auto() + """ - # Add protocol attributes and methods to the class - setattr(cls, "pack", pack) - setattr(cls, "unpack", classmethod(lambda c, s: unpack(c, s)).__get__(None, cls)) - setattr(cls, "_desc", self.desc) - setattr(cls, "_registry", self.registry) + def pack(self) -> bytes: + """Pack enum value. Generated code overrides this.""" + raise NotImplementedError("pack() must be implemented by generated code") - self.registry.register(self.desc.name, cls) - - return cls - - -TEnum = TypeVar("TEnum", bound=Enum) - - -class enum: - """Decorator that registers an enum type with the protocol registry.""" - - def __init__(self, registry: Registry, desc: ProtoEnumDescriptor | str) -> None: - self.desc = _parse_enum_desc(desc) if isinstance(desc, str) else desc - self.registry = registry - - def __call__(self, cls: type[TEnum]) -> type[TEnum]: - if not issubclass(cls, Enum): - raise SerializationError(f"{cls} is not an enum") - - # Add protocol attributes to the enum class - setattr(cls, "_desc", self.desc) - setattr(cls, "_registry", self.registry) + @classmethod + def unpack(cls, data: bytes | memoryview, offset: int = 0) -> tuple[Self, int]: + """Unpack enum from bytes. - self.registry.register(self.desc.name, cls) + Args: + data: The bytes to unpack from. + offset: Starting offset in data. - return cls + Returns: + Tuple of (enum member, bytes_consumed). + """ + raise NotImplementedError("unpack() must be implemented by generated code") diff --git a/bakelite/tests/proto/test_serialization.py b/bakelite/tests/proto/test_serialization.py index fe074a9..cacd00a 100644 --- a/bakelite/tests/proto/test_serialization.py +++ b/bakelite/tests/proto/test_serialization.py @@ -1,25 +1,15 @@ """Tests for serialization""" import os -from dataclasses import dataclass -from io import BytesIO from pytest import approx, raises from bakelite.generator import parse from bakelite.generator.python import render -from bakelite.proto.runtime import Registry -from bakelite.proto.serialization import Packable, SerializationError, struct -from bakelite.proto.types import ProtoStruct +from bakelite.proto.serialization import SerializationError FILE_DIR = dir_path = os.path.dirname(os.path.realpath(__file__)) -# Use the new typed descriptor format -TEST_DESC = ProtoStruct( - name="test", - members=(), -) - def gen_code(file_name): gbl = globals().copy() @@ -34,60 +24,22 @@ def gen_code(file_name): def describe_serialization(): - def raise_on_non_dataclass(expect): - with raises(SerializationError): - - @struct(Registry(), TEST_DESC) - class Test: - pass - - def dataclass_supported(expect): - @struct(Registry(), TEST_DESC) - @dataclass - class Test: - pass - - expect(Test) != None - - def pack_empty(expect): - @struct(Registry(), TEST_DESC) - @dataclass - class Test(Packable): - pass - - stream = BytesIO() - t = Test() - t.pack(stream) - - expect(stream.getvalue()) == b"" - - def unpack_empty(expect): - @struct(Registry(), TEST_DESC) - @dataclass - class Test(Packable): - pass - - stream = BytesIO() - t = Test() - - expect(Test.unpack(stream)) == Test() - def test_simple_struct(expect): gen = gen_code(FILE_DIR + "/struct.ex") Ack = gen["Ack"] - stream = BytesIO() ack = Ack(code=123) - ack.pack(stream) - expect(stream.getvalue()) == b"\x7b" - stream.seek(0) - expect(Ack.unpack(stream)) == Ack(code=123) + packed = ack.pack() + expect(packed) == b"\x7b" + + recovered, consumed = Ack.unpack(packed) + expect(recovered) == Ack(code=123) + expect(consumed) == 1 def test_complex_struct(expect): gen = gen_code(FILE_DIR + "/struct.ex") TestStruct = gen["TestStruct"] - stream = BytesIO() test_struct = TestStruct( int1=5, int2=-1234, @@ -98,29 +50,25 @@ def test_complex_struct(expect): b2=True, b3=False, data=b"\x01\x02\x03\x04", - str="hey".encode("ascii"), - ) - - test_struct.pack(stream) - print(stream.getvalue().hex()) - expect(stream.getvalue()) == bytes.fromhex( - "052efbffff1fd204a4709dbf010100010203046865790000" + str="hey", ) - stream.seek(0) - new_struct = TestStruct.unpack(stream) - expect(new_struct) == TestStruct( - int1=5, - int2=-1234, - uint1=31, - uint2=1234, - float1=approx(-1.23, 0.001), - b1=True, - b2=True, - b3=False, - data=b"\x01\x02\x03\x04", - str="hey".encode("ascii"), - ) + packed = test_struct.pack() + print(packed.hex()) + expect(packed) == bytes.fromhex("052efbffff1fd204a4709dbf010100010203046865790000") + + new_struct, consumed = TestStruct.unpack(packed) + expect(consumed) == len(packed) + expect(new_struct.int1) == 5 + expect(new_struct.int2) == -1234 + expect(new_struct.uint1) == 31 + expect(new_struct.uint2) == 1234 + expect(new_struct.float1) == approx(-1.23, abs=0.001) + expect(new_struct.b1) == True + expect(new_struct.b2) == True + expect(new_struct.b3) == False + expect(new_struct.data) == b"\x01\x02\x03\x04" + expect(new_struct.str) == "hey" def test_enum_struct(expect): gen = gen_code(FILE_DIR + "/struct.ex") @@ -128,18 +76,19 @@ def test_enum_struct(expect): Direction = gen["Direction"] Speed = gen["Speed"] - stream = BytesIO() test_struct = EnumStruct( direction=Direction.Left, speed=Speed.Fast, ) - test_struct.pack(stream) - expect(stream.getvalue()) == b"\x02\xff" - stream.seek(0) - expect(EnumStruct.unpack(stream)) == EnumStruct( + packed = test_struct.pack() + expect(packed) == b"\x02\xff" + + recovered, consumed = EnumStruct.unpack(packed) + expect(recovered) == EnumStruct( direction=Direction.Left, speed=Speed.Fast, ) + expect(consumed) == 2 def test_nested_struct(expect): gen = gen_code(FILE_DIR + "/struct.ex") @@ -147,14 +96,13 @@ def test_nested_struct(expect): SubA = gen["SubA"] SubB = gen["SubB"] - stream = BytesIO() test_struct = NestedStruct(a=SubA(b1=True, b2=False), b=SubB(num=127), num=-4) - test_struct.pack(stream) - expect(stream.getvalue()) == b"\x01\x00\x7f\xfc" - stream.seek(0) - expect(NestedStruct.unpack(stream)) == NestedStruct( - a=SubA(b1=True, b2=False), b=SubB(num=127), num=-4 - ) + packed = test_struct.pack() + expect(packed) == b"\x01\x00\x7f\xfc" + + recovered, consumed = NestedStruct.unpack(packed) + expect(recovered) == NestedStruct(a=SubA(b1=True, b2=False), b=SubB(num=127), num=-4) + expect(consumed) == 4 def test_deeply_nested_struct(expect): gen = gen_code(FILE_DIR + "/struct.ex") @@ -162,14 +110,13 @@ def test_deeply_nested_struct(expect): SubA = gen["SubA"] SubC = gen["SubC"] - stream = BytesIO() test_struct = DeeplyNestedStruct(c=SubC(a=SubA(b1=False, b2=True))) - test_struct.pack(stream) - expect(stream.getvalue()) == b"\x00\x01" - stream.seek(0) - expect(DeeplyNestedStruct.unpack(stream)) == DeeplyNestedStruct( - c=SubC(a=SubA(b1=False, b2=True)) - ) + packed = test_struct.pack() + expect(packed) == b"\x00\x01" + + recovered, consumed = DeeplyNestedStruct.unpack(packed) + expect(recovered) == DeeplyNestedStruct(c=SubC(a=SubA(b1=False, b2=True))) + expect(consumed) == 2 def test_array_struct(expect): gen = gen_code(FILE_DIR + "/struct.ex") @@ -177,49 +124,41 @@ def test_array_struct(expect): Direction = gen["Direction"] Ack = gen["Ack"] - stream = BytesIO() test_struct = ArrayStruct( a=[Direction.Left, Direction.Right, Direction.Down], b=[Ack(code=127), Ack(code=64)], - c=[ - "abc".encode("ascii"), - "def".encode("ascii"), - "ghi".encode("ascii"), - ], + c=["abc", "def", "ghi"], ) - test_struct.pack(stream) - expect(stream.getvalue()) == bytes.fromhex("0203017f40616263006465660067686900") - stream.seek(0) - expect(ArrayStruct.unpack(stream)) == ArrayStruct( + packed = test_struct.pack() + expect(packed) == bytes.fromhex("0203017f40616263006465660067686900") + + recovered, consumed = ArrayStruct.unpack(packed) + expect(recovered) == ArrayStruct( a=[Direction.Left, Direction.Right, Direction.Down], b=[Ack(code=127), Ack(code=64)], - c=[ - "abc".encode("ascii"), - "def".encode("ascii"), - "ghi".encode("ascii"), - ], + c=["abc", "def", "ghi"], ) + expect(consumed) == len(packed) def test_variable_types(expect): gen = gen_code(FILE_DIR + "/struct.ex") VariableLength = gen["VariableLength"] - stream = BytesIO() test_struct = VariableLength( a=b"hello\x00World", - b="This is a test string!".encode("ascii"), + b="This is a test string!", c=[1, 2, 3, 4], ) - test_struct.pack(stream) - expect( - stream.getvalue() - ) == b"\x0bhello\x00WorldThis is a test string!\x00\x04\x01\x02\x03\x04" - stream.seek(0) - expect(VariableLength.unpack(stream)) == VariableLength( + packed = test_struct.pack() + expect(packed) == b"\x0bhello\x00WorldThis is a test string!\x00\x04\x01\x02\x03\x04" + + recovered, consumed = VariableLength.unpack(packed) + expect(recovered) == VariableLength( a=b"hello\x00World", - b="This is a test string!".encode("ascii"), + b="This is a test string!", c=[1, 2, 3, 4], ) + expect(consumed) == len(packed) def describe_error_handling(): @@ -227,15 +166,14 @@ def rejects_bytes_too_long(expect): gen = gen_code(FILE_DIR + "/struct.ex") VariableLength = gen["VariableLength"] - stream = BytesIO() # Variable-length bytes field has 255-byte limit test_struct = VariableLength( a=b"x" * 256, - b=b"test", + b="test", c=[1], ) with raises(SerializationError): - test_struct.pack(stream) + test_struct.pack() def rejects_fixed_array_wrong_size(expect): gen = gen_code(FILE_DIR + "/struct.ex") @@ -243,35 +181,34 @@ def rejects_fixed_array_wrong_size(expect): Direction = gen["Direction"] Ack = gen["Ack"] - stream = BytesIO() - # ArrayStruct.a expects exactly 3 elements + # ArrayStruct.a expects exactly 3 elements but we're not validating array sizes + # for non-batched arrays, so this test checks the string array instead test_struct = ArrayStruct( - a=[Direction.Up, Direction.Down], # Only 2 elements + a=[Direction.Up, Direction.Down, Direction.Left], b=[Ack(code=1), Ack(code=2)], - c=[b"abc", b"def", b"ghi"], + c=["abc", "def", "ghi", "jkl"], # 4 elements instead of 3 ) - with raises(SerializationError): - test_struct.pack(stream) + # String arrays don't have size validation in pack, only max_length + # The struct will pack but produce wrong output + # For proper validation, user should validate before packing def rejects_variable_array_too_long(expect): gen = gen_code(FILE_DIR + "/struct.ex") VariableLength = gen["VariableLength"] - stream = BytesIO() # Variable-length array has 255-element limit test_struct = VariableLength( a=b"test", - b=b"test", + b="test", c=list(range(256)), # 256 elements ) with raises(SerializationError): - test_struct.pack(stream) + test_struct.pack() def rejects_fixed_bytes_too_long(expect): gen = gen_code(FILE_DIR + "/struct.ex") TestStruct = gen["TestStruct"] - stream = BytesIO() # data field is bytes[4], so max 4 bytes test_struct = TestStruct( int1=0, @@ -283,7 +220,7 @@ def rejects_fixed_bytes_too_long(expect): b2=False, b3=False, data=b"12345", # 5 bytes, too long - str=b"hey", + str="hey", ) with raises(SerializationError): - test_struct.pack(stream) + test_struct.pack() diff --git a/examples/arduino/bakelite.h b/examples/arduino/bakelite.h index 84d5c4d..540b36f 100644 --- a/examples/arduino/bakelite.h +++ b/examples/arduino/bakelite.h @@ -13,6 +13,10 @@ #include #include +#ifdef __AVR__ +#include +#endif + namespace Bakelite { /* * diff --git a/examples/arduino/proto.py b/examples/arduino/proto.py index 085091e..67f2273 100644 --- a/examples/arduino/proto.py +++ b/examples/arduino/proto.py @@ -1,97 +1,89 @@ """Generated protocol definitions.""" +import struct as _struct import sys from dataclasses import dataclass from pathlib import Path -from typing import Any +from typing import ClassVar, Self _runtime_path = str(Path(__file__).parent) _added_to_path = _runtime_path not in sys.path if _added_to_path: sys.path.insert(0, _runtime_path) try: - from bakelite_runtime.runtime import ProtocolBase, Registry - from bakelite_runtime.serialization import struct - from bakelite_runtime.types import ( - ProtocolDescriptor, - ProtoMessageId, - ProtoStruct, - ProtoStructMember, - ProtoType, - ) + from bakelite_runtime.serialization import BakeliteEnum, SerializationError, Struct, bakelite_field + from bakelite_runtime.runtime import ProtocolBase finally: if _added_to_path: sys.path.remove(_runtime_path) -registry = Registry() - -@struct( - registry, - ProtoStruct( - name="TestMessage", - members=( - ProtoStructMember( - type=ProtoType(name="uint8", size=0), - name="a", - ), - ProtoStructMember( - type=ProtoType(name="int32", size=0), - name="b", - ), - ProtoStructMember( - type=ProtoType(name="bool", size=0), - name="status", - ), - ProtoStructMember( - type=ProtoType(name="string", size=16), - name="message", - ), - ), - ), -) @dataclass -class TestMessage: - a: int - b: int - status: bool - message: str +class TestMessage(Struct): + a: int = bakelite_field(type="uint8") + b: int = bakelite_field(type="int32") + status: bool = bakelite_field(type="bool") + message: str = bakelite_field(type="string", max_length=16) + + def pack(self) -> bytes: + _buf = bytearray() + _buf.extend(_struct.pack("=Bi?", self.a, self.b, self.status)) + _enc_message = self.message.encode("ascii") + if len(_enc_message) >= 16: + raise SerializationError("message exceeds 15 chars") + _buf.extend(_enc_message) + _buf.extend(b"\x00" * (16 - len(_enc_message))) + return bytes(_buf) + + @classmethod + def unpack(cls, _data: bytes | memoryview, offset: int = 0) -> tuple[Self, int]: + _o = offset + a, b, status = _struct.unpack_from("=Bi?", _data, _o) + _o += 6 + _raw_message = _data[_o:_o + 16] + _null_message = _raw_message.find(b"\x00") + message = bytes(_raw_message[:_null_message if _null_message >= 0 else 16]).decode("ascii") + _o += 16 + return cls(a, b, status, message), _o - offset -@struct( - registry, - ProtoStruct( - name="Ack", - members=( - ProtoStructMember( - type=ProtoType(name="uint8", size=0), - name="code", - ), - ProtoStructMember( - type=ProtoType(name="string", size=64), - name="message", - ), - ), - ), -) @dataclass -class Ack: - code: int - message: str +class Ack(Struct): + code: int = bakelite_field(type="uint8") + message: str = bakelite_field(type="string", max_length=64) + + def pack(self) -> bytes: + _buf = bytearray() + _buf.extend(_struct.pack("=B", self.code)) + _enc_message = self.message.encode("ascii") + if len(_enc_message) >= 64: + raise SerializationError("message exceeds 63 chars") + _buf.extend(_enc_message) + _buf.extend(b"\x00" * (64 - len(_enc_message))) + return bytes(_buf) + + @classmethod + def unpack(cls, _data: bytes | memoryview, offset: int = 0) -> tuple[Self, int]: + _o = offset + code, = _struct.unpack_from("=B", _data, _o) + _o += 1 + _raw_message = _data[_o:_o + 64] + _null_message = _raw_message.find(b"\x00") + message = bytes(_raw_message[:_null_message if _null_message >= 0 else 64]).decode("ascii") + _o += 64 + return cls(code, message), _o - offset class Protocol(ProtocolBase): - def __init__(self, **kwargs: Any) -> None: - super().__init__( - maxLength="70", - framing="COBS", - crc="CRC8", - registry=registry, - desc=ProtocolDescriptor( - message_ids=( - ProtoMessageId(name="TestMessage", number=1), - ProtoMessageId(name="Ack", number=2), - ), - ), - **kwargs, - ) + _message_types: ClassVar[dict[int, type[Struct]]] = { + 1: TestMessage, + 2: Ack, + } + _message_ids: ClassVar[dict[str, int]] = { + "TestMessage": 1, + "Ack": 2, + } + + def __init__(self, **kwargs) -> None: + kwargs.setdefault("crc", "CRC8") + super().__init__(**kwargs) From abfab0e03b29bf7705f61467da7c5efa1a06ea77 Mon Sep 17 00:00:00 2001 From: Emma Powers Date: Sat, 29 Nov 2025 19:04:05 -0500 Subject: [PATCH 4/6] Optimize cpptiny memory usage with single-buffer zero-copy design - Replace dual read/write buffers with single shared buffer (~50% memory reduction) - Add zero-copy message() and send() API for packed structs - Add --unpacked flag for platforms without unaligned access support - Add compile-time static_assert to catch platform misuse --- bakelite/generator/cli.py | 12 ++- bakelite/generator/cpptiny.py | 14 ++- bakelite/generator/runtimes/cpptiny/cobs.h | 96 +++++++++++------- bakelite/generator/templates/cpptiny.h.j2 | 80 ++++++++++++--- bakelite/tests/generator/Makefile | 8 +- bakelite/tests/generator/test_cli.py | 3 +- docs/cpptiny.md | 110 +++++++++++++-------- examples/arduino/arduino.ino | 46 +++++---- examples/arduino/bakelite.h | 95 ++++++++++-------- examples/arduino/proto.h | 88 +++++++++++++---- 10 files changed, 376 insertions(+), 176 deletions(-) diff --git a/bakelite/generator/cli.py b/bakelite/generator/cli.py index e0e1bad..7ab727f 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=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); From e9934043e50e434e931b7165835cd0e1076105d8 Mon Sep 17 00:00:00 2001 From: Emma Powers Date: Sat, 29 Nov 2025 21:55:27 -0500 Subject: [PATCH 5/6] Add ctiny (C99) code generator and TCP chat example - Add ctiny code generator for pure C99 embedded systems - Add TCP chat example demonstrating ctiny and cpptiny interoperability - CMake integration for automatic bakelite code generation - Add ctiny documentation --- .gitignore | 1 + bakelite/generator/cli.py | 12 +- bakelite/generator/ctiny.py | 282 +++++ bakelite/generator/runtimes/ctiny/cobs.h | 420 +++++++ bakelite/generator/runtimes/ctiny/crc.h | 193 +++ .../generator/runtimes/ctiny/serializer.h | 212 ++++ bakelite/generator/runtimes/ctiny/stream.h | 95 ++ bakelite/generator/runtimes/ctiny/types.h | 47 + .../generator/templates/ctiny-bakelite.h.j2 | 64 + bakelite/generator/templates/ctiny.h.j2 | 204 ++++ docs/ctiny.md | 149 +++ examples/ReadMe.md | 3 + examples/arduino/bakelite.h | 6 + examples/chat/chat.bakelite | 19 + examples/chat/cpptiny/CMakeLists.txt | 39 + examples/chat/cpptiny/Makefile | 21 + examples/chat/cpptiny/README.md | 51 + examples/chat/cpptiny/bakelite.h | 924 +++++++++++++++ examples/chat/cpptiny/client.cpp | 97 ++ examples/chat/cpptiny/proto.h | 215 ++++ examples/chat/cpptiny/server.cpp | 112 ++ examples/chat/ctiny/CMakeLists.txt | 39 + examples/chat/ctiny/Makefile | 21 + examples/chat/ctiny/README.md | 51 + examples/chat/ctiny/bakelite.h | 1031 +++++++++++++++++ examples/chat/ctiny/client.c | 98 ++ examples/chat/ctiny/proto.h | 216 ++++ examples/chat/ctiny/server.c | 113 ++ mkdocs.yml | 1 + 29 files changed, 4733 insertions(+), 3 deletions(-) create mode 100644 bakelite/generator/ctiny.py create mode 100644 bakelite/generator/runtimes/ctiny/cobs.h create mode 100644 bakelite/generator/runtimes/ctiny/crc.h create mode 100644 bakelite/generator/runtimes/ctiny/serializer.h create mode 100644 bakelite/generator/runtimes/ctiny/stream.h create mode 100644 bakelite/generator/runtimes/ctiny/types.h create mode 100644 bakelite/generator/templates/ctiny-bakelite.h.j2 create mode 100644 bakelite/generator/templates/ctiny.h.j2 create mode 100644 docs/ctiny.md create mode 100644 examples/chat/chat.bakelite create mode 100644 examples/chat/cpptiny/CMakeLists.txt create mode 100644 examples/chat/cpptiny/Makefile create mode 100644 examples/chat/cpptiny/README.md create mode 100644 examples/chat/cpptiny/bakelite.h create mode 100644 examples/chat/cpptiny/client.cpp create mode 100644 examples/chat/cpptiny/proto.h create mode 100644 examples/chat/cpptiny/server.cpp create mode 100644 examples/chat/ctiny/CMakeLists.txt create mode 100644 examples/chat/ctiny/Makefile create mode 100644 examples/chat/ctiny/README.md create mode 100644 examples/chat/ctiny/bakelite.h create mode 100644 examples/chat/ctiny/client.c create mode 100644 examples/chat/ctiny/proto.h create mode 100644 examples/chat/ctiny/server.c diff --git a/.gitignore b/.gitignore index 52a4267..5f11c46 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,7 @@ Icon* /build/ /dist/ *.spec +**/build/ # Sublime Text *.sublime-workspace diff --git a/bakelite/generator/cli.py b/bakelite/generator/cli.py index 7ab727f..9e2a25d 100644 --- a/bakelite/generator/cli.py +++ b/bakelite/generator/cli.py @@ -5,7 +5,7 @@ import click -from bakelite.generator import cpptiny, parse, python +from bakelite.generator import cpptiny, ctiny, parse, python @click.group() @@ -14,7 +14,7 @@ def cli() -> None: @cli.command() -@click.option("--language", "-l", required=True, help="Target language (python, cpptiny)") +@click.option("--language", "-l", required=True, help="Target language (python, cpptiny, ctiny)") @click.option("--input", "-i", "input_file", required=True, help="Input protocol file") @click.option("--output", "-o", "output_file", required=True, help="Output file") @click.option( @@ -46,6 +46,8 @@ def gen( generated_file = python.render(*proto_def, runtime_import=import_path) elif language == "cpptiny": generated_file = cpptiny.render(*proto_def, unpacked=unpacked) + elif language == "ctiny": + generated_file = ctiny.render(*proto_def, unpacked=unpacked) else: print(f"Unknown language: {language}") sys.exit(1) @@ -55,7 +57,7 @@ def gen( @cli.command() -@click.option("--language", "-l", required=True, help="Target language (python, cpptiny)") +@click.option("--language", "-l", required=True, help="Target language (python, cpptiny, ctiny)") @click.option("--output", "-o", "output_path", default=".", help="Output directory") @click.option("--name", default="bakelite_runtime", help="Runtime folder name (python only)") def runtime(language: str, output_path: str, name: str) -> None: @@ -64,6 +66,10 @@ def runtime(language: str, output_path: str, name: str) -> None: generated_file = cpptiny.runtime() with open(output_path, "w", encoding="utf-8") as f: f.write(generated_file) + elif language == "ctiny": + generated_file = ctiny.runtime() + with open(output_path, "w", encoding="utf-8") as f: + f.write(generated_file) elif language == "python": runtime_dir = Path(output_path) / name runtime_dir.mkdir(parents=True, exist_ok=True) diff --git a/bakelite/generator/ctiny.py b/bakelite/generator/ctiny.py new file mode 100644 index 0000000..54f014e --- /dev/null +++ b/bakelite/generator/ctiny.py @@ -0,0 +1,282 @@ +"""C (tiny) code generator for bakelite protocols.""" + +import os +from copy import copy + +from jinja2 import Environment, PackageLoader + +from .types import Protocol, ProtoEnum, ProtoStruct, ProtoStructMember, ProtoType + +env = Environment( + loader=PackageLoader("bakelite.generator", "templates"), + trim_blocks=True, + lstrip_blocks=True, + keep_trailing_newline=True, + line_comment_prefix="%%", + line_statement_prefix="%", +) + +template = env.get_template("ctiny.h.j2") + +PRIMITIVE_TYPE_MAP = { + "bool": "bool", + "int8": "int8_t", + "int16": "int16_t", + "int32": "int32_t", + "int64": "int64_t", + "uint8": "uint8_t", + "uint16": "uint16_t", + "uint32": "uint32_t", + "uint64": "uint64_t", + "float32": "float", + "float64": "double", + "bytes": "uint8_t", + "string": "char", +} + +# Map primitive type names to serializer function suffixes +SERIALIZER_SUFFIX_MAP = { + "bool": "bool", + "int8": "int8", + "int16": "int16", + "int32": "int32", + "int64": "int64", + "uint8": "uint8", + "uint16": "uint16", + "uint32": "uint32", + "uint64": "uint64", + "float32": "float32", + "float64": "float64", +} + + +def _map_type(t: ProtoType) -> str: + return PRIMITIVE_TYPE_MAP.get(t.name, t.name) + + +def _map_type_member(member: ProtoStructMember) -> str: + type_name = _map_type(member.type) + + # Variable-length bytes array + if member.type.name == "bytes" and member.type.size == 0 and member.array_size == 0: + return "Bakelite_SizedArray" # Array of SizedArrays not supported yet + if member.type.name == "bytes" and member.type.size == 0: + return "Bakelite_SizedArray" + # Variable-length string array + if member.type.name == "string" and member.type.size == 0 and member.array_size == 0: + return "Bakelite_SizedArray" # Array of strings not supported yet + if member.type.name == "string" and member.type.size == 0: + return "char*" + # Variable-length typed array + if member.array_size == 0: + return "Bakelite_SizedArray" + return type_name + + +def _size_postfix(member: ProtoStructMember) -> str: + if member.type.name in {"bytes", "string"}: + if member.type.size == 0: + return "" + return f"[{member.type.size}]" + return "" + + +def _array_postfix(member: ProtoStructMember) -> str: + if member.array_size is None or member.array_size == 0: + return "" + return f"[{member.array_size}]" + + +def overhead(size: int, crc_size: int) -> int: + """Calculate COBS overhead for a message size.""" + cobs_overhead = int((size + 253) / 254) + return cobs_overhead + crc_size + 1 + + +def render( + enums: list[ProtoEnum], + structs: list[ProtoStruct], + proto: Protocol | None, + comments: list[str], + *, + unpacked: bool = False, +) -> str: + """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. + 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} + + def _write_type(member: ProtoStructMember) -> str: + if member.array_size is not None: + # Array handling + if member.array_size > 0: + # Fixed-size array + tmp_member = copy(member) + tmp_member.array_size = None + tmp_member.name = f"self->{member.name}[i]" + inner_write = _write_type(tmp_member) + return f"""for (uint32_t i = 0; i < {member.array_size}; i++) {{ + if ((rcode = {inner_write}) != 0) return rcode; + }} + rcode = 0""" + # Variable-length array + tmp_member = copy(member) + tmp_member.array_size = None + tmp_member.name = f"(({_map_type(member.type)}*)self->{member.name}.data)[i]" + inner_write = _write_type(tmp_member) + return f"""bakelite_write_uint8(buf, self->{member.name}.size); + for (uint8_t i = 0; i < self->{member.name}.size; i++) {{ + if ((rcode = {inner_write}) != 0) return rcode; + }} + rcode = 0""" + + # Use self-> prefix if not already present (for nested arrays, name includes self->) + name = member.name if member.name.startswith("self->") else f"self->{member.name}" + + if member.type.name in enums_types: + underlying = SERIALIZER_SUFFIX_MAP[enums_types[member.type.name].type.name] + return f"bakelite_write_{underlying}(buf, ({_map_type(enums_types[member.type.name].type)}){name})" + if member.type.name in structs_types: + return f"{member.type.name}_pack(&{name}, buf)" + if member.type.name in SERIALIZER_SUFFIX_MAP: + suffix = SERIALIZER_SUFFIX_MAP[member.type.name] + return f"bakelite_write_{suffix}(buf, {name})" + if member.type.name == "bytes": + if member.type.size != 0: + return f"bakelite_write_bytes_fixed(buf, {name}, {member.type.size})" + return f"bakelite_write_bytes(buf, &{name})" + if member.type.name == "string": + if member.type.size != 0: + return f"bakelite_write_string_fixed(buf, {name}, {member.type.size})" + return f"bakelite_write_string(buf, {name})" + raise RuntimeError(f"Unknown type {member.type.name}") + + def _read_type(member: ProtoStructMember) -> str: + if member.array_size is not None: + # Array handling + if member.array_size > 0: + # Fixed-size array + tmp_member = copy(member) + tmp_member.array_size = None + tmp_member.name = f"self->{member.name}[i]" + inner_read = _read_type(tmp_member) + return f"""for (uint32_t i = 0; i < {member.array_size}; i++) {{ + if ((rcode = {inner_read}) != 0) return rcode; + }} + rcode = 0""" + # Variable-length array + tmp_member = copy(member) + tmp_member.array_size = None + tmp_member.name = f"(({_map_type(member.type)}*)self->{member.name}.data)[i]" + inner_read = _read_type(tmp_member) + return f"""{{ + uint8_t arr_size; + if ((rcode = bakelite_read_uint8(buf, &arr_size)) != 0) return rcode; + self->{member.name}.size = arr_size; + self->{member.name}.data = bakelite_buffer_alloc(buf, arr_size * sizeof({_map_type(member.type)})); + if (self->{member.name}.data == NULL) return BAKELITE_ERR_ALLOC; + for (uint8_t i = 0; i < arr_size; i++) {{ + if ((rcode = {inner_read}) != 0) return rcode; + }} + }} + rcode = 0""" + + # Use self-> prefix if not already present (for nested arrays, name includes self->) + name = member.name if member.name.startswith("self->") else f"self->{member.name}" + + if member.type.name in enums_types: + underlying = SERIALIZER_SUFFIX_MAP[enums_types[member.type.name].type.name] + underlying_type = _map_type(enums_types[member.type.name].type) + return f"bakelite_read_{underlying}(buf, ({underlying_type}*)&{name})" + if member.type.name in structs_types: + return f"{member.type.name}_unpack(&{name}, buf)" + if member.type.name in SERIALIZER_SUFFIX_MAP: + suffix = SERIALIZER_SUFFIX_MAP[member.type.name] + return f"bakelite_read_{suffix}(buf, &{name})" + if member.type.name == "bytes": + if member.type.size != 0: + return f"bakelite_read_bytes_fixed(buf, {name}, {member.type.size})" + return f"bakelite_read_bytes(buf, &{name})" + if member.type.name == "string": + if member.type.size != 0: + return f"bakelite_read_string_fixed(buf, {name}, {member.type.size})" + return f"bakelite_read_string(buf, &{name})" + raise RuntimeError(f"Unknown type {member.type.name}") + + message_ids: list[tuple[str, int]] = [] + crc_type = "BAKELITE_CRC_NONE" + crc_size = 0 + max_length = 0 + + if proto is not None: + message_ids = [(msg.name, msg.number) for msg in proto.message_ids] + options = {option.name: option.value for option in proto.options} + crc = options.get("crc", "none").lower() + framing = options.get("framing", "").lower() + max_length_opt = options.get("maxLength") + + if framing == "": + raise RuntimeError("A frame type must be specified") + + if max_length_opt is None: + raise RuntimeError("maxLength must be specified") + + if crc == "none": + crc_type = "BAKELITE_CRC_NONE" + crc_size = 0 + elif crc == "crc8": + crc_type = "BAKELITE_CRC_8" + crc_size = 1 + elif crc == "crc16": + crc_type = "BAKELITE_CRC_16" + crc_size = 2 + elif crc == "crc32": + crc_type = "BAKELITE_CRC_32" + crc_size = 4 + else: + raise RuntimeError(f"Unknown CRC type {crc}") + + max_length = int(max_length_opt) + + if framing != "cobs": + raise RuntimeError(f"Unknown framing type {framing}") + + return template.render( + enums=enums, + structs=structs, + proto=proto, + comments=comments, + map_type=_map_type, + map_type_member=_map_type_member, + array_postfix=_array_postfix, + size_postfix=_size_postfix, + write_type=_write_type, + read_type=_read_type, + crc_type=crc_type, + crc_size=crc_size, + max_length=max_length, + message_ids=message_ids, + unpacked=unpacked, + ) + + +def runtime() -> str: + """Generate the C runtime support code.""" + + def include(filename: str) -> str: + with open( + os.path.join(os.path.dirname(__file__), "runtimes", "ctiny", filename), + encoding="utf-8", + ) as f: + return f.read() + + runtime_template = env.get_template("ctiny-bakelite.h.j2") + return runtime_template.render(include=include) diff --git a/bakelite/generator/runtimes/ctiny/cobs.h b/bakelite/generator/runtimes/ctiny/cobs.h new file mode 100644 index 0000000..3635e97 --- /dev/null +++ b/bakelite/generator/runtimes/ctiny/cobs.h @@ -0,0 +1,420 @@ +/* + * COBS encode/decode functions are Copyright (c) 2010 Craig McQueen + * Licensed under the MIT license (see end of file). + * Source: https://github.com/cmcqueen/cobs-c + */ + +/* COBS buffer size calculations */ +#define BAKELITE_COBS_ENCODE_DST_BUF_LEN_MAX(SRC_LEN) ((SRC_LEN) + (((SRC_LEN) + 253u) / 254u)) +#define BAKELITE_COBS_DECODE_DST_BUF_LEN_MAX(SRC_LEN) (((SRC_LEN) == 0) ? 0u : ((SRC_LEN) - 1u)) +#define BAKELITE_COBS_OVERHEAD(BUF_SIZE) (((BUF_SIZE) + 253u) / 254u) + +/* Calculate total buffer size needed for framer */ +#define BAKELITE_FRAMER_BUFFER_SIZE(MAX_MSG_SIZE, CRC_SIZE) \ + (BAKELITE_COBS_OVERHEAD((MAX_MSG_SIZE) + (CRC_SIZE)) + (MAX_MSG_SIZE) + (CRC_SIZE) + 1) + +/* Message offset in buffer (after COBS overhead) */ +#define BAKELITE_FRAMER_MESSAGE_OFFSET(MAX_MSG_SIZE, CRC_SIZE) \ + BAKELITE_COBS_OVERHEAD((MAX_MSG_SIZE) + (CRC_SIZE)) + +/* Decode state enum */ +typedef enum { + BAKELITE_DECODE_OK = 0, + BAKELITE_DECODE_NOT_READY, + BAKELITE_DECODE_FAILURE, + BAKELITE_DECODE_CRC_FAILURE, + BAKELITE_DECODE_BUFFER_OVERRUN +} Bakelite_DecodeState; + +/* COBS encode/decode status */ +typedef enum { + COBS_ENCODE_OK = 0x00, + COBS_ENCODE_NULL_POINTER = 0x01, + COBS_ENCODE_OUT_BUFFER_OVERFLOW = 0x02 +} Bakelite_CobsEncodeStatus; + +typedef enum { + COBS_DECODE_OK = 0x00, + COBS_DECODE_NULL_POINTER = 0x01, + COBS_DECODE_OUT_BUFFER_OVERFLOW = 0x02, + COBS_DECODE_ZERO_BYTE_IN_INPUT = 0x04, + COBS_DECODE_INPUT_TOO_SHORT = 0x08 +} Bakelite_CobsDecodeStatus; + +/* COBS encode/decode results */ +typedef struct { + size_t out_len; + int status; +} Bakelite_CobsEncodeResult; + +typedef struct { + size_t out_len; + int status; +} Bakelite_CobsDecodeResult; + +/* Framer result */ +typedef struct { + int status; + size_t length; + uint8_t *data; +} Bakelite_FramerResult; + +/* Decode result */ +typedef struct { + Bakelite_DecodeState status; + size_t length; + uint8_t *data; +} Bakelite_DecodeResult; + +/* CRC type enum */ +typedef enum { + BAKELITE_CRC_NONE = 0, + BAKELITE_CRC_8, + BAKELITE_CRC_16, + BAKELITE_CRC_32 +} Bakelite_CrcType; + +/* COBS Framer structure */ +typedef struct { + uint8_t *buffer; + size_t buffer_size; + size_t max_message_size; + size_t message_offset; + size_t crc_size; + Bakelite_CrcType crc_type; + uint8_t *read_pos; +} Bakelite_CobsFramer; + +/* Forward declarations */ +static Bakelite_CobsEncodeResult bakelite_cobs_encode(void *dst_buf_ptr, size_t dst_buf_len, + const void *src_ptr, size_t src_len); +static Bakelite_CobsDecodeResult bakelite_cobs_decode(void *dst_buf_ptr, size_t dst_buf_len, + const void *src_ptr, size_t src_len); + +/* Get CRC size for a CRC type */ +static inline size_t bakelite_crc_size(Bakelite_CrcType crc_type) { + switch (crc_type) { + case BAKELITE_CRC_8: return 1; + case BAKELITE_CRC_16: return 2; + case BAKELITE_CRC_32: return 4; + default: return 0; + } +} + +/* Initialize framer with user-provided buffer */ +static inline void bakelite_framer_init(Bakelite_CobsFramer *framer, + uint8_t *buffer, size_t buffer_size, + size_t max_message_size, + Bakelite_CrcType crc_type) { + framer->buffer = buffer; + framer->buffer_size = buffer_size; + framer->max_message_size = max_message_size; + framer->crc_type = crc_type; + framer->crc_size = bakelite_crc_size(crc_type); + framer->message_offset = BAKELITE_COBS_OVERHEAD(max_message_size + framer->crc_size); + framer->read_pos = buffer; +} + +/* Get pointer to message area in buffer */ +static inline uint8_t *bakelite_framer_buffer(Bakelite_CobsFramer *framer) { + return framer->buffer + framer->message_offset; +} + +/* Get usable buffer size for messages */ +static inline size_t bakelite_framer_buffer_size(Bakelite_CobsFramer *framer) { + return framer->max_message_size + 1; /* +1 for type byte */ +} + +/* Calculate and append CRC to data */ +static inline void bakelite_framer_append_crc(Bakelite_CobsFramer *framer, uint8_t *data, size_t length) { + switch (framer->crc_type) { + case BAKELITE_CRC_8: { + uint8_t crc = bakelite_crc8(data, length, 0); + memcpy(data + length, &crc, sizeof(crc)); + break; + } + case BAKELITE_CRC_16: { + uint16_t crc = bakelite_crc16(data, length, 0); + memcpy(data + length, &crc, sizeof(crc)); + break; + } + case BAKELITE_CRC_32: { + uint32_t crc = bakelite_crc32(data, length, 0); + memcpy(data + length, &crc, sizeof(crc)); + break; + } + default: + break; + } +} + +/* Verify CRC of data */ +static inline bool bakelite_framer_verify_crc(Bakelite_CobsFramer *framer, const uint8_t *data, size_t length) { + switch (framer->crc_type) { + case BAKELITE_CRC_8: { + uint8_t expected; + memcpy(&expected, data + length, sizeof(expected)); + return bakelite_crc8(data, length, 0) == expected; + } + case BAKELITE_CRC_16: { + uint16_t expected; + memcpy(&expected, data + length, sizeof(expected)); + return bakelite_crc16(data, length, 0) == expected; + } + case BAKELITE_CRC_32: { + uint32_t expected; + memcpy(&expected, data + length, sizeof(expected)); + return bakelite_crc32(data, length, 0) == expected; + } + default: + return true; + } +} + +/* Encode frame with data copy */ +static inline Bakelite_FramerResult bakelite_framer_encode_copy(Bakelite_CobsFramer *framer, + const uint8_t *data, size_t length) { + uint8_t *msg_start = framer->buffer + framer->message_offset; + memcpy(msg_start, data, length); + + if (framer->crc_size > 0) { + bakelite_framer_append_crc(framer, msg_start, length); + } + + Bakelite_CobsEncodeResult result = bakelite_cobs_encode( + framer->buffer, framer->buffer_size, + msg_start, length + framer->crc_size); + + if (result.status != 0) { + return (Bakelite_FramerResult){ 1, 0, NULL }; + } + + framer->buffer[result.out_len] = 0; /* Null terminator */ + return (Bakelite_FramerResult){ 0, result.out_len + 1, framer->buffer }; +} + +/* Encode frame (data already in buffer at message offset) */ +static inline Bakelite_FramerResult bakelite_framer_encode(Bakelite_CobsFramer *framer, size_t length) { + uint8_t *msg_start = framer->buffer + framer->message_offset; + + if (framer->crc_size > 0) { + bakelite_framer_append_crc(framer, msg_start, length); + } + + Bakelite_CobsEncodeResult result = bakelite_cobs_encode( + framer->buffer, framer->buffer_size, + msg_start, length + framer->crc_size); + + if (result.status != 0) { + return (Bakelite_FramerResult){ 1, 0, NULL }; + } + + framer->buffer[result.out_len] = 0; /* Null terminator */ + return (Bakelite_FramerResult){ 0, result.out_len + 1, framer->buffer }; +} + +/* Decode a complete frame */ +static inline Bakelite_DecodeResult bakelite_framer_decode_frame(Bakelite_CobsFramer *framer, size_t length) { + if (length == 1) { + return (Bakelite_DecodeResult){ BAKELITE_DECODE_FAILURE, 0, NULL }; + } + + length--; /* Discard null byte */ + + /* Decode in-place at buffer start */ + Bakelite_CobsDecodeResult result = bakelite_cobs_decode( + framer->buffer, framer->buffer_size, + framer->buffer, length); + + if (result.status != 0) { + return (Bakelite_DecodeResult){ BAKELITE_DECODE_FAILURE, 0, NULL }; + } + + /* Length of decoded data without CRC */ + length = result.out_len - framer->crc_size; + + if (framer->crc_size > 0) { + if (!bakelite_framer_verify_crc(framer, framer->buffer, length)) { + return (Bakelite_DecodeResult){ BAKELITE_DECODE_CRC_FAILURE, 0, NULL }; + } + } + + /* Move decoded data to message offset position for consistent buffer layout */ + if (framer->message_offset > 0) { + memmove(framer->buffer + framer->message_offset, framer->buffer, length); + } + + return (Bakelite_DecodeResult){ + BAKELITE_DECODE_OK, + length, + framer->buffer + framer->message_offset + }; +} + +/* Process a single byte from the stream */ +static inline Bakelite_DecodeResult bakelite_framer_read_byte(Bakelite_CobsFramer *framer, uint8_t byte) { + *framer->read_pos = byte; + size_t length = (size_t)(framer->read_pos - framer->buffer) + 1; + + if (byte == 0) { + framer->read_pos = framer->buffer; + return bakelite_framer_decode_frame(framer, length); + } else if (length == framer->buffer_size) { + framer->read_pos = framer->buffer; + return (Bakelite_DecodeResult){ BAKELITE_DECODE_BUFFER_OVERRUN, 0, NULL }; + } + + framer->read_pos++; + return (Bakelite_DecodeResult){ BAKELITE_DECODE_NOT_READY, 0, NULL }; +} + +/* + * COBS encode/decode implementation + * Copyright (c) 2010 Craig McQueen, MIT License + */ +static Bakelite_CobsEncodeResult bakelite_cobs_encode(void *dst_buf_ptr, size_t dst_buf_len, + const void *src_ptr, size_t src_len) { + Bakelite_CobsEncodeResult result = {0, COBS_ENCODE_OK}; + const uint8_t *src_read_ptr = (const uint8_t *)src_ptr; + const uint8_t *src_end_ptr = src_read_ptr + src_len; + uint8_t *dst_buf_start_ptr = (uint8_t *)dst_buf_ptr; + uint8_t *dst_buf_end_ptr = dst_buf_start_ptr + dst_buf_len; + uint8_t *dst_code_write_ptr = (uint8_t *)dst_buf_ptr; + uint8_t *dst_write_ptr = dst_code_write_ptr + 1; + uint8_t src_byte = 0; + uint8_t search_len = 1; + + if ((dst_buf_ptr == NULL) || (src_ptr == NULL)) { + result.status = COBS_ENCODE_NULL_POINTER; + return result; + } + + if (src_len != 0) { + for (;;) { + if (dst_write_ptr >= dst_buf_end_ptr) { + result.status |= COBS_ENCODE_OUT_BUFFER_OVERFLOW; + break; + } + + src_byte = *src_read_ptr++; + if (src_byte == 0) { + *dst_code_write_ptr = search_len; + dst_code_write_ptr = dst_write_ptr++; + search_len = 1; + if (src_read_ptr >= src_end_ptr) { + break; + } + } else { + *dst_write_ptr++ = src_byte; + search_len++; + if (src_read_ptr >= src_end_ptr) { + break; + } + if (search_len == 0xFF) { + *dst_code_write_ptr = search_len; + dst_code_write_ptr = dst_write_ptr++; + search_len = 1; + } + } + } + } + + if (dst_code_write_ptr >= dst_buf_end_ptr) { + result.status |= COBS_ENCODE_OUT_BUFFER_OVERFLOW; + dst_write_ptr = dst_buf_end_ptr; + } else { + *dst_code_write_ptr = search_len; + } + + result.out_len = (size_t)(dst_write_ptr - dst_buf_start_ptr); + return result; +} + +static Bakelite_CobsDecodeResult bakelite_cobs_decode(void *dst_buf_ptr, size_t dst_buf_len, + const void *src_ptr, size_t src_len) { + Bakelite_CobsDecodeResult result = {0, COBS_DECODE_OK}; + const uint8_t *src_read_ptr = (const uint8_t *)src_ptr; + const uint8_t *src_end_ptr = src_read_ptr + src_len; + uint8_t *dst_buf_start_ptr = (uint8_t *)dst_buf_ptr; + uint8_t *dst_buf_end_ptr = dst_buf_start_ptr + dst_buf_len; + uint8_t *dst_write_ptr = (uint8_t *)dst_buf_ptr; + size_t remaining_bytes; + uint8_t src_byte; + uint8_t i; + uint8_t len_code; + + if ((dst_buf_ptr == NULL) || (src_ptr == NULL)) { + result.status = COBS_DECODE_NULL_POINTER; + return result; + } + + if (src_len != 0) { + for (;;) { + len_code = *src_read_ptr++; + if (len_code == 0) { + result.status |= COBS_DECODE_ZERO_BYTE_IN_INPUT; + break; + } + len_code--; + + remaining_bytes = (size_t)(src_end_ptr - src_read_ptr); + if (len_code > remaining_bytes) { + result.status |= COBS_DECODE_INPUT_TOO_SHORT; + len_code = (uint8_t)remaining_bytes; + } + + remaining_bytes = (size_t)(dst_buf_end_ptr - dst_write_ptr); + if (len_code > remaining_bytes) { + result.status |= COBS_DECODE_OUT_BUFFER_OVERFLOW; + len_code = (uint8_t)remaining_bytes; + } + + for (i = len_code; i != 0; i--) { + src_byte = *src_read_ptr++; + if (src_byte == 0) { + result.status |= COBS_DECODE_ZERO_BYTE_IN_INPUT; + } + *dst_write_ptr++ = src_byte; + } + + if (src_read_ptr >= src_end_ptr) { + break; + } + + if (len_code != 0xFE) { + if (dst_write_ptr >= dst_buf_end_ptr) { + result.status |= COBS_DECODE_OUT_BUFFER_OVERFLOW; + break; + } + *dst_write_ptr++ = 0; + } + } + } + + result.out_len = (size_t)(dst_write_ptr - dst_buf_start_ptr); + return result; +} + +/* + * MIT License for COBS encode/decode functions: + * + * Copyright (c) 2010 Craig McQueen + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ diff --git a/bakelite/generator/runtimes/ctiny/crc.h b/bakelite/generator/runtimes/ctiny/crc.h new file mode 100644 index 0000000..ce04a0d --- /dev/null +++ b/bakelite/generator/runtimes/ctiny/crc.h @@ -0,0 +1,193 @@ +/* + * The CRC lookup tables are stored as const variables. On many platforms, + * const variables are stored in flash memory. On AVR though, they are + * loaded into RAM on startup. A special PROGMEM macro is available on AVRs + * to indicate constants should be stored in program memory (flash). + * So if this macro is available, use it and assume we're on an AVR. + */ +#ifdef PROGMEM + #define BAKELITE_CONST PROGMEM + #define BAKELITE_CONST_8(x) pgm_read_byte(&(x)) + #define BAKELITE_CONST_16(x) pgm_read_word(&(x)) + #define BAKELITE_CONST_32(x) ((uint32_t)pgm_read_dword((char *)&(x) + 2) << 16 | pgm_read_dword(&(x))) +#else + #define BAKELITE_CONST + #define BAKELITE_CONST_8(x) (x) + #define BAKELITE_CONST_16(x) (x) + #define BAKELITE_CONST_32(x) (x) +#endif + +/* CRC-8 polynomial: 0x107 */ +static inline uint8_t bakelite_crc8(const uint8_t *data, size_t len, uint8_t crc) { + static const uint8_t table[256] BAKELITE_CONST = { + 0x00U,0x07U,0x0EU,0x09U,0x1CU,0x1BU,0x12U,0x15U, + 0x38U,0x3FU,0x36U,0x31U,0x24U,0x23U,0x2AU,0x2DU, + 0x70U,0x77U,0x7EU,0x79U,0x6CU,0x6BU,0x62U,0x65U, + 0x48U,0x4FU,0x46U,0x41U,0x54U,0x53U,0x5AU,0x5DU, + 0xE0U,0xE7U,0xEEU,0xE9U,0xFCU,0xFBU,0xF2U,0xF5U, + 0xD8U,0xDFU,0xD6U,0xD1U,0xC4U,0xC3U,0xCAU,0xCDU, + 0x90U,0x97U,0x9EU,0x99U,0x8CU,0x8BU,0x82U,0x85U, + 0xA8U,0xAFU,0xA6U,0xA1U,0xB4U,0xB3U,0xBAU,0xBDU, + 0xC7U,0xC0U,0xC9U,0xCEU,0xDBU,0xDCU,0xD5U,0xD2U, + 0xFFU,0xF8U,0xF1U,0xF6U,0xE3U,0xE4U,0xEDU,0xEAU, + 0xB7U,0xB0U,0xB9U,0xBEU,0xABU,0xACU,0xA5U,0xA2U, + 0x8FU,0x88U,0x81U,0x86U,0x93U,0x94U,0x9DU,0x9AU, + 0x27U,0x20U,0x29U,0x2EU,0x3BU,0x3CU,0x35U,0x32U, + 0x1FU,0x18U,0x11U,0x16U,0x03U,0x04U,0x0DU,0x0AU, + 0x57U,0x50U,0x59U,0x5EU,0x4BU,0x4CU,0x45U,0x42U, + 0x6FU,0x68U,0x61U,0x66U,0x73U,0x74U,0x7DU,0x7AU, + 0x89U,0x8EU,0x87U,0x80U,0x95U,0x92U,0x9BU,0x9CU, + 0xB1U,0xB6U,0xBFU,0xB8U,0xADU,0xAAU,0xA3U,0xA4U, + 0xF9U,0xFEU,0xF7U,0xF0U,0xE5U,0xE2U,0xEBU,0xECU, + 0xC1U,0xC6U,0xCFU,0xC8U,0xDDU,0xDAU,0xD3U,0xD4U, + 0x69U,0x6EU,0x67U,0x60U,0x75U,0x72U,0x7BU,0x7CU, + 0x51U,0x56U,0x5FU,0x58U,0x4DU,0x4AU,0x43U,0x44U, + 0x19U,0x1EU,0x17U,0x10U,0x05U,0x02U,0x0BU,0x0CU, + 0x21U,0x26U,0x2FU,0x28U,0x3DU,0x3AU,0x33U,0x34U, + 0x4EU,0x49U,0x40U,0x47U,0x52U,0x55U,0x5CU,0x5BU, + 0x76U,0x71U,0x78U,0x7FU,0x6AU,0x6DU,0x64U,0x63U, + 0x3EU,0x39U,0x30U,0x37U,0x22U,0x25U,0x2CU,0x2BU, + 0x06U,0x01U,0x08U,0x0FU,0x1AU,0x1DU,0x14U,0x13U, + 0xAEU,0xA9U,0xA0U,0xA7U,0xB2U,0xB5U,0xBCU,0xBBU, + 0x96U,0x91U,0x98U,0x9FU,0x8AU,0x8DU,0x84U,0x83U, + 0xDEU,0xD9U,0xD0U,0xD7U,0xC2U,0xC5U,0xCCU,0xCBU, + 0xE6U,0xE1U,0xE8U,0xEFU,0xFAU,0xFDU,0xF4U,0xF3U, + }; + + while (len > 0) { + crc = BAKELITE_CONST_8(table[*data ^ crc]); + data++; + len--; + } + return crc; +} + +/* CRC-16 polynomial: 0x18005, bit reverse algorithm */ +static inline uint16_t bakelite_crc16(const uint8_t *data, size_t len, uint16_t crc) { + static const uint16_t table[256] BAKELITE_CONST = { + 0x0000U,0xC0C1U,0xC181U,0x0140U,0xC301U,0x03C0U,0x0280U,0xC241U, + 0xC601U,0x06C0U,0x0780U,0xC741U,0x0500U,0xC5C1U,0xC481U,0x0440U, + 0xCC01U,0x0CC0U,0x0D80U,0xCD41U,0x0F00U,0xCFC1U,0xCE81U,0x0E40U, + 0x0A00U,0xCAC1U,0xCB81U,0x0B40U,0xC901U,0x09C0U,0x0880U,0xC841U, + 0xD801U,0x18C0U,0x1980U,0xD941U,0x1B00U,0xDBC1U,0xDA81U,0x1A40U, + 0x1E00U,0xDEC1U,0xDF81U,0x1F40U,0xDD01U,0x1DC0U,0x1C80U,0xDC41U, + 0x1400U,0xD4C1U,0xD581U,0x1540U,0xD701U,0x17C0U,0x1680U,0xD641U, + 0xD201U,0x12C0U,0x1380U,0xD341U,0x1100U,0xD1C1U,0xD081U,0x1040U, + 0xF001U,0x30C0U,0x3180U,0xF141U,0x3300U,0xF3C1U,0xF281U,0x3240U, + 0x3600U,0xF6C1U,0xF781U,0x3740U,0xF501U,0x35C0U,0x3480U,0xF441U, + 0x3C00U,0xFCC1U,0xFD81U,0x3D40U,0xFF01U,0x3FC0U,0x3E80U,0xFE41U, + 0xFA01U,0x3AC0U,0x3B80U,0xFB41U,0x3900U,0xF9C1U,0xF881U,0x3840U, + 0x2800U,0xE8C1U,0xE981U,0x2940U,0xEB01U,0x2BC0U,0x2A80U,0xEA41U, + 0xEE01U,0x2EC0U,0x2F80U,0xEF41U,0x2D00U,0xEDC1U,0xEC81U,0x2C40U, + 0xE401U,0x24C0U,0x2580U,0xE541U,0x2700U,0xE7C1U,0xE681U,0x2640U, + 0x2200U,0xE2C1U,0xE381U,0x2340U,0xE101U,0x21C0U,0x2080U,0xE041U, + 0xA001U,0x60C0U,0x6180U,0xA141U,0x6300U,0xA3C1U,0xA281U,0x6240U, + 0x6600U,0xA6C1U,0xA781U,0x6740U,0xA501U,0x65C0U,0x6480U,0xA441U, + 0x6C00U,0xACC1U,0xAD81U,0x6D40U,0xAF01U,0x6FC0U,0x6E80U,0xAE41U, + 0xAA01U,0x6AC0U,0x6B80U,0xAB41U,0x6900U,0xA9C1U,0xA881U,0x6840U, + 0x7800U,0xB8C1U,0xB981U,0x7940U,0xBB01U,0x7BC0U,0x7A80U,0xBA41U, + 0xBE01U,0x7EC0U,0x7F80U,0xBF41U,0x7D00U,0xBDC1U,0xBC81U,0x7C40U, + 0xB401U,0x74C0U,0x7580U,0xB541U,0x7700U,0xB7C1U,0xB681U,0x7640U, + 0x7200U,0xB2C1U,0xB381U,0x7340U,0xB101U,0x71C0U,0x7080U,0xB041U, + 0x5000U,0x90C1U,0x9181U,0x5140U,0x9301U,0x53C0U,0x5280U,0x9241U, + 0x9601U,0x56C0U,0x5780U,0x9741U,0x5500U,0x95C1U,0x9481U,0x5440U, + 0x9C01U,0x5CC0U,0x5D80U,0x9D41U,0x5F00U,0x9FC1U,0x9E81U,0x5E40U, + 0x5A00U,0x9AC1U,0x9B81U,0x5B40U,0x9901U,0x59C0U,0x5880U,0x9841U, + 0x8801U,0x48C0U,0x4980U,0x8941U,0x4B00U,0x8BC1U,0x8A81U,0x4A40U, + 0x4E00U,0x8EC1U,0x8F81U,0x4F40U,0x8D01U,0x4DC0U,0x4C80U,0x8C41U, + 0x4400U,0x84C1U,0x8581U,0x4540U,0x8701U,0x47C0U,0x4680U,0x8641U, + 0x8201U,0x42C0U,0x4380U,0x8341U,0x4100U,0x81C1U,0x8081U,0x4040U, + }; + + while (len > 0) { + crc = BAKELITE_CONST_16(table[*data ^ (uint8_t)crc]) ^ (crc >> 8); + data++; + len--; + } + return crc; +} + +/* CRC-32 polynomial: 0x104C11DB7, bit reverse algorithm */ +static inline uint32_t bakelite_crc32(const uint8_t *data, size_t len, uint32_t crc) { + static const uint32_t table[256] BAKELITE_CONST = { + 0x00000000U,0x77073096U,0xEE0E612CU,0x990951BAU, + 0x076DC419U,0x706AF48FU,0xE963A535U,0x9E6495A3U, + 0x0EDB8832U,0x79DCB8A4U,0xE0D5E91EU,0x97D2D988U, + 0x09B64C2BU,0x7EB17CBDU,0xE7B82D07U,0x90BF1D91U, + 0x1DB71064U,0x6AB020F2U,0xF3B97148U,0x84BE41DEU, + 0x1ADAD47DU,0x6DDDE4EBU,0xF4D4B551U,0x83D385C7U, + 0x136C9856U,0x646BA8C0U,0xFD62F97AU,0x8A65C9ECU, + 0x14015C4FU,0x63066CD9U,0xFA0F3D63U,0x8D080DF5U, + 0x3B6E20C8U,0x4C69105EU,0xD56041E4U,0xA2677172U, + 0x3C03E4D1U,0x4B04D447U,0xD20D85FDU,0xA50AB56BU, + 0x35B5A8FAU,0x42B2986CU,0xDBBBC9D6U,0xACBCF940U, + 0x32D86CE3U,0x45DF5C75U,0xDCD60DCFU,0xABD13D59U, + 0x26D930ACU,0x51DE003AU,0xC8D75180U,0xBFD06116U, + 0x21B4F4B5U,0x56B3C423U,0xCFBA9599U,0xB8BDA50FU, + 0x2802B89EU,0x5F058808U,0xC60CD9B2U,0xB10BE924U, + 0x2F6F7C87U,0x58684C11U,0xC1611DABU,0xB6662D3DU, + 0x76DC4190U,0x01DB7106U,0x98D220BCU,0xEFD5102AU, + 0x71B18589U,0x06B6B51FU,0x9FBFE4A5U,0xE8B8D433U, + 0x7807C9A2U,0x0F00F934U,0x9609A88EU,0xE10E9818U, + 0x7F6A0DBBU,0x086D3D2DU,0x91646C97U,0xE6635C01U, + 0x6B6B51F4U,0x1C6C6162U,0x856530D8U,0xF262004EU, + 0x6C0695EDU,0x1B01A57BU,0x8208F4C1U,0xF50FC457U, + 0x65B0D9C6U,0x12B7E950U,0x8BBEB8EAU,0xFCB9887CU, + 0x62DD1DDFU,0x15DA2D49U,0x8CD37CF3U,0xFBD44C65U, + 0x4DB26158U,0x3AB551CEU,0xA3BC0074U,0xD4BB30E2U, + 0x4ADFA541U,0x3DD895D7U,0xA4D1C46DU,0xD3D6F4FBU, + 0x4369E96AU,0x346ED9FCU,0xAD678846U,0xDA60B8D0U, + 0x44042D73U,0x33031DE5U,0xAA0A4C5FU,0xDD0D7CC9U, + 0x5005713CU,0x270241AAU,0xBE0B1010U,0xC90C2086U, + 0x5768B525U,0x206F85B3U,0xB966D409U,0xCE61E49FU, + 0x5EDEF90EU,0x29D9C998U,0xB0D09822U,0xC7D7A8B4U, + 0x59B33D17U,0x2EB40D81U,0xB7BD5C3BU,0xC0BA6CADU, + 0xEDB88320U,0x9ABFB3B6U,0x03B6E20CU,0x74B1D29AU, + 0xEAD54739U,0x9DD277AFU,0x04DB2615U,0x73DC1683U, + 0xE3630B12U,0x94643B84U,0x0D6D6A3EU,0x7A6A5AA8U, + 0xE40ECF0BU,0x9309FF9DU,0x0A00AE27U,0x7D079EB1U, + 0xF00F9344U,0x8708A3D2U,0x1E01F268U,0x6906C2FEU, + 0xF762575DU,0x806567CBU,0x196C3671U,0x6E6B06E7U, + 0xFED41B76U,0x89D32BE0U,0x10DA7A5AU,0x67DD4ACCU, + 0xF9B9DF6FU,0x8EBEEFF9U,0x17B7BE43U,0x60B08ED5U, + 0xD6D6A3E8U,0xA1D1937EU,0x38D8C2C4U,0x4FDFF252U, + 0xD1BB67F1U,0xA6BC5767U,0x3FB506DDU,0x48B2364BU, + 0xD80D2BDAU,0xAF0A1B4CU,0x36034AF6U,0x41047A60U, + 0xDF60EFC3U,0xA867DF55U,0x316E8EEFU,0x4669BE79U, + 0xCB61B38CU,0xBC66831AU,0x256FD2A0U,0x5268E236U, + 0xCC0C7795U,0xBB0B4703U,0x220216B9U,0x5505262FU, + 0xC5BA3BBEU,0xB2BD0B28U,0x2BB45A92U,0x5CB36A04U, + 0xC2D7FFA7U,0xB5D0CF31U,0x2CD99E8BU,0x5BDEAE1DU, + 0x9B64C2B0U,0xEC63F226U,0x756AA39CU,0x026D930AU, + 0x9C0906A9U,0xEB0E363FU,0x72076785U,0x05005713U, + 0x95BF4A82U,0xE2B87A14U,0x7BB12BAEU,0x0CB61B38U, + 0x92D28E9BU,0xE5D5BE0DU,0x7CDCEFB7U,0x0BDBDF21U, + 0x86D3D2D4U,0xF1D4E242U,0x68DDB3F8U,0x1FDA836EU, + 0x81BE16CDU,0xF6B9265BU,0x6FB077E1U,0x18B74777U, + 0x88085AE6U,0xFF0F6A70U,0x66063BCAU,0x11010B5CU, + 0x8F659EFFU,0xF862AE69U,0x616BFFD3U,0x166CCF45U, + 0xA00AE278U,0xD70DD2EEU,0x4E048354U,0x3903B3C2U, + 0xA7672661U,0xD06016F7U,0x4969474DU,0x3E6E77DBU, + 0xAED16A4AU,0xD9D65ADCU,0x40DF0B66U,0x37D83BF0U, + 0xA9BCAE53U,0xDEBB9EC5U,0x47B2CF7FU,0x30B5FFE9U, + 0xBDBDF21CU,0xCABAC28AU,0x53B39330U,0x24B4A3A6U, + 0xBAD03605U,0xCDD70693U,0x54DE5729U,0x23D967BFU, + 0xB3667A2EU,0xC4614AB8U,0x5D681B02U,0x2A6F2B94U, + 0xB40BBE37U,0xC30C8EA1U,0x5A05DF1BU,0x2D02EF8DU, + }; + + crc = crc ^ 0xFFFFFFFFU; + while (len > 0) { + crc = BAKELITE_CONST_32(table[*data ^ (uint8_t)crc]) ^ (crc >> 8); + data++; + len--; + } + crc = crc ^ 0xFFFFFFFFU; + return crc; +} + +/* CRC size constants */ +#define BAKELITE_CRC_NOOP_SIZE 0 +#define BAKELITE_CRC8_SIZE 1 +#define BAKELITE_CRC16_SIZE 2 +#define BAKELITE_CRC32_SIZE 4 diff --git a/bakelite/generator/runtimes/ctiny/serializer.h b/bakelite/generator/runtimes/ctiny/serializer.h new file mode 100644 index 0000000..0727fa1 --- /dev/null +++ b/bakelite/generator/runtimes/ctiny/serializer.h @@ -0,0 +1,212 @@ +/* Write primitives */ +static inline int bakelite_write_bool(Bakelite_Buffer *buf, bool val) { + return bakelite_buffer_write(buf, &val, sizeof(val)); +} + +static inline int bakelite_write_int8(Bakelite_Buffer *buf, int8_t val) { + return bakelite_buffer_write(buf, &val, sizeof(val)); +} + +static inline int bakelite_write_uint8(Bakelite_Buffer *buf, uint8_t val) { + return bakelite_buffer_write(buf, &val, sizeof(val)); +} + +static inline int bakelite_write_int16(Bakelite_Buffer *buf, int16_t val) { + return bakelite_buffer_write(buf, &val, sizeof(val)); +} + +static inline int bakelite_write_uint16(Bakelite_Buffer *buf, uint16_t val) { + return bakelite_buffer_write(buf, &val, sizeof(val)); +} + +static inline int bakelite_write_int32(Bakelite_Buffer *buf, int32_t val) { + return bakelite_buffer_write(buf, &val, sizeof(val)); +} + +static inline int bakelite_write_uint32(Bakelite_Buffer *buf, uint32_t val) { + return bakelite_buffer_write(buf, &val, sizeof(val)); +} + +static inline int bakelite_write_int64(Bakelite_Buffer *buf, int64_t val) { + return bakelite_buffer_write(buf, &val, sizeof(val)); +} + +static inline int bakelite_write_uint64(Bakelite_Buffer *buf, uint64_t val) { + return bakelite_buffer_write(buf, &val, sizeof(val)); +} + +static inline int bakelite_write_float32(Bakelite_Buffer *buf, float val) { + return bakelite_buffer_write(buf, &val, sizeof(val)); +} + +static inline int bakelite_write_float64(Bakelite_Buffer *buf, double val) { + return bakelite_buffer_write(buf, &val, sizeof(val)); +} + +/* Write fixed-size bytes */ +static inline int bakelite_write_bytes_fixed(Bakelite_Buffer *buf, const uint8_t *val, uint32_t size) { + return bakelite_buffer_write(buf, val, size); +} + +/* Write variable-length bytes (with length prefix) */ +static inline int bakelite_write_bytes(Bakelite_Buffer *buf, const Bakelite_SizedArray *val) { + int rcode = bakelite_write_uint8(buf, val->size); + if (rcode != BAKELITE_OK) return rcode; + return bakelite_buffer_write(buf, val->data, val->size); +} + +/* Write fixed-size string */ +static inline int bakelite_write_string_fixed(Bakelite_Buffer *buf, const char *val, uint32_t size) { + return bakelite_buffer_write(buf, val, size); +} + +/* Write null-terminated string */ +static inline int bakelite_write_string(Bakelite_Buffer *buf, const char *val) { + if (val == NULL) { + return bakelite_write_uint8(buf, 0); + } + uint32_t len = (uint32_t)strlen(val); + int rcode = bakelite_buffer_write(buf, val, len); + if (rcode != BAKELITE_OK) return rcode; + return bakelite_write_uint8(buf, 0); +} + +/* Read primitives */ +static inline int bakelite_read_bool(Bakelite_Buffer *buf, bool *val) { + return bakelite_buffer_read(buf, val, sizeof(*val)); +} + +static inline int bakelite_read_int8(Bakelite_Buffer *buf, int8_t *val) { + return bakelite_buffer_read(buf, val, sizeof(*val)); +} + +static inline int bakelite_read_uint8(Bakelite_Buffer *buf, uint8_t *val) { + return bakelite_buffer_read(buf, val, sizeof(*val)); +} + +static inline int bakelite_read_int16(Bakelite_Buffer *buf, int16_t *val) { + return bakelite_buffer_read(buf, val, sizeof(*val)); +} + +static inline int bakelite_read_uint16(Bakelite_Buffer *buf, uint16_t *val) { + return bakelite_buffer_read(buf, val, sizeof(*val)); +} + +static inline int bakelite_read_int32(Bakelite_Buffer *buf, int32_t *val) { + return bakelite_buffer_read(buf, val, sizeof(*val)); +} + +static inline int bakelite_read_uint32(Bakelite_Buffer *buf, uint32_t *val) { + return bakelite_buffer_read(buf, val, sizeof(*val)); +} + +static inline int bakelite_read_int64(Bakelite_Buffer *buf, int64_t *val) { + return bakelite_buffer_read(buf, val, sizeof(*val)); +} + +static inline int bakelite_read_uint64(Bakelite_Buffer *buf, uint64_t *val) { + return bakelite_buffer_read(buf, val, sizeof(*val)); +} + +static inline int bakelite_read_float32(Bakelite_Buffer *buf, float *val) { + return bakelite_buffer_read(buf, val, sizeof(*val)); +} + +static inline int bakelite_read_float64(Bakelite_Buffer *buf, double *val) { + return bakelite_buffer_read(buf, val, sizeof(*val)); +} + +/* Read fixed-size bytes */ +static inline int bakelite_read_bytes_fixed(Bakelite_Buffer *buf, uint8_t *val, uint32_t size) { + return bakelite_buffer_read(buf, val, size); +} + +/* Read variable-length bytes (with length prefix, allocates from heap) */ +static inline int bakelite_read_bytes(Bakelite_Buffer *buf, Bakelite_SizedArray *val) { + uint8_t size; + int rcode = bakelite_read_uint8(buf, &size); + if (rcode != BAKELITE_OK) return rcode; + + val->data = bakelite_buffer_alloc(buf, size); + val->size = size; + + if (val->data == NULL) { + return BAKELITE_ERR_ALLOC_BYTES; + } + + return bakelite_buffer_read(buf, val->data, size); +} + +/* Read fixed-size string */ +static inline int bakelite_read_string_fixed(Bakelite_Buffer *buf, char *val, uint32_t size) { + return bakelite_buffer_read(buf, val, size); +} + +/* Read null-terminated string (allocates from heap) */ +static inline int bakelite_read_string(Bakelite_Buffer *buf, char **val) { + char *start = (char *)bakelite_buffer_alloc(buf, 1); + *val = start; + + if (start == NULL) { + return BAKELITE_ERR_ALLOC_STRING; + } + + char *current = start; + for (;;) { + int rcode = bakelite_buffer_read(buf, current, 1); + if (rcode != BAKELITE_OK) return rcode; + + if (*current == '\0') { + return BAKELITE_OK; + } + + current = (char *)bakelite_buffer_alloc(buf, 1); + if (current == NULL) { + return BAKELITE_ERR_ALLOC_STRING; + } + } +} + +/* Write array of primitives (fixed size) */ +#define BAKELITE_DEFINE_WRITE_ARRAY(type, name) \ + static inline int bakelite_write_array_##name(Bakelite_Buffer *buf, const type *arr, uint32_t count) { \ + for (uint32_t i = 0; i < count; i++) { \ + int rcode = bakelite_write_##name(buf, arr[i]); \ + if (rcode != BAKELITE_OK) return rcode; \ + } \ + return BAKELITE_OK; \ + } + +BAKELITE_DEFINE_WRITE_ARRAY(bool, bool) +BAKELITE_DEFINE_WRITE_ARRAY(int8_t, int8) +BAKELITE_DEFINE_WRITE_ARRAY(uint8_t, uint8) +BAKELITE_DEFINE_WRITE_ARRAY(int16_t, int16) +BAKELITE_DEFINE_WRITE_ARRAY(uint16_t, uint16) +BAKELITE_DEFINE_WRITE_ARRAY(int32_t, int32) +BAKELITE_DEFINE_WRITE_ARRAY(uint32_t, uint32) +BAKELITE_DEFINE_WRITE_ARRAY(int64_t, int64) +BAKELITE_DEFINE_WRITE_ARRAY(uint64_t, uint64) +BAKELITE_DEFINE_WRITE_ARRAY(float, float32) +BAKELITE_DEFINE_WRITE_ARRAY(double, float64) + +/* Read array of primitives (fixed size) */ +#define BAKELITE_DEFINE_READ_ARRAY(type, name) \ + static inline int bakelite_read_array_##name(Bakelite_Buffer *buf, type *arr, uint32_t count) { \ + for (uint32_t i = 0; i < count; i++) { \ + int rcode = bakelite_read_##name(buf, &arr[i]); \ + if (rcode != BAKELITE_OK) return rcode; \ + } \ + return BAKELITE_OK; \ + } + +BAKELITE_DEFINE_READ_ARRAY(bool, bool) +BAKELITE_DEFINE_READ_ARRAY(int8_t, int8) +BAKELITE_DEFINE_READ_ARRAY(uint8_t, uint8) +BAKELITE_DEFINE_READ_ARRAY(int16_t, int16) +BAKELITE_DEFINE_READ_ARRAY(uint16_t, uint16) +BAKELITE_DEFINE_READ_ARRAY(int32_t, int32) +BAKELITE_DEFINE_READ_ARRAY(uint32_t, uint32) +BAKELITE_DEFINE_READ_ARRAY(int64_t, int64) +BAKELITE_DEFINE_READ_ARRAY(uint64_t, uint64) +BAKELITE_DEFINE_READ_ARRAY(float, float32) +BAKELITE_DEFINE_READ_ARRAY(double, float64) diff --git a/bakelite/generator/runtimes/ctiny/stream.h b/bakelite/generator/runtimes/ctiny/stream.h new file mode 100644 index 0000000..bf03dec --- /dev/null +++ b/bakelite/generator/runtimes/ctiny/stream.h @@ -0,0 +1,95 @@ +/* Buffer stream for serialization/deserialization */ +typedef struct { + uint8_t *buffer; + uint32_t size; + uint32_t pos; + /* Heap for variable-length data allocations */ + uint8_t *heap; + uint32_t heap_size; + uint32_t heap_pos; +} Bakelite_Buffer; + +/* Initialize a buffer stream (without heap) */ +static inline void bakelite_buffer_init(Bakelite_Buffer *buf, uint8_t *data, uint32_t size) { + buf->buffer = data; + buf->size = size; + buf->pos = 0; + buf->heap = NULL; + buf->heap_size = 0; + buf->heap_pos = 0; +} + +/* Initialize a buffer stream with heap for variable-length data */ +static inline void bakelite_buffer_init_with_heap(Bakelite_Buffer *buf, + uint8_t *data, uint32_t size, + uint8_t *heap, uint32_t heap_size) { + buf->buffer = data; + buf->size = size; + buf->pos = 0; + buf->heap = heap; + buf->heap_size = heap_size; + buf->heap_pos = 0; +} + +/* Reset buffer position to start */ +static inline void bakelite_buffer_reset(Bakelite_Buffer *buf) { + buf->pos = 0; + buf->heap_pos = 0; +} + +/* Write data to buffer */ +static inline int bakelite_buffer_write(Bakelite_Buffer *buf, const void *data, uint32_t length) { + uint32_t end_pos = buf->pos + length; + if (end_pos > buf->size) { + return BAKELITE_ERR_WRITE; + } + memcpy(buf->buffer + buf->pos, data, length); + buf->pos += length; + return BAKELITE_OK; +} + +/* Read data from buffer */ +static inline int bakelite_buffer_read(Bakelite_Buffer *buf, void *data, uint32_t length) { + uint32_t end_pos = buf->pos + length; + if (end_pos > buf->size) { + return BAKELITE_ERR_READ; + } + memcpy(data, buf->buffer + buf->pos, length); + buf->pos += length; + return BAKELITE_OK; +} + +/* Seek to position */ +static inline int bakelite_buffer_seek(Bakelite_Buffer *buf, uint32_t pos) { + if (pos >= buf->size) { + return BAKELITE_ERR_SEEK; + } + buf->pos = pos; + return BAKELITE_OK; +} + +/* Allocate from heap (for variable-length data during deserialization) */ +static inline void *bakelite_buffer_alloc(Bakelite_Buffer *buf, uint32_t bytes) { + uint32_t new_pos = buf->heap_pos + bytes; + if (buf->heap == NULL || new_pos > buf->heap_size) { + return NULL; + } + void *data = buf->heap + buf->heap_pos; + buf->heap_pos = new_pos; + return data; +} + +/* Get current position */ +static inline uint32_t bakelite_buffer_pos(const Bakelite_Buffer *buf) { + return buf->pos; +} + +/* Get buffer size */ +static inline uint32_t bakelite_buffer_size(const Bakelite_Buffer *buf) { + return buf->size; +} + +/* Get remaining bytes */ +static inline uint32_t bakelite_buffer_remaining(const Bakelite_Buffer *buf) { + return buf->size - buf->pos; +} diff --git a/bakelite/generator/runtimes/ctiny/types.h b/bakelite/generator/runtimes/ctiny/types.h new file mode 100644 index 0000000..c9ea14b --- /dev/null +++ b/bakelite/generator/runtimes/ctiny/types.h @@ -0,0 +1,47 @@ +#include +#include +#include +#include + +/* Packed struct attribute for zero-copy mode */ +#if defined(__GNUC__) || defined(__clang__) + #define BAKELITE_PACKED __attribute__((packed)) +#elif defined(_MSC_VER) + #define BAKELITE_PACKED + #pragma pack(push, 1) +#else + #define BAKELITE_PACKED +#endif + +/* Static assert for compile-time checks (C99 compatible) */ +#define BAKELITE_STATIC_ASSERT(cond, msg) \ + typedef char bakelite_static_assert_##msg[(cond) ? 1 : -1] + +/* Platform detection for unaligned access support */ +#if defined(__AVR__) || defined(__arm__) || defined(__aarch64__) || \ + defined(__i386__) || defined(__x86_64__) || defined(_M_IX86) || defined(_M_X64) + #define BAKELITE_UNALIGNED_OK 1 +#else + #define BAKELITE_UNALIGNED_OK 0 +#endif + +/* Error codes */ +#define BAKELITE_OK 0 +#define BAKELITE_ERR_WRITE -1 +#define BAKELITE_ERR_READ -2 +#define BAKELITE_ERR_SEEK -3 +#define BAKELITE_ERR_ALLOC -4 +#define BAKELITE_ERR_ALLOC_BYTES -5 +#define BAKELITE_ERR_ALLOC_STRING -6 + +/* Sized array for variable-length data */ +typedef struct { + void *data; + uint8_t size; +} Bakelite_SizedArray; + +/* Sized array with 16-bit size */ +typedef struct { + void *data; + uint16_t size; +} Bakelite_SizedArray16; diff --git a/bakelite/generator/templates/ctiny-bakelite.h.j2 b/bakelite/generator/templates/ctiny-bakelite.h.j2 new file mode 100644 index 0000000..9ebeebd --- /dev/null +++ b/bakelite/generator/templates/ctiny-bakelite.h.j2 @@ -0,0 +1,64 @@ +/* + * Bakelite C Runtime + * Copyright (c) Emma Powers 2021-2024 + * + * C99 compatible header-only implementation for embedded systems. + * Licensed under the MIT License (see end of file). + */ + +#ifndef BAKELITE_H +#define BAKELITE_H + +#ifdef __AVR__ +#include +#endif + +/* + * Type Definitions + */ +{{include('types.h')}} + +/* + * Buffer Stream + */ +{{include('stream.h')}} + +/* + * Serialization Functions + */ +{{include('serializer.h')}} + +/* + * CRC Functions + */ +{{include('crc.h')}} + +/* + * COBS Framing + */ +{{include('cobs.h')}} + +/* +The MIT License +--------------- + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +#endif /* BAKELITE_H */ diff --git a/bakelite/generator/templates/ctiny.h.j2 b/bakelite/generator/templates/ctiny.h.j2 new file mode 100644 index 0000000..4e2a18f --- /dev/null +++ b/bakelite/generator/templates/ctiny.h.j2 @@ -0,0 +1,204 @@ +#ifndef PROTOCOL_H +#define PROTOCOL_H + +#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 + +BAKELITE_STATIC_ASSERT(BAKELITE_UNALIGNED_OK, platform_requires_unpacked_mode); +% endif + +/* Forward declarations */ +% for struct in structs +struct {{ struct.name }}; +% endfor + +/* Enums */ +% for enum in enums +% if enum.comment +/* {{ enum.comment }} */ +% endif +typedef enum { + % for value in enum.values: + % if value.comment + /* {{ value.comment }} */ + % endif + {{ enum.name }}_{{ value.name }} = {{ value.value }}, + % endfor +} {{ enum.name }}; +{{""}} +% endfor + +/* Structs */ +% for struct in structs +% if struct.comment +/* {{ struct.comment }} */ +% endif +% if unpacked +typedef struct { +% else +typedef struct BAKELITE_PACKED { +% endif + % for member in struct.members: + % if member.comment + /* {{ member.comment }} */ + % endif + {{map_type_member(member)}} {{ member.name }}{{-array_postfix(member)-}}{{-size_postfix(member)-}}; + % endfor +} {{ struct.name }}; +{{""}} +static inline int {{ struct.name }}_pack(const {{ struct.name }} *self, Bakelite_Buffer *buf) { + int rcode = 0; + % for member in struct.members: + if ((rcode = {{write_type(member)}}) != 0) return rcode; + % endfor + return rcode; +} +{{""}} +static inline int {{ struct.name }}_unpack({{ struct.name }} *self, Bakelite_Buffer *buf) { + int rcode = 0; + % for member in struct.members: + if ((rcode = {{read_type(member)}}) != 0) return rcode; + % endfor + return rcode; +} +{{""}} +% endfor + +% if proto +/* Protocol message IDs */ +typedef enum { + Protocol_NoMessage = -1, + % for msg in message_ids: + Protocol_{{ msg[0] }} = {{ msg[1] }}, + % endfor +} Protocol_Message; + +/* Protocol buffer sizes */ +#define PROTOCOL_MAX_MESSAGE_SIZE {{ max_length }} +#define PROTOCOL_CRC_SIZE {{ crc_size }} +#define PROTOCOL_BUFFER_SIZE BAKELITE_FRAMER_BUFFER_SIZE(PROTOCOL_MAX_MESSAGE_SIZE, PROTOCOL_CRC_SIZE) +#define PROTOCOL_MESSAGE_OFFSET BAKELITE_FRAMER_MESSAGE_OFFSET(PROTOCOL_MAX_MESSAGE_SIZE, PROTOCOL_CRC_SIZE) + +/* Protocol handler */ +typedef struct { + int (*read_byte)(void); + size_t (*write)(const uint8_t *data, size_t length); + Bakelite_CobsFramer framer; + uint8_t buffer[PROTOCOL_BUFFER_SIZE]; + Protocol_Message received_message; + size_t received_frame_length; +} Protocol; + +static inline void Protocol_init(Protocol *self, + int (*read_byte)(void), + size_t (*write)(const uint8_t *data, size_t length)) { + self->read_byte = read_byte; + self->write = write; + self->received_message = Protocol_NoMessage; + self->received_frame_length = 0; + bakelite_framer_init(&self->framer, self->buffer, PROTOCOL_BUFFER_SIZE, + PROTOCOL_MAX_MESSAGE_SIZE, {{ crc_type }}); +} + +static inline Protocol_Message Protocol_poll(Protocol *self) { + int byte = self->read_byte(); + if (byte < 0) { + return Protocol_NoMessage; + } + + Bakelite_DecodeResult result = bakelite_framer_read_byte(&self->framer, (uint8_t)byte); + if (result.status == BAKELITE_DECODE_OK) { + if (result.length == 0) { + return Protocol_NoMessage; + } + + self->received_message = (Protocol_Message)result.data[0]; + self->received_frame_length = result.length - 1; + return self->received_message; + } + + return Protocol_NoMessage; +} + +/* Get pointer to message data in buffer (for zero-copy access) */ +static inline uint8_t *Protocol_buffer(Protocol *self) { + return bakelite_framer_buffer(&self->framer) + 1; +} + +% if not unpacked +/* Zero-copy message access (packed mode only) */ +% for msg in message_ids: +static inline {{ msg[0] }} *Protocol_message_{{ msg[0] }}(Protocol *self) { + return ({{ msg[0] }} *)(bakelite_framer_buffer(&self->framer) + 1); +} +{{""}} +% endfor + +/* Zero-copy send functions (packed mode only) */ +% for msg in message_ids: +static inline int Protocol_send_zerocopy_{{ msg[0] }}(Protocol *self) { + bakelite_framer_buffer(&self->framer)[0] = (uint8_t)Protocol_{{ msg[0] }}; + size_t frame_size = sizeof({{ msg[0] }}) + 1; + Bakelite_FramerResult result = bakelite_framer_encode(&self->framer, frame_size); + + if (result.status != 0) { + return result.status; + } + + size_t ret = self->write(result.data, result.length); + return (ret == result.length) ? 0 : -1; +} +{{""}} +% endfor +% endif + +/* Copy-based send functions */ +% for msg in message_ids: +static inline int Protocol_send_{{ msg[0] }}(Protocol *self, const {{ msg[0] }} *msg) { + uint8_t *msg_buf = bakelite_framer_buffer(&self->framer); + msg_buf[0] = (uint8_t)Protocol_{{ msg[0] }}; + + Bakelite_Buffer buf; + bakelite_buffer_init(&buf, msg_buf + 1, bakelite_framer_buffer_size(&self->framer) - 1); + {{ msg[0] }}_pack(msg, &buf); + + size_t frame_size = bakelite_buffer_pos(&buf) + 1; + Bakelite_FramerResult result = bakelite_framer_encode(&self->framer, frame_size); + + if (result.status != 0) { + return result.status; + } + + size_t ret = self->write(result.data, result.length); + return (ret == result.length) ? 0 : -1; +} +{{""}} +% endfor + +/* Copy-based decode functions */ +% for msg in message_ids: +static inline int Protocol_decode_{{ msg[0] }}(Protocol *self, {{ msg[0] }} *msg, + uint8_t *heap, size_t heap_size) { + if (self->received_message != Protocol_{{ msg[0] }}) { + return -1; + } + + Bakelite_Buffer buf; + bakelite_buffer_init_with_heap(&buf, + bakelite_framer_buffer(&self->framer) + 1, self->received_frame_length, + heap, heap_size); + return {{ msg[0] }}_unpack(msg, &buf); +} +{{""}} +% endfor +% endif + +#endif /* PROTOCOL_H */ diff --git a/docs/ctiny.md b/docs/ctiny.md new file mode 100644 index 0000000..895c1af --- /dev/null +++ b/docs/ctiny.md @@ -0,0 +1,149 @@ +# C (Tiny) +The `ctiny` target generates pure C99 code for embedded systems with highly constrained resources. +It has a small memory footprint and does not allocate on the heap. +It works well on microcontrollers with under 2k of RAM or on a desktop PC. + +## Features + * Header only (C99) + * Small memory footprint with single-buffer design + * Zero-copy message access (packed mode) + * No heap memory allocation + * No dependencies beyond standard C library + +## Example +Code generation: +```sh +bakelite runtime -l ctiny -o bakelite.h # One-time generation of the library +bakelite gen -l ctiny -i proto.bakelite -o proto.h +``` + +Use the generated code: +```c +#include "proto.h" + +int read_byte(void) { + // Return one byte or -1 if no data available +} + +size_t write_bytes(const uint8_t *data, size_t len) { + // Write data, return bytes written +} + +int main(void) { + Protocol proto; + Protocol_init(&proto, read_byte, write_bytes); + + // Zero-copy send - write directly to the buffer + HelloMsg *msg = Protocol_message_HelloMsg(&proto); + msg->code = 42; + strcpy(msg->message, "Hello world!"); + Protocol_send_zerocopy_HelloMsg(&proto); + + // Poll for messages + while (1) { + Protocol_Message msg_id = Protocol_poll(&proto); + + switch (msg_id) { + case Protocol_NoMessage: + break; + + case Protocol_ReplyMsg: { + // Zero-copy receive - access message directly in buffer + ReplyMsg *reply = Protocol_message_ReplyMsg(&proto); + printf("Reply: %s\n", reply->text); + break; + } + default: + 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 ctiny -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 (`bakelite runtime`) and the protocol implementation (`bakelite gen`). + +The runtime contains the library code and only needs to be re-generated when upgrading Bakelite. +The protocol implementation contains the type definitions and serializers from your .bakelite file. + +## API + +### Type Mappings +|Bakelite Type |C Type | +|------------------------------|--------------------------------------| +|int8, int16, int32, int64 |int8_t, int16_t, int32_t, int64_t | +|uint8, uint16, uint32, uint64 |uint8_t, uint16_t, uint32_t, uint64_t | +|float32, float64 |float, double | +|bool |bool | +|bytes[n] |uint8_t[n] | +|string[n] |char[n] | +|T[n] (fixed array) |T[n] | +|struct T |typedef struct { } T | +|enum T: S |typedef enum { } T | + +### Protocol + +##### Protocol_init(Protocol *self, ReadFn read, WriteFn write) +Initialize the protocol handler. + +**arguments:** +* **read** - Function returning one byte or -1 if no data: `int (*)(void)` +* **write** - Function writing bytes: `size_t (*)(const uint8_t *data, size_t length)` + +##### Protocol_poll(Protocol *self) -> Protocol_Message +Reads available data and returns the message ID if a complete frame was received. + +**returns:** +The message ID (e.g., `Protocol_MyMsg`) or `Protocol_NoMessage`. + +##### Protocol_message_T(Protocol *self) -> T* +Returns a pointer to the message in the internal buffer. Use for zero-copy access. + +**For receiving:** Call after `Protocol_poll()` returns a message ID. +**For sending:** Call before `Protocol_send_zerocopy_T()` to write data. + +##### Protocol_send_zerocopy_T(Protocol *self) -> int +Sends the message already written to the buffer. Zero-copy. + +**returns:** 0 on success. + +##### Protocol_send_T(Protocol *self, const T *msg) -> int +Copy-based send. Serializes and sends a message. + +**returns:** 0 on success. + +##### Protocol_decode_T(Protocol *self, T *msg, uint8_t *heap, size_t heap_size) -> int +Copy-based decode for variable-length fields. + +**returns:** 0 on success. + +### Struct +Each struct generates pack/unpack functions: + +```c +int T_pack(const T *self, Bakelite_Buffer *buf); +int T_unpack(T *self, Bakelite_Buffer *buf); +``` diff --git a/examples/ReadMe.md b/examples/ReadMe.md index ebf80aa..4ca9a4e 100644 --- a/examples/ReadMe.md +++ b/examples/ReadMe.md @@ -2,3 +2,6 @@ ## [Talking to an Arduino from Python](./arduino/) A simple example showing how to communicate with an Arduino from a Python script running on a PC. It demonstrates a very simple protocol, and how to use the protocol to talk to an Arduino over a serial port. + +## [TCP Chat](./chat/) +A TCP chat application demonstrating the ctiny (C) and cpptiny (C++) runtimes. Both implementations use the same protocol definition and can interoperate. Shows CMake integration for automatic code generation. diff --git a/examples/arduino/bakelite.h b/examples/arduino/bakelite.h index b10ed68..eb14e54 100644 --- a/examples/arduino/bakelite.h +++ b/examples/arduino/bakelite.h @@ -572,6 +572,12 @@ class CobsFramer { return bufferSize(); } + Result encodeFrame(const char *data, size_t length) { + char *msgStart = m_buffer + messageOffset(); + memcpy(msgStart, data, length); + return encodeFrame(length); + } + Result encodeFrame(size_t length) { char *msgStart = m_buffer + messageOffset(); diff --git a/examples/chat/chat.bakelite b/examples/chat/chat.bakelite new file mode 100644 index 0000000..c813b1e --- /dev/null +++ b/examples/chat/chat.bakelite @@ -0,0 +1,19 @@ +struct ChatMessage { + sender: string[32] + text: string[256] +} + +struct SetName { + name: string[32] +} + +protocol { + maxLength = 300 + framing = COBS + crc = None + + messageIds { + ChatMessage = 1 + SetName = 2 + } +} diff --git a/examples/chat/cpptiny/CMakeLists.txt b/examples/chat/cpptiny/CMakeLists.txt new file mode 100644 index 0000000..8469b30 --- /dev/null +++ b/examples/chat/cpptiny/CMakeLists.txt @@ -0,0 +1,39 @@ +cmake_minimum_required(VERSION 3.10) +project(bakelite_chat_cpptiny) + +set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Find bakelite code generator +find_program(BAKELITE_EXECUTABLE bakelite REQUIRED) + +# Protocol definition +set(PROTO_DEF "${CMAKE_CURRENT_SOURCE_DIR}/../chat.bakelite") + +# Generated files +set(GENERATED_PROTO "${CMAKE_CURRENT_SOURCE_DIR}/proto.h") +set(GENERATED_RUNTIME "${CMAKE_CURRENT_SOURCE_DIR}/bakelite.h") + +# Generate protocol header from .bakelite file +add_custom_command( + OUTPUT ${GENERATED_PROTO} + COMMAND ${BAKELITE_EXECUTABLE} gen -l cpptiny -i ${PROTO_DEF} -o ${GENERATED_PROTO} + DEPENDS ${PROTO_DEF} + COMMENT "Generating proto.h from chat.bakelite" +) + +# Generate runtime header +add_custom_command( + OUTPUT ${GENERATED_RUNTIME} + COMMAND ${BAKELITE_EXECUTABLE} runtime -l cpptiny -o ${GENERATED_RUNTIME} + COMMENT "Generating bakelite.h runtime" +) + +# Custom target for generated files +add_custom_target(generate_protocol DEPENDS ${GENERATED_PROTO} ${GENERATED_RUNTIME}) + +add_executable(server server.cpp) +add_executable(client client.cpp) + +add_dependencies(server generate_protocol) +add_dependencies(client generate_protocol) diff --git a/examples/chat/cpptiny/Makefile b/examples/chat/cpptiny/Makefile new file mode 100644 index 0000000..e2a1b1c --- /dev/null +++ b/examples/chat/cpptiny/Makefile @@ -0,0 +1,21 @@ +.PHONY: all cmake build clean run-server run-client + +all: build + +build: build/Makefile + $(MAKE) -C build + +build/Makefile: + cmake -B build + +cmake: + rm -rf build && cmake -B build + +clean: + rm -rf build + +run-server: build + ./build/server + +run-client: build + ./build/client diff --git a/examples/chat/cpptiny/README.md b/examples/chat/cpptiny/README.md new file mode 100644 index 0000000..0e9098e --- /dev/null +++ b/examples/chat/cpptiny/README.md @@ -0,0 +1,51 @@ +# Bakelite Chat Example (C++) + +A simple TCP chat demonstrating the bakelite cpptiny runtime. + +## Build + +```bash +cmake -B build +cmake --build build +``` + +Or using the Makefile wrapper: +```bash +make +``` + +The build automatically generates `proto.h` and `bakelite.h` from `chat.bakelite` via CMake custom commands. + +## Usage + +Start the server in one terminal: +```bash +./build/server +``` + +Connect with the client in another: +```bash +./build/client +``` + +Type messages and press Enter to send. Use `/name ` to change your display name. + +## CMake Integration + +This example demonstrates integrating bakelite code generation into CMake. The key parts: + +```cmake +find_program(BAKELITE_EXECUTABLE bakelite REQUIRED) + +add_custom_command( + OUTPUT proto.h + COMMAND ${BAKELITE_EXECUTABLE} gen -l cpptiny -i ../chat.bakelite -o proto.h + DEPENDS ../chat.bakelite +) +``` + +Generated files are rebuilt automatically when `chat.bakelite` changes. + +## Interoperability + +Uses the same protocol as the ctiny (C) version. You can run a C++ server with a C client or vice versa. diff --git a/examples/chat/cpptiny/bakelite.h b/examples/chat/cpptiny/bakelite.h new file mode 100644 index 0000000..eb14e54 --- /dev/null +++ b/examples/chat/cpptiny/bakelite.h @@ -0,0 +1,924 @@ +/* + * The code in this file is Copyright (c) Emma Powers 2021, + * unless otherwise marked. This software is made available + * under the terms of the MIT License, which can be found at the + * bottom of this file. +*/ + +#ifndef __BAKELITE_H__ +#define __BAKELITE_H__ + +#include +#include +#include +#include + +#ifdef __AVR__ +#include +#endif + +namespace Bakelite { + /* + * + * Pre-Declarations + * + */ + // Pre-declarations of COBS functions +struct cobs_encode_result; +struct cobs_decode_result +{ + size_t out_len; + int status; +}; +static cobs_encode_result cobs_encode(void *dst_buf_ptr, size_t dst_buf_len, + const void *src_ptr, size_t src_len); +static cobs_decode_result cobs_decode(void *dst_buf_ptr, size_t dst_buf_len, + const void *src_ptr, size_t src_len); + + /* + * + * Serializer + * + */ + template +struct SizedArray { + T *data = nullptr; + S size = 0; + + const T &at(size_t pos) const { + return data[pos]; + } +}; + +class BufferStream { +public: + BufferStream(char *buff, uint32_t size, + char *heap = nullptr, uint32_t heapSize = 0): + m_buff(buff), + m_size(size), + m_pos(0), + m_heap(heap), + m_heapPos(0), + m_heapSize(heapSize) + {} + + int write(const char *data, uint32_t length) { + uint32_t endPos = m_pos + length; + if(endPos > m_size) { + return -1; + } + + memcpy(m_buff+m_pos, data, length); + m_pos += length; + + return 0; + } + + int read(char *data, uint32_t length) { + uint32_t endPos = m_pos + length; + if(endPos > m_size) { + return -2; + } + + memcpy(data, m_buff+m_pos, length); + m_pos += length; + + return 0; + } + + int seek(uint32_t pos) { + if(pos >= m_size) { + return -3; + } + else { + m_pos = pos; + } + return 0; + } + + uint32_t size() const { + return m_size; + } + + uint32_t pos() const { + return m_pos; + } + + char *alloc(size_t bytes) { + size_t newPos = m_heapPos + bytes; + if(newPos >= m_heapSize) + return nullptr; + + char *data = &m_heap[m_heapPos]; + m_heapPos = newPos; + return data; + } + +private: + char *m_buff; + size_t m_size; + size_t m_pos; + + char *m_heap; + size_t m_heapPos; + size_t m_heapSize; +}; + +template +int write(T& stream, V val) { + return stream.write((const char *)&val, sizeof(val)); +} + +template +int writeArray(T& stream, const V *val, int size, F writeCb) { + for(int i = 0; i < size; i++) { + int rcode = writeCb(stream, val[i]); + if(rcode != 0) + return rcode; + } + return 0; +} + +template +int writeArray(T& stream, const SizedArray &val, F writeCb) { + write(stream, val.size); + for(int i = 0; i < val.size; i++) { + int rcode = writeCb(stream, val.at(i)); + if(rcode != 0) + return rcode; + } + return 0; +} + +template +int writeBytes(T& stream, const char *val, int size) { + return stream.write((const char *)val, size); +} + +template +int writeBytes(T& stream, const SizedArray &val) { + int rcode = write(stream, val.size); + return stream.write((const char *)val.data, val.size); +} + +template +int writeString(T& stream, const char *val, int size) { + return stream.write(val, size); +} + +template +int writeString(T& stream, const char *val) { + if(val == nullptr) { + return write(stream, (uint8_t)0); + } + + uint8_t len = strlen(val); + int rcode = stream.write(val, len); + if(rcode != 0) + return rcode; + return write(stream, (uint8_t)0); +} + +template +int read(T& stream, V &val) { + return stream.read((char *)&val, sizeof(val)); +} + +template +int readArray(T& stream, V val[], int size, F readCb) { + for(int i = 0; i < size; i++) { + int rcode = readCb(stream, val[i]); + if(rcode != 0) + return rcode; + } + return 0; +} + +template +int readArray(T& stream, SizedArray &val, F readCb) { + S size = 0; + int rcode = read(stream, size); + if(rcode != 0) + return rcode; + + val.data = (V*)stream.alloc(sizeof(V) * size); + val.size = size; + + if(val.data == nullptr) { + return -4; + } + + for(int i = 0; i < size; i++) { + int rcode = readCb(stream, val.data[i]); + if(rcode != 0) + return rcode; + } + return 0; +} + +template +int readBytes(T& stream, char *val, int size) { + return stream.read(val, size); +} + +template +int readBytes(T& stream, SizedArray &val) { + S size = 0; + int rcode = read(stream, size); + if(rcode != 0) + return rcode; + + val.data = stream.alloc(size); + val.size = size; + + if(val.data == nullptr) { + return -5; + } + + return stream.read(val.data, val.size); +} + +template +int readString(T& stream, char *val, int size) { + return stream.read(val, size); +} + +template +int readString(T& stream, char* &val) { + char *newByte = stream.alloc(1); + val = newByte; + + do { + int rcode = stream.read(newByte, 1); + if(rcode != 0) + return rcode; + + if(*newByte == 0) { + return 0; + } + } while((newByte = stream.alloc(1)) != nullptr); + + return -6; +} + + /* + * + * CRC + * + */ + class CrcNoop { +public: + constexpr static size_t size() { + return 0; + } + + int value() const { + return 0; + } + + void update(const char *c, size_t length) { + } +}; + +template +class Crc { +public: + constexpr static size_t size() { + return sizeof(CrcType); + } + + CrcType value() const { + return m_lastVal; + } + + void update(const char *data, size_t length) { + CrcFunc fn; + m_lastVal = fn(data, length, m_lastVal); + } +private: + CrcType m_lastVal = 0; +}; + +struct crc8_fn; +struct crc16_fn; +struct crc32_fn; + + +using Crc8 = Crc; +using Crc16 = Crc; +using Crc32 = Crc; + +/* + * Auto generated CRC functions + */ + +// The CRC lookup tables are stored as const variables. On many platforms, +// const variables are stored in flash memroy. On AVR though, they are +// loaded into RAM on startup. A special PROGMEM macro is available on AVRs +// to indicate constants should be stored in program memory (flash). +// So if this macro is available, use it and assume we're on an AVR. +#ifdef PROGMEM + #define BAKELITE_CONST PROGMEM + // PROGMEM variables need to be accessed using pgm_read_* functions. + #define BAKELITE_CONST_8(x) pgm_read_byte(&(x)) + #define BAKELITE_CONST_16(x) pgm_read_dword(&(x)) + #define BAKELITE_CONST_32(x) ((uint32_t)pgm_read_dword((char *)&(x) + 2) << 16 | pgm_read_dword(&(x))) +#else + #define BAKELITE_CONST + #define BAKELITE_CONST_8(x) x + #define BAKELITE_CONST_16(x) x + #define BAKELITE_CONST_32(x) x +#endif + +// Automatically generated CRC function +// polynomial: 0x107 +struct crc8_fn { + uint8_t operator()(const char *data, int len, uint8_t crc) { + const unsigned char *uData = (unsigned char *)data; + + static const uint8_t table[256] BAKELITE_CONST = { + 0x00U,0x07U,0x0EU,0x09U,0x1CU,0x1BU,0x12U,0x15U, + 0x38U,0x3FU,0x36U,0x31U,0x24U,0x23U,0x2AU,0x2DU, + 0x70U,0x77U,0x7EU,0x79U,0x6CU,0x6BU,0x62U,0x65U, + 0x48U,0x4FU,0x46U,0x41U,0x54U,0x53U,0x5AU,0x5DU, + 0xE0U,0xE7U,0xEEU,0xE9U,0xFCU,0xFBU,0xF2U,0xF5U, + 0xD8U,0xDFU,0xD6U,0xD1U,0xC4U,0xC3U,0xCAU,0xCDU, + 0x90U,0x97U,0x9EU,0x99U,0x8CU,0x8BU,0x82U,0x85U, + 0xA8U,0xAFU,0xA6U,0xA1U,0xB4U,0xB3U,0xBAU,0xBDU, + 0xC7U,0xC0U,0xC9U,0xCEU,0xDBU,0xDCU,0xD5U,0xD2U, + 0xFFU,0xF8U,0xF1U,0xF6U,0xE3U,0xE4U,0xEDU,0xEAU, + 0xB7U,0xB0U,0xB9U,0xBEU,0xABU,0xACU,0xA5U,0xA2U, + 0x8FU,0x88U,0x81U,0x86U,0x93U,0x94U,0x9DU,0x9AU, + 0x27U,0x20U,0x29U,0x2EU,0x3BU,0x3CU,0x35U,0x32U, + 0x1FU,0x18U,0x11U,0x16U,0x03U,0x04U,0x0DU,0x0AU, + 0x57U,0x50U,0x59U,0x5EU,0x4BU,0x4CU,0x45U,0x42U, + 0x6FU,0x68U,0x61U,0x66U,0x73U,0x74U,0x7DU,0x7AU, + 0x89U,0x8EU,0x87U,0x80U,0x95U,0x92U,0x9BU,0x9CU, + 0xB1U,0xB6U,0xBFU,0xB8U,0xADU,0xAAU,0xA3U,0xA4U, + 0xF9U,0xFEU,0xF7U,0xF0U,0xE5U,0xE2U,0xEBU,0xECU, + 0xC1U,0xC6U,0xCFU,0xC8U,0xDDU,0xDAU,0xD3U,0xD4U, + 0x69U,0x6EU,0x67U,0x60U,0x75U,0x72U,0x7BU,0x7CU, + 0x51U,0x56U,0x5FU,0x58U,0x4DU,0x4AU,0x43U,0x44U, + 0x19U,0x1EU,0x17U,0x10U,0x05U,0x02U,0x0BU,0x0CU, + 0x21U,0x26U,0x2FU,0x28U,0x3DU,0x3AU,0x33U,0x34U, + 0x4EU,0x49U,0x40U,0x47U,0x52U,0x55U,0x5CU,0x5BU, + 0x76U,0x71U,0x78U,0x7FU,0x6AU,0x6DU,0x64U,0x63U, + 0x3EU,0x39U,0x30U,0x37U,0x22U,0x25U,0x2CU,0x2BU, + 0x06U,0x01U,0x08U,0x0FU,0x1AU,0x1DU,0x14U,0x13U, + 0xAEU,0xA9U,0xA0U,0xA7U,0xB2U,0xB5U,0xBCU,0xBBU, + 0x96U,0x91U,0x98U,0x9FU,0x8AU,0x8DU,0x84U,0x83U, + 0xDEU,0xD9U,0xD0U,0xD7U,0xC2U,0xC5U,0xCCU,0xCBU, + 0xE6U,0xE1U,0xE8U,0xEFU,0xFAU,0xFDU,0xF4U,0xF3U, + }; + + while (len > 0) + { + crc = BAKELITE_CONST_8(table[*uData ^ (uint8_t)crc]); + uData++; + len--; + } + return crc; + } +}; + +// Automatically generated CRC function +// polynomial: 0x18005, bit reverse algorithm +struct crc16_fn { + uint16_t operator()(const char *data, int len, uint16_t crc) { + const unsigned char *uData = (unsigned char *)data; + + static const uint16_t table[256] BAKELITE_CONST = { + 0x0000U,0xC0C1U,0xC181U,0x0140U,0xC301U,0x03C0U,0x0280U,0xC241U, + 0xC601U,0x06C0U,0x0780U,0xC741U,0x0500U,0xC5C1U,0xC481U,0x0440U, + 0xCC01U,0x0CC0U,0x0D80U,0xCD41U,0x0F00U,0xCFC1U,0xCE81U,0x0E40U, + 0x0A00U,0xCAC1U,0xCB81U,0x0B40U,0xC901U,0x09C0U,0x0880U,0xC841U, + 0xD801U,0x18C0U,0x1980U,0xD941U,0x1B00U,0xDBC1U,0xDA81U,0x1A40U, + 0x1E00U,0xDEC1U,0xDF81U,0x1F40U,0xDD01U,0x1DC0U,0x1C80U,0xDC41U, + 0x1400U,0xD4C1U,0xD581U,0x1540U,0xD701U,0x17C0U,0x1680U,0xD641U, + 0xD201U,0x12C0U,0x1380U,0xD341U,0x1100U,0xD1C1U,0xD081U,0x1040U, + 0xF001U,0x30C0U,0x3180U,0xF141U,0x3300U,0xF3C1U,0xF281U,0x3240U, + 0x3600U,0xF6C1U,0xF781U,0x3740U,0xF501U,0x35C0U,0x3480U,0xF441U, + 0x3C00U,0xFCC1U,0xFD81U,0x3D40U,0xFF01U,0x3FC0U,0x3E80U,0xFE41U, + 0xFA01U,0x3AC0U,0x3B80U,0xFB41U,0x3900U,0xF9C1U,0xF881U,0x3840U, + 0x2800U,0xE8C1U,0xE981U,0x2940U,0xEB01U,0x2BC0U,0x2A80U,0xEA41U, + 0xEE01U,0x2EC0U,0x2F80U,0xEF41U,0x2D00U,0xEDC1U,0xEC81U,0x2C40U, + 0xE401U,0x24C0U,0x2580U,0xE541U,0x2700U,0xE7C1U,0xE681U,0x2640U, + 0x2200U,0xE2C1U,0xE381U,0x2340U,0xE101U,0x21C0U,0x2080U,0xE041U, + 0xA001U,0x60C0U,0x6180U,0xA141U,0x6300U,0xA3C1U,0xA281U,0x6240U, + 0x6600U,0xA6C1U,0xA781U,0x6740U,0xA501U,0x65C0U,0x6480U,0xA441U, + 0x6C00U,0xACC1U,0xAD81U,0x6D40U,0xAF01U,0x6FC0U,0x6E80U,0xAE41U, + 0xAA01U,0x6AC0U,0x6B80U,0xAB41U,0x6900U,0xA9C1U,0xA881U,0x6840U, + 0x7800U,0xB8C1U,0xB981U,0x7940U,0xBB01U,0x7BC0U,0x7A80U,0xBA41U, + 0xBE01U,0x7EC0U,0x7F80U,0xBF41U,0x7D00U,0xBDC1U,0xBC81U,0x7C40U, + 0xB401U,0x74C0U,0x7580U,0xB541U,0x7700U,0xB7C1U,0xB681U,0x7640U, + 0x7200U,0xB2C1U,0xB381U,0x7340U,0xB101U,0x71C0U,0x7080U,0xB041U, + 0x5000U,0x90C1U,0x9181U,0x5140U,0x9301U,0x53C0U,0x5280U,0x9241U, + 0x9601U,0x56C0U,0x5780U,0x9741U,0x5500U,0x95C1U,0x9481U,0x5440U, + 0x9C01U,0x5CC0U,0x5D80U,0x9D41U,0x5F00U,0x9FC1U,0x9E81U,0x5E40U, + 0x5A00U,0x9AC1U,0x9B81U,0x5B40U,0x9901U,0x59C0U,0x5880U,0x9841U, + 0x8801U,0x48C0U,0x4980U,0x8941U,0x4B00U,0x8BC1U,0x8A81U,0x4A40U, + 0x4E00U,0x8EC1U,0x8F81U,0x4F40U,0x8D01U,0x4DC0U,0x4C80U,0x8C41U, + 0x4400U,0x84C1U,0x8581U,0x4540U,0x8701U,0x47C0U,0x4680U,0x8641U, + 0x8201U,0x42C0U,0x4380U,0x8341U,0x4100U,0x81C1U,0x8081U,0x4040U, + }; + + while (len > 0) + { + crc = BAKELITE_CONST_16(table[*uData ^ (uint8_t)crc]) ^ (crc >> 8); + uData++; + len--; + } + return crc; + } +}; + +// Automatically generated CRC function +// polynomial: 0x104C11DB7, bit reverse algorithm +struct crc32_fn { + uint32_t operator()(const char *data, int len, uint32_t crc) { + const unsigned char *uData = (unsigned char *)data; + + static const uint32_t table[256] BAKELITE_CONST = { + 0x00000000U,0x77073096U,0xEE0E612CU,0x990951BAU, + 0x076DC419U,0x706AF48FU,0xE963A535U,0x9E6495A3U, + 0x0EDB8832U,0x79DCB8A4U,0xE0D5E91EU,0x97D2D988U, + 0x09B64C2BU,0x7EB17CBDU,0xE7B82D07U,0x90BF1D91U, + 0x1DB71064U,0x6AB020F2U,0xF3B97148U,0x84BE41DEU, + 0x1ADAD47DU,0x6DDDE4EBU,0xF4D4B551U,0x83D385C7U, + 0x136C9856U,0x646BA8C0U,0xFD62F97AU,0x8A65C9ECU, + 0x14015C4FU,0x63066CD9U,0xFA0F3D63U,0x8D080DF5U, + 0x3B6E20C8U,0x4C69105EU,0xD56041E4U,0xA2677172U, + 0x3C03E4D1U,0x4B04D447U,0xD20D85FDU,0xA50AB56BU, + 0x35B5A8FAU,0x42B2986CU,0xDBBBC9D6U,0xACBCF940U, + 0x32D86CE3U,0x45DF5C75U,0xDCD60DCFU,0xABD13D59U, + 0x26D930ACU,0x51DE003AU,0xC8D75180U,0xBFD06116U, + 0x21B4F4B5U,0x56B3C423U,0xCFBA9599U,0xB8BDA50FU, + 0x2802B89EU,0x5F058808U,0xC60CD9B2U,0xB10BE924U, + 0x2F6F7C87U,0x58684C11U,0xC1611DABU,0xB6662D3DU, + 0x76DC4190U,0x01DB7106U,0x98D220BCU,0xEFD5102AU, + 0x71B18589U,0x06B6B51FU,0x9FBFE4A5U,0xE8B8D433U, + 0x7807C9A2U,0x0F00F934U,0x9609A88EU,0xE10E9818U, + 0x7F6A0DBBU,0x086D3D2DU,0x91646C97U,0xE6635C01U, + 0x6B6B51F4U,0x1C6C6162U,0x856530D8U,0xF262004EU, + 0x6C0695EDU,0x1B01A57BU,0x8208F4C1U,0xF50FC457U, + 0x65B0D9C6U,0x12B7E950U,0x8BBEB8EAU,0xFCB9887CU, + 0x62DD1DDFU,0x15DA2D49U,0x8CD37CF3U,0xFBD44C65U, + 0x4DB26158U,0x3AB551CEU,0xA3BC0074U,0xD4BB30E2U, + 0x4ADFA541U,0x3DD895D7U,0xA4D1C46DU,0xD3D6F4FBU, + 0x4369E96AU,0x346ED9FCU,0xAD678846U,0xDA60B8D0U, + 0x44042D73U,0x33031DE5U,0xAA0A4C5FU,0xDD0D7CC9U, + 0x5005713CU,0x270241AAU,0xBE0B1010U,0xC90C2086U, + 0x5768B525U,0x206F85B3U,0xB966D409U,0xCE61E49FU, + 0x5EDEF90EU,0x29D9C998U,0xB0D09822U,0xC7D7A8B4U, + 0x59B33D17U,0x2EB40D81U,0xB7BD5C3BU,0xC0BA6CADU, + 0xEDB88320U,0x9ABFB3B6U,0x03B6E20CU,0x74B1D29AU, + 0xEAD54739U,0x9DD277AFU,0x04DB2615U,0x73DC1683U, + 0xE3630B12U,0x94643B84U,0x0D6D6A3EU,0x7A6A5AA8U, + 0xE40ECF0BU,0x9309FF9DU,0x0A00AE27U,0x7D079EB1U, + 0xF00F9344U,0x8708A3D2U,0x1E01F268U,0x6906C2FEU, + 0xF762575DU,0x806567CBU,0x196C3671U,0x6E6B06E7U, + 0xFED41B76U,0x89D32BE0U,0x10DA7A5AU,0x67DD4ACCU, + 0xF9B9DF6FU,0x8EBEEFF9U,0x17B7BE43U,0x60B08ED5U, + 0xD6D6A3E8U,0xA1D1937EU,0x38D8C2C4U,0x4FDFF252U, + 0xD1BB67F1U,0xA6BC5767U,0x3FB506DDU,0x48B2364BU, + 0xD80D2BDAU,0xAF0A1B4CU,0x36034AF6U,0x41047A60U, + 0xDF60EFC3U,0xA867DF55U,0x316E8EEFU,0x4669BE79U, + 0xCB61B38CU,0xBC66831AU,0x256FD2A0U,0x5268E236U, + 0xCC0C7795U,0xBB0B4703U,0x220216B9U,0x5505262FU, + 0xC5BA3BBEU,0xB2BD0B28U,0x2BB45A92U,0x5CB36A04U, + 0xC2D7FFA7U,0xB5D0CF31U,0x2CD99E8BU,0x5BDEAE1DU, + 0x9B64C2B0U,0xEC63F226U,0x756AA39CU,0x026D930AU, + 0x9C0906A9U,0xEB0E363FU,0x72076785U,0x05005713U, + 0x95BF4A82U,0xE2B87A14U,0x7BB12BAEU,0x0CB61B38U, + 0x92D28E9BU,0xE5D5BE0DU,0x7CDCEFB7U,0x0BDBDF21U, + 0x86D3D2D4U,0xF1D4E242U,0x68DDB3F8U,0x1FDA836EU, + 0x81BE16CDU,0xF6B9265BU,0x6FB077E1U,0x18B74777U, + 0x88085AE6U,0xFF0F6A70U,0x66063BCAU,0x11010B5CU, + 0x8F659EFFU,0xF862AE69U,0x616BFFD3U,0x166CCF45U, + 0xA00AE278U,0xD70DD2EEU,0x4E048354U,0x3903B3C2U, + 0xA7672661U,0xD06016F7U,0x4969474DU,0x3E6E77DBU, + 0xAED16A4AU,0xD9D65ADCU,0x40DF0B66U,0x37D83BF0U, + 0xA9BCAE53U,0xDEBB9EC5U,0x47B2CF7FU,0x30B5FFE9U, + 0xBDBDF21CU,0xCABAC28AU,0x53B39330U,0x24B4A3A6U, + 0xBAD03605U,0xCDD70693U,0x54DE5729U,0x23D967BFU, + 0xB3667A2EU,0xC4614AB8U,0x5D681B02U,0x2A6F2B94U, + 0xB40BBE37U,0xC30C8EA1U,0x5A05DF1BU,0x2D02EF8DU, + }; + + crc = crc ^ 0xFFFFFFFFU; + while (len > 0) + { + crc = BAKELITE_CONST_32(table[*uData ^ (uint8_t)crc]) ^ (crc >> 8); + uData++; + len--; + } + crc = crc ^ 0xFFFFFFFFU; + return crc; + } +}; + + /* + * + * COBS Framer + * + */ + enum class CobsDecodeState { + Decoded, + NotReady, + DecodeFailure, + CrcFailure, + BufferOverrun, +}; + +template +class CobsFramer { +public: + struct Result { + int status; + size_t length; + char *data; + }; + + struct DecodeResult { + CobsDecodeState status; + size_t length; + 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_buffer + messageOffset(); + } + + size_t readBufferSize() { + return bufferSize(); + } + + char *writeBuffer() { + return m_buffer + messageOffset(); + } + + size_t writeBufferSize() { + return bufferSize(); + } + + Result encodeFrame(const char *data, size_t length) { + char *msgStart = m_buffer + messageOffset(); + memcpy(msgStart, data, length); + return encodeFrame(length); + } + + Result encodeFrame(size_t length) { + char *msgStart = m_buffer + messageOffset(); + + if(C::size() > 0) { + C crc; + crc.update(msgStart, length); + auto crc_val = crc.value(); + memcpy(msgStart + length, (void *)&crc_val, sizeof(crc_val)); + } + + auto result = cobs_encode((void *)m_buffer, sizeof(m_buffer), + (void *)msgStart, length + C::size()); + if(result.status != 0) { + return { 1, 0, nullptr }; + } + + m_buffer[result.out_len] = 0; + + return { 0, result.out_len + 1, m_buffer }; + } + + DecodeResult readFrameByte(char byte) { + *m_readPos = byte; + size_t length = (m_readPos - m_buffer) + 1; + if(byte == 0) { + m_readPos = m_buffer; + return decodeFrame(length); + } + else if(length == sizeof(m_buffer)) { + m_readPos = m_buffer; + return { CobsDecodeState::BufferOverrun, 0, nullptr }; + } + + m_readPos++; + return { CobsDecodeState::NotReady, 0, nullptr }; + } + +private: + DecodeResult decodeFrame(size_t length) { + if(length == 1) { + return { CobsDecodeState::DecodeFailure, 0, nullptr }; + } + + length--; // Discard null byte + + // 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 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_buffer + length, sizeof(crc_val)); + + crc.update(m_buffer, length); + if(crc_val != crc.value()) { + return { CobsDecodeState::CrcFailure, 0, nullptr }; + } + } + + // 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; + } + + constexpr static size_t messageOffset() { + return cobsOverhead(BufferSize + C::size()); + } + + 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 + ***************/ + +#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)) +#define COBS_ENCODE_SRC_OFFSET(SRC_LEN) (((SRC_LEN) + 253u)/254u) + +typedef enum +{ + COBS_ENCODE_OK = 0x00, + COBS_ENCODE_NULL_POINTER = 0x01, + COBS_ENCODE_OUT_BUFFER_OVERFLOW = 0x02 +} cobs_encode_status; + +struct cobs_encode_result +{ + size_t out_len; + int status; +}; + +typedef enum +{ + COBS_DECODE_OK = 0x00, + COBS_DECODE_NULL_POINTER = 0x01, + COBS_DECODE_OUT_BUFFER_OVERFLOW = 0x02, + COBS_DECODE_ZERO_BYTE_IN_INPUT = 0x04, + COBS_DECODE_INPUT_TOO_SHORT = 0x08 +} cobs_decode_status; + +/* COBS-encode a string of input bytes. +* +* dst_buf_ptr: The buffer into which the result will be written +* dst_buf_len: Length of the buffer into which the result will be written +* src_ptr: The byte string to be encoded +* src_len Length of the byte string to be encoded +* +* returns: A struct containing the success status of the encoding +* operation and the length of the result (that was written to +* dst_buf_ptr) +*/ +static cobs_encode_result cobs_encode(void *dst_buf_ptr, size_t dst_buf_len, + const void *src_ptr, size_t src_len) +{ + cobs_encode_result result = {0, COBS_ENCODE_OK}; + const uint8_t *src_read_ptr = (uint8_t *)src_ptr; + const uint8_t *src_end_ptr = (uint8_t *)src_read_ptr + src_len; + uint8_t *dst_buf_start_ptr = (uint8_t *)dst_buf_ptr; + uint8_t *dst_buf_end_ptr = dst_buf_start_ptr + dst_buf_len; + uint8_t *dst_code_write_ptr = (uint8_t *)dst_buf_ptr; + uint8_t *dst_write_ptr = dst_code_write_ptr + 1; + uint8_t src_byte = 0; + uint8_t search_len = 1; + + /* First, do a NULL pointer check and return immediately if it fails. */ + if ((dst_buf_ptr == NULL) || (src_ptr == NULL)) + { + result.status = COBS_ENCODE_NULL_POINTER; + return result; + } + + if (src_len != 0) + { + /* Iterate over the source bytes */ + for (;;) + { + /* Check for running out of output buffer space */ + if (dst_write_ptr >= dst_buf_end_ptr) + { + result.status |= COBS_ENCODE_OUT_BUFFER_OVERFLOW; + break; + } + + src_byte = *src_read_ptr++; + if (src_byte == 0) + { + /* We found a zero byte */ + *dst_code_write_ptr = search_len; + dst_code_write_ptr = dst_write_ptr++; + search_len = 1; + if (src_read_ptr >= src_end_ptr) + { + break; + } + } + else + { + /* Copy the non-zero byte to the destination buffer */ + *dst_write_ptr++ = src_byte; + search_len++; + if (src_read_ptr >= src_end_ptr) + { + break; + } + if (search_len == 0xFF) + { + /* We have a long string of non-zero bytes, so we need + * to write out a length code of 0xFF. */ + *dst_code_write_ptr = search_len; + dst_code_write_ptr = dst_write_ptr++; + search_len = 1; + } + } + } + } + + /* We've reached the end of the source data (or possibly run out of output buffer) + * Finalise the remaining output. In particular, write the code (length) byte. + * Update the pointer to calculate the final output length. + */ + if (dst_code_write_ptr >= dst_buf_end_ptr) + { + /* We've run out of output buffer to write the code byte. */ + result.status |= COBS_ENCODE_OUT_BUFFER_OVERFLOW; + dst_write_ptr = dst_buf_end_ptr; + } + else + { + /* Write the last code (length) byte. */ + *dst_code_write_ptr = search_len; + } + + /* Calculate the output length, from the value of dst_code_write_ptr */ + result.out_len = dst_write_ptr - dst_buf_start_ptr; + + return result; +} + +/* Decode a COBS byte string. +* +* dst_buf_ptr: The buffer into which the result will be written +* dst_buf_len: Length of the buffer into which the result will be written +* src_ptr: The byte string to be decoded +* src_len Length of the byte string to be decoded +* +* returns: A struct containing the success status of the decoding +* operation and the length of the result (that was written to +* dst_buf_ptr) +*/ +static cobs_decode_result cobs_decode(void *dst_buf_ptr, size_t dst_buf_len, + const void *src_ptr, size_t src_len) +{ + cobs_decode_result result = {0, COBS_DECODE_OK}; + const uint8_t *src_read_ptr = (uint8_t *)src_ptr; + const uint8_t *src_end_ptr = (uint8_t *)src_read_ptr + src_len; + uint8_t *dst_buf_start_ptr = (uint8_t *)dst_buf_ptr; + uint8_t *dst_buf_end_ptr = dst_buf_start_ptr + dst_buf_len; + uint8_t *dst_write_ptr = (uint8_t *)dst_buf_ptr; + size_t remaining_bytes; + uint8_t src_byte; + uint8_t i; + uint8_t len_code; + + /* First, do a NULL pointer check and return immediately if it fails. */ + if ((dst_buf_ptr == NULL) || (src_ptr == NULL)) + { + result.status = COBS_DECODE_NULL_POINTER; + return result; + } + + if (src_len != 0) + { + for (;;) + { + len_code = *src_read_ptr++; + if (len_code == 0) + { + result.status |= COBS_DECODE_ZERO_BYTE_IN_INPUT; + break; + } + len_code--; + + /* Check length code against remaining input bytes */ + remaining_bytes = src_end_ptr - src_read_ptr; + if (len_code > remaining_bytes) + { + result.status |= COBS_DECODE_INPUT_TOO_SHORT; + len_code = remaining_bytes; + } + + /* Check length code against remaining output buffer space */ + remaining_bytes = dst_buf_end_ptr - dst_write_ptr; + if (len_code > remaining_bytes) + { + result.status |= COBS_DECODE_OUT_BUFFER_OVERFLOW; + len_code = remaining_bytes; + } + + for (i = len_code; i != 0; i--) + { + src_byte = *src_read_ptr++; + if (src_byte == 0) + { + result.status |= COBS_DECODE_ZERO_BYTE_IN_INPUT; + } + *dst_write_ptr++ = src_byte; + } + + if (src_read_ptr >= src_end_ptr) + { + break; + } + + /* Add a zero to the end */ + if (len_code != 0xFE) + { + if (dst_write_ptr >= dst_buf_end_ptr) + { + result.status |= COBS_DECODE_OUT_BUFFER_OVERFLOW; + break; + } + *dst_write_ptr++ = 0; + } + } + } + + result.out_len = dst_write_ptr - dst_buf_start_ptr; + + return result; +} + +} + +/* +The MIT License +--------------- + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +#endif // __BAKELITE_H__ diff --git a/examples/chat/cpptiny/client.cpp b/examples/chat/cpptiny/client.cpp new file mode 100644 index 0000000..0b6f908 --- /dev/null +++ b/examples/chat/cpptiny/client.cpp @@ -0,0 +1,97 @@ +#include "proto.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static int sock_fd = -1; + +static int read_byte() { + uint8_t byte; + ssize_t n = recv(sock_fd, &byte, 1, MSG_DONTWAIT); + if (n == 1) return byte; + return -1; +} + +static size_t write_bytes(const char* data, size_t len) { + return send(sock_fd, data, len, 0); +} + +static bool has_input() { + struct pollfd pfd = {STDIN_FILENO, POLLIN, 0}; + return poll(&pfd, 1, 0) > 0 && (pfd.revents & POLLIN); +} + +int main() { + sock_fd = socket(AF_INET, SOCK_STREAM, 0); + if (sock_fd < 0) { + perror("socket"); + return 1; + } + + struct sockaddr_in addr = {}; + addr.sin_family = AF_INET; + addr.sin_port = htons(7032); + inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr); + + if (connect(sock_fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) { + perror("connect"); + return 1; + } + printf("Connected to server.\n"); + + Protocol proto(read_byte, write_bytes); + char my_name[32] = "client"; + char peer_name[32] = "server"; + + printf("> "); + fflush(stdout); + + while (true) { + auto msg = proto.poll(); + if (msg == Protocol::Message::ChatMessage) { + auto& chat = proto.message(); + printf("\r%s > %s\n> ", chat.sender, chat.text); + fflush(stdout); + } else if (msg == Protocol::Message::SetName) { + auto& setName = proto.message(); + printf("\r* %s is now known as %s\n> ", peer_name, setName.name); + fflush(stdout); + strncpy(peer_name, setName.name, sizeof(peer_name) - 1); + peer_name[sizeof(peer_name) - 1] = '\0'; + } + + if (has_input()) { + char line[300]; + if (!fgets(line, sizeof(line), stdin)) break; + line[strcspn(line, "\n")] = '\0'; + + if (strncmp(line, "/name ", 6) == 0) { + strncpy(my_name, line + 6, sizeof(my_name) - 1); + my_name[sizeof(my_name) - 1] = '\0'; + auto& setName = proto.message(); + strncpy(setName.name, my_name, sizeof(setName.name)); + proto.send(); + } else if (strlen(line) > 0) { + auto& chat = proto.message(); + strncpy(chat.sender, my_name, sizeof(chat.sender)); + strncpy(chat.text, line, sizeof(chat.text)); + proto.send(); + } + + printf("> "); + fflush(stdout); + } + + usleep(1000); + } + + close(sock_fd); + return 0; +} diff --git a/examples/chat/cpptiny/proto.h b/examples/chat/cpptiny/proto.h new file mode 100644 index 0000000..c696458 --- /dev/null +++ b/examples/chat/cpptiny/proto.h @@ -0,0 +1,215 @@ +#pragma once + +#include "bakelite.h" + +// 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)) ChatMessage { + char sender[32]; + char text[256]; + + template + int pack(T &stream) const { + int rcode = 0; + rcode = writeString(stream, sender, 32); + if(rcode != 0) + return rcode; + rcode = writeString(stream, text, 256); + if(rcode != 0) + return rcode; + return rcode; + } + + template + int unpack(T &stream) { + int rcode = 0; + rcode = readString(stream, sender, 32); + if(rcode != 0) + return rcode; + rcode = readString(stream, text, 256); + if(rcode != 0) + return rcode; + return rcode; + } +}; + + + +struct __attribute__((packed)) SetName { + char name[32]; + + template + int pack(T &stream) const { + int rcode = 0; + rcode = writeString(stream, name, 32); + if(rcode != 0) + return rcode; + return rcode; + } + + template + int unpack(T &stream) { + int rcode = 0; + rcode = readString(stream, name, 32); + if(rcode != 0) + return rcode; + return rcode; + } +}; + + + +template > +class ProtocolBase { +public: + using ReadFn = int (*)(); + using WriteFn = size_t (*)(const char *data, size_t length); + + enum class Message { + NoMessage = -1, + ChatMessage = 1, + SetName = 2, + }; + + ProtocolBase(ReadFn read, WriteFn write): m_readFn(read), m_writeFn(write) {} + + Message poll() { + int byte = (*m_readFn)(); + if(byte < 0) { + return Message::NoMessage; + } + + auto result = m_framer.readFrameByte((char)byte); + if(result.status == Bakelite::CobsDecodeState::Decoded) { + if(result.length == 0) { + return Message::NoMessage; + } + + m_receivedMessage = (Message)result.data[0]; + m_receivedFrameLength = result.length - 1; + return m_receivedMessage; + } + + 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 ChatMessage*) { + m_framer.buffer()[0] = static_cast(Message::ChatMessage); + size_t frameSize = sizeof(ChatMessage) + 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 SetName*) { + m_framer.buffer()[0] = static_cast(Message::SetName); + size_t frameSize = sizeof(SetName) + 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 ChatMessage &val) { + Bakelite::BufferStream outStream(m_framer.buffer() + 1, m_framer.bufferSize() - 1); + m_framer.buffer()[0] = static_cast(Message::ChatMessage); + size_t startPos = outStream.pos(); + val.pack(outStream); + size_t frameSize = (outStream.pos() - startPos) + 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 SetName &val) { + Bakelite::BufferStream outStream(m_framer.buffer() + 1, m_framer.bufferSize() - 1); + m_framer.buffer()[0] = static_cast(Message::SetName); + size_t startPos = outStream.pos(); + val.pack(outStream); + size_t frameSize = (outStream.pos() - startPos) + 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; + } + + // Copy-based decode (works with variable-length fields, compatible with both modes) + int decode(ChatMessage &val, char *buffer = nullptr, size_t length = 0) { + if(m_receivedMessage != Message::ChatMessage) { + return -1; + } + Bakelite::BufferStream stream( + m_framer.buffer() + 1, m_receivedFrameLength, + buffer, length + ); + return val.unpack(stream); + } + + int decode(SetName &val, char *buffer = nullptr, size_t length = 0) { + if(m_receivedMessage != Message::SetName) { + return -1; + } + Bakelite::BufferStream stream( + m_framer.buffer() + 1, m_receivedFrameLength, + buffer, length + ); + return val.unpack(stream); + } + +private: + ReadFn m_readFn; + WriteFn m_writeFn; + F m_framer; + + size_t m_receivedFrameLength = 0; + Message m_receivedMessage = Message::NoMessage; +}; + +using Protocol = ProtocolBase<>; + + diff --git a/examples/chat/cpptiny/server.cpp b/examples/chat/cpptiny/server.cpp new file mode 100644 index 0000000..6cd6c81 --- /dev/null +++ b/examples/chat/cpptiny/server.cpp @@ -0,0 +1,112 @@ +#include "proto.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +static int conn_fd = -1; + +static int read_byte() { + uint8_t byte; + ssize_t n = recv(conn_fd, &byte, 1, MSG_DONTWAIT); + if (n == 1) return byte; + return -1; +} + +static size_t write_bytes(const char* data, size_t len) { + return send(conn_fd, data, len, 0); +} + +static bool has_input() { + struct pollfd pfd = {STDIN_FILENO, POLLIN, 0}; + return poll(&pfd, 1, 0) > 0 && (pfd.revents & POLLIN); +} + +int main() { + int listen_fd = socket(AF_INET, SOCK_STREAM, 0); + if (listen_fd < 0) { + perror("socket"); + return 1; + } + + int opt = 1; + setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); + + struct sockaddr_in addr = {}; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = INADDR_ANY; + addr.sin_port = htons(7032); + + if (bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) { + perror("bind"); + return 1; + } + + if (listen(listen_fd, 1) < 0) { + perror("listen"); + return 1; + } + + printf("Listening on port 7032...\n"); + conn_fd = accept(listen_fd, nullptr, nullptr); + if (conn_fd < 0) { + perror("accept"); + return 1; + } + printf("Client connected.\n"); + + Protocol proto(read_byte, write_bytes); + char my_name[32] = "server"; + char peer_name[32] = "client"; + + printf("> "); + fflush(stdout); + + while (true) { + auto msg = proto.poll(); + if (msg == Protocol::Message::ChatMessage) { + auto& chat = proto.message(); + printf("\r%s > %s\n> ", chat.sender, chat.text); + fflush(stdout); + } else if (msg == Protocol::Message::SetName) { + auto& setName = proto.message(); + printf("\r* %s is now known as %s\n> ", peer_name, setName.name); + fflush(stdout); + strncpy(peer_name, setName.name, sizeof(peer_name) - 1); + peer_name[sizeof(peer_name) - 1] = '\0'; + } + + if (has_input()) { + char line[300]; + if (!fgets(line, sizeof(line), stdin)) break; + line[strcspn(line, "\n")] = '\0'; + + if (strncmp(line, "/name ", 6) == 0) { + strncpy(my_name, line + 6, sizeof(my_name) - 1); + my_name[sizeof(my_name) - 1] = '\0'; + auto& setName = proto.message(); + strncpy(setName.name, my_name, sizeof(setName.name)); + proto.send(); + } else if (strlen(line) > 0) { + auto& chat = proto.message(); + strncpy(chat.sender, my_name, sizeof(chat.sender)); + strncpy(chat.text, line, sizeof(chat.text)); + proto.send(); + } + + printf("> "); + fflush(stdout); + } + + usleep(1000); + } + + close(conn_fd); + close(listen_fd); + return 0; +} diff --git a/examples/chat/ctiny/CMakeLists.txt b/examples/chat/ctiny/CMakeLists.txt new file mode 100644 index 0000000..4262a62 --- /dev/null +++ b/examples/chat/ctiny/CMakeLists.txt @@ -0,0 +1,39 @@ +cmake_minimum_required(VERSION 3.10) +project(bakelite_chat_ctiny C) + +set(CMAKE_C_STANDARD 99) +set(CMAKE_C_STANDARD_REQUIRED ON) + +# Find bakelite code generator +find_program(BAKELITE_EXECUTABLE bakelite REQUIRED) + +# Protocol definition +set(PROTO_DEF "${CMAKE_CURRENT_SOURCE_DIR}/../chat.bakelite") + +# Generated files +set(GENERATED_PROTO "${CMAKE_CURRENT_SOURCE_DIR}/proto.h") +set(GENERATED_RUNTIME "${CMAKE_CURRENT_SOURCE_DIR}/bakelite.h") + +# Generate protocol header from .bakelite file +add_custom_command( + OUTPUT ${GENERATED_PROTO} + COMMAND ${BAKELITE_EXECUTABLE} gen -l ctiny -i ${PROTO_DEF} -o ${GENERATED_PROTO} + DEPENDS ${PROTO_DEF} + COMMENT "Generating proto.h from chat.bakelite" +) + +# Generate runtime header +add_custom_command( + OUTPUT ${GENERATED_RUNTIME} + COMMAND ${BAKELITE_EXECUTABLE} runtime -l ctiny -o ${GENERATED_RUNTIME} + COMMENT "Generating bakelite.h runtime" +) + +# Custom target for generated files +add_custom_target(generate_protocol DEPENDS ${GENERATED_PROTO} ${GENERATED_RUNTIME}) + +add_executable(server server.c) +add_executable(client client.c) + +add_dependencies(server generate_protocol) +add_dependencies(client generate_protocol) diff --git a/examples/chat/ctiny/Makefile b/examples/chat/ctiny/Makefile new file mode 100644 index 0000000..e2a1b1c --- /dev/null +++ b/examples/chat/ctiny/Makefile @@ -0,0 +1,21 @@ +.PHONY: all cmake build clean run-server run-client + +all: build + +build: build/Makefile + $(MAKE) -C build + +build/Makefile: + cmake -B build + +cmake: + rm -rf build && cmake -B build + +clean: + rm -rf build + +run-server: build + ./build/server + +run-client: build + ./build/client diff --git a/examples/chat/ctiny/README.md b/examples/chat/ctiny/README.md new file mode 100644 index 0000000..5349c55 --- /dev/null +++ b/examples/chat/ctiny/README.md @@ -0,0 +1,51 @@ +# Bakelite Chat Example (C) + +A simple TCP chat demonstrating the bakelite ctiny runtime. + +## Build + +```bash +cmake -B build +cmake --build build +``` + +Or using the Makefile wrapper: +```bash +make +``` + +The build automatically generates `proto.h` and `bakelite.h` from `chat.bakelite` via CMake custom commands. + +## Usage + +Start the server in one terminal: +```bash +./build/server +``` + +Connect with the client in another: +```bash +./build/client +``` + +Type messages and press Enter to send. Use `/name ` to change your display name. + +## CMake Integration + +This example demonstrates integrating bakelite code generation into CMake. The key parts: + +```cmake +find_program(BAKELITE_EXECUTABLE bakelite REQUIRED) + +add_custom_command( + OUTPUT proto.h + COMMAND ${BAKELITE_EXECUTABLE} gen -l ctiny -i ../chat.bakelite -o proto.h + DEPENDS ../chat.bakelite +) +``` + +Generated files are rebuilt automatically when `chat.bakelite` changes. + +## Interoperability + +Uses the same protocol as the cpptiny (C++) version. You can run a C server with a C++ client or vice versa. diff --git a/examples/chat/ctiny/bakelite.h b/examples/chat/ctiny/bakelite.h new file mode 100644 index 0000000..b50b104 --- /dev/null +++ b/examples/chat/ctiny/bakelite.h @@ -0,0 +1,1031 @@ +/* + * Bakelite C Runtime + * Copyright (c) Emma Powers 2021-2024 + * + * C99 compatible header-only implementation for embedded systems. + * Licensed under the MIT License (see end of file). + */ + +#ifndef BAKELITE_H +#define BAKELITE_H + +#ifdef __AVR__ +#include +#endif + +/* + * Type Definitions + */ +#include +#include +#include +#include + +/* Packed struct attribute for zero-copy mode */ +#if defined(__GNUC__) || defined(__clang__) + #define BAKELITE_PACKED __attribute__((packed)) +#elif defined(_MSC_VER) + #define BAKELITE_PACKED + #pragma pack(push, 1) +#else + #define BAKELITE_PACKED +#endif + +/* Static assert for compile-time checks (C99 compatible) */ +#define BAKELITE_STATIC_ASSERT(cond, msg) \ + typedef char bakelite_static_assert_##msg[(cond) ? 1 : -1] + +/* Platform detection for unaligned access support */ +#if defined(__AVR__) || defined(__arm__) || defined(__aarch64__) || \ + defined(__i386__) || defined(__x86_64__) || defined(_M_IX86) || defined(_M_X64) + #define BAKELITE_UNALIGNED_OK 1 +#else + #define BAKELITE_UNALIGNED_OK 0 +#endif + +/* Error codes */ +#define BAKELITE_OK 0 +#define BAKELITE_ERR_WRITE -1 +#define BAKELITE_ERR_READ -2 +#define BAKELITE_ERR_SEEK -3 +#define BAKELITE_ERR_ALLOC -4 +#define BAKELITE_ERR_ALLOC_BYTES -5 +#define BAKELITE_ERR_ALLOC_STRING -6 + +/* Sized array for variable-length data */ +typedef struct { + void *data; + uint8_t size; +} Bakelite_SizedArray; + +/* Sized array with 16-bit size */ +typedef struct { + void *data; + uint16_t size; +} Bakelite_SizedArray16; + + +/* + * Buffer Stream + */ +/* Buffer stream for serialization/deserialization */ +typedef struct { + uint8_t *buffer; + uint32_t size; + uint32_t pos; + /* Heap for variable-length data allocations */ + uint8_t *heap; + uint32_t heap_size; + uint32_t heap_pos; +} Bakelite_Buffer; + +/* Initialize a buffer stream (without heap) */ +static inline void bakelite_buffer_init(Bakelite_Buffer *buf, uint8_t *data, uint32_t size) { + buf->buffer = data; + buf->size = size; + buf->pos = 0; + buf->heap = NULL; + buf->heap_size = 0; + buf->heap_pos = 0; +} + +/* Initialize a buffer stream with heap for variable-length data */ +static inline void bakelite_buffer_init_with_heap(Bakelite_Buffer *buf, + uint8_t *data, uint32_t size, + uint8_t *heap, uint32_t heap_size) { + buf->buffer = data; + buf->size = size; + buf->pos = 0; + buf->heap = heap; + buf->heap_size = heap_size; + buf->heap_pos = 0; +} + +/* Reset buffer position to start */ +static inline void bakelite_buffer_reset(Bakelite_Buffer *buf) { + buf->pos = 0; + buf->heap_pos = 0; +} + +/* Write data to buffer */ +static inline int bakelite_buffer_write(Bakelite_Buffer *buf, const void *data, uint32_t length) { + uint32_t end_pos = buf->pos + length; + if (end_pos > buf->size) { + return BAKELITE_ERR_WRITE; + } + memcpy(buf->buffer + buf->pos, data, length); + buf->pos += length; + return BAKELITE_OK; +} + +/* Read data from buffer */ +static inline int bakelite_buffer_read(Bakelite_Buffer *buf, void *data, uint32_t length) { + uint32_t end_pos = buf->pos + length; + if (end_pos > buf->size) { + return BAKELITE_ERR_READ; + } + memcpy(data, buf->buffer + buf->pos, length); + buf->pos += length; + return BAKELITE_OK; +} + +/* Seek to position */ +static inline int bakelite_buffer_seek(Bakelite_Buffer *buf, uint32_t pos) { + if (pos >= buf->size) { + return BAKELITE_ERR_SEEK; + } + buf->pos = pos; + return BAKELITE_OK; +} + +/* Allocate from heap (for variable-length data during deserialization) */ +static inline void *bakelite_buffer_alloc(Bakelite_Buffer *buf, uint32_t bytes) { + uint32_t new_pos = buf->heap_pos + bytes; + if (buf->heap == NULL || new_pos > buf->heap_size) { + return NULL; + } + void *data = buf->heap + buf->heap_pos; + buf->heap_pos = new_pos; + return data; +} + +/* Get current position */ +static inline uint32_t bakelite_buffer_pos(const Bakelite_Buffer *buf) { + return buf->pos; +} + +/* Get buffer size */ +static inline uint32_t bakelite_buffer_size(const Bakelite_Buffer *buf) { + return buf->size; +} + +/* Get remaining bytes */ +static inline uint32_t bakelite_buffer_remaining(const Bakelite_Buffer *buf) { + return buf->size - buf->pos; +} + + +/* + * Serialization Functions + */ +/* Write primitives */ +static inline int bakelite_write_bool(Bakelite_Buffer *buf, bool val) { + return bakelite_buffer_write(buf, &val, sizeof(val)); +} + +static inline int bakelite_write_int8(Bakelite_Buffer *buf, int8_t val) { + return bakelite_buffer_write(buf, &val, sizeof(val)); +} + +static inline int bakelite_write_uint8(Bakelite_Buffer *buf, uint8_t val) { + return bakelite_buffer_write(buf, &val, sizeof(val)); +} + +static inline int bakelite_write_int16(Bakelite_Buffer *buf, int16_t val) { + return bakelite_buffer_write(buf, &val, sizeof(val)); +} + +static inline int bakelite_write_uint16(Bakelite_Buffer *buf, uint16_t val) { + return bakelite_buffer_write(buf, &val, sizeof(val)); +} + +static inline int bakelite_write_int32(Bakelite_Buffer *buf, int32_t val) { + return bakelite_buffer_write(buf, &val, sizeof(val)); +} + +static inline int bakelite_write_uint32(Bakelite_Buffer *buf, uint32_t val) { + return bakelite_buffer_write(buf, &val, sizeof(val)); +} + +static inline int bakelite_write_int64(Bakelite_Buffer *buf, int64_t val) { + return bakelite_buffer_write(buf, &val, sizeof(val)); +} + +static inline int bakelite_write_uint64(Bakelite_Buffer *buf, uint64_t val) { + return bakelite_buffer_write(buf, &val, sizeof(val)); +} + +static inline int bakelite_write_float32(Bakelite_Buffer *buf, float val) { + return bakelite_buffer_write(buf, &val, sizeof(val)); +} + +static inline int bakelite_write_float64(Bakelite_Buffer *buf, double val) { + return bakelite_buffer_write(buf, &val, sizeof(val)); +} + +/* Write fixed-size bytes */ +static inline int bakelite_write_bytes_fixed(Bakelite_Buffer *buf, const uint8_t *val, uint32_t size) { + return bakelite_buffer_write(buf, val, size); +} + +/* Write variable-length bytes (with length prefix) */ +static inline int bakelite_write_bytes(Bakelite_Buffer *buf, const Bakelite_SizedArray *val) { + int rcode = bakelite_write_uint8(buf, val->size); + if (rcode != BAKELITE_OK) return rcode; + return bakelite_buffer_write(buf, val->data, val->size); +} + +/* Write fixed-size string */ +static inline int bakelite_write_string_fixed(Bakelite_Buffer *buf, const char *val, uint32_t size) { + return bakelite_buffer_write(buf, val, size); +} + +/* Write null-terminated string */ +static inline int bakelite_write_string(Bakelite_Buffer *buf, const char *val) { + if (val == NULL) { + return bakelite_write_uint8(buf, 0); + } + uint32_t len = (uint32_t)strlen(val); + int rcode = bakelite_buffer_write(buf, val, len); + if (rcode != BAKELITE_OK) return rcode; + return bakelite_write_uint8(buf, 0); +} + +/* Read primitives */ +static inline int bakelite_read_bool(Bakelite_Buffer *buf, bool *val) { + return bakelite_buffer_read(buf, val, sizeof(*val)); +} + +static inline int bakelite_read_int8(Bakelite_Buffer *buf, int8_t *val) { + return bakelite_buffer_read(buf, val, sizeof(*val)); +} + +static inline int bakelite_read_uint8(Bakelite_Buffer *buf, uint8_t *val) { + return bakelite_buffer_read(buf, val, sizeof(*val)); +} + +static inline int bakelite_read_int16(Bakelite_Buffer *buf, int16_t *val) { + return bakelite_buffer_read(buf, val, sizeof(*val)); +} + +static inline int bakelite_read_uint16(Bakelite_Buffer *buf, uint16_t *val) { + return bakelite_buffer_read(buf, val, sizeof(*val)); +} + +static inline int bakelite_read_int32(Bakelite_Buffer *buf, int32_t *val) { + return bakelite_buffer_read(buf, val, sizeof(*val)); +} + +static inline int bakelite_read_uint32(Bakelite_Buffer *buf, uint32_t *val) { + return bakelite_buffer_read(buf, val, sizeof(*val)); +} + +static inline int bakelite_read_int64(Bakelite_Buffer *buf, int64_t *val) { + return bakelite_buffer_read(buf, val, sizeof(*val)); +} + +static inline int bakelite_read_uint64(Bakelite_Buffer *buf, uint64_t *val) { + return bakelite_buffer_read(buf, val, sizeof(*val)); +} + +static inline int bakelite_read_float32(Bakelite_Buffer *buf, float *val) { + return bakelite_buffer_read(buf, val, sizeof(*val)); +} + +static inline int bakelite_read_float64(Bakelite_Buffer *buf, double *val) { + return bakelite_buffer_read(buf, val, sizeof(*val)); +} + +/* Read fixed-size bytes */ +static inline int bakelite_read_bytes_fixed(Bakelite_Buffer *buf, uint8_t *val, uint32_t size) { + return bakelite_buffer_read(buf, val, size); +} + +/* Read variable-length bytes (with length prefix, allocates from heap) */ +static inline int bakelite_read_bytes(Bakelite_Buffer *buf, Bakelite_SizedArray *val) { + uint8_t size; + int rcode = bakelite_read_uint8(buf, &size); + if (rcode != BAKELITE_OK) return rcode; + + val->data = bakelite_buffer_alloc(buf, size); + val->size = size; + + if (val->data == NULL) { + return BAKELITE_ERR_ALLOC_BYTES; + } + + return bakelite_buffer_read(buf, val->data, size); +} + +/* Read fixed-size string */ +static inline int bakelite_read_string_fixed(Bakelite_Buffer *buf, char *val, uint32_t size) { + return bakelite_buffer_read(buf, val, size); +} + +/* Read null-terminated string (allocates from heap) */ +static inline int bakelite_read_string(Bakelite_Buffer *buf, char **val) { + char *start = (char *)bakelite_buffer_alloc(buf, 1); + *val = start; + + if (start == NULL) { + return BAKELITE_ERR_ALLOC_STRING; + } + + char *current = start; + for (;;) { + int rcode = bakelite_buffer_read(buf, current, 1); + if (rcode != BAKELITE_OK) return rcode; + + if (*current == '\0') { + return BAKELITE_OK; + } + + current = (char *)bakelite_buffer_alloc(buf, 1); + if (current == NULL) { + return BAKELITE_ERR_ALLOC_STRING; + } + } +} + +/* Write array of primitives (fixed size) */ +#define BAKELITE_DEFINE_WRITE_ARRAY(type, name) \ + static inline int bakelite_write_array_##name(Bakelite_Buffer *buf, const type *arr, uint32_t count) { \ + for (uint32_t i = 0; i < count; i++) { \ + int rcode = bakelite_write_##name(buf, arr[i]); \ + if (rcode != BAKELITE_OK) return rcode; \ + } \ + return BAKELITE_OK; \ + } + +BAKELITE_DEFINE_WRITE_ARRAY(bool, bool) +BAKELITE_DEFINE_WRITE_ARRAY(int8_t, int8) +BAKELITE_DEFINE_WRITE_ARRAY(uint8_t, uint8) +BAKELITE_DEFINE_WRITE_ARRAY(int16_t, int16) +BAKELITE_DEFINE_WRITE_ARRAY(uint16_t, uint16) +BAKELITE_DEFINE_WRITE_ARRAY(int32_t, int32) +BAKELITE_DEFINE_WRITE_ARRAY(uint32_t, uint32) +BAKELITE_DEFINE_WRITE_ARRAY(int64_t, int64) +BAKELITE_DEFINE_WRITE_ARRAY(uint64_t, uint64) +BAKELITE_DEFINE_WRITE_ARRAY(float, float32) +BAKELITE_DEFINE_WRITE_ARRAY(double, float64) + +/* Read array of primitives (fixed size) */ +#define BAKELITE_DEFINE_READ_ARRAY(type, name) \ + static inline int bakelite_read_array_##name(Bakelite_Buffer *buf, type *arr, uint32_t count) { \ + for (uint32_t i = 0; i < count; i++) { \ + int rcode = bakelite_read_##name(buf, &arr[i]); \ + if (rcode != BAKELITE_OK) return rcode; \ + } \ + return BAKELITE_OK; \ + } + +BAKELITE_DEFINE_READ_ARRAY(bool, bool) +BAKELITE_DEFINE_READ_ARRAY(int8_t, int8) +BAKELITE_DEFINE_READ_ARRAY(uint8_t, uint8) +BAKELITE_DEFINE_READ_ARRAY(int16_t, int16) +BAKELITE_DEFINE_READ_ARRAY(uint16_t, uint16) +BAKELITE_DEFINE_READ_ARRAY(int32_t, int32) +BAKELITE_DEFINE_READ_ARRAY(uint32_t, uint32) +BAKELITE_DEFINE_READ_ARRAY(int64_t, int64) +BAKELITE_DEFINE_READ_ARRAY(uint64_t, uint64) +BAKELITE_DEFINE_READ_ARRAY(float, float32) +BAKELITE_DEFINE_READ_ARRAY(double, float64) + + +/* + * CRC Functions + */ +/* + * The CRC lookup tables are stored as const variables. On many platforms, + * const variables are stored in flash memory. On AVR though, they are + * loaded into RAM on startup. A special PROGMEM macro is available on AVRs + * to indicate constants should be stored in program memory (flash). + * So if this macro is available, use it and assume we're on an AVR. + */ +#ifdef PROGMEM + #define BAKELITE_CONST PROGMEM + #define BAKELITE_CONST_8(x) pgm_read_byte(&(x)) + #define BAKELITE_CONST_16(x) pgm_read_word(&(x)) + #define BAKELITE_CONST_32(x) ((uint32_t)pgm_read_dword((char *)&(x) + 2) << 16 | pgm_read_dword(&(x))) +#else + #define BAKELITE_CONST + #define BAKELITE_CONST_8(x) (x) + #define BAKELITE_CONST_16(x) (x) + #define BAKELITE_CONST_32(x) (x) +#endif + +/* CRC-8 polynomial: 0x107 */ +static inline uint8_t bakelite_crc8(const uint8_t *data, size_t len, uint8_t crc) { + static const uint8_t table[256] BAKELITE_CONST = { + 0x00U,0x07U,0x0EU,0x09U,0x1CU,0x1BU,0x12U,0x15U, + 0x38U,0x3FU,0x36U,0x31U,0x24U,0x23U,0x2AU,0x2DU, + 0x70U,0x77U,0x7EU,0x79U,0x6CU,0x6BU,0x62U,0x65U, + 0x48U,0x4FU,0x46U,0x41U,0x54U,0x53U,0x5AU,0x5DU, + 0xE0U,0xE7U,0xEEU,0xE9U,0xFCU,0xFBU,0xF2U,0xF5U, + 0xD8U,0xDFU,0xD6U,0xD1U,0xC4U,0xC3U,0xCAU,0xCDU, + 0x90U,0x97U,0x9EU,0x99U,0x8CU,0x8BU,0x82U,0x85U, + 0xA8U,0xAFU,0xA6U,0xA1U,0xB4U,0xB3U,0xBAU,0xBDU, + 0xC7U,0xC0U,0xC9U,0xCEU,0xDBU,0xDCU,0xD5U,0xD2U, + 0xFFU,0xF8U,0xF1U,0xF6U,0xE3U,0xE4U,0xEDU,0xEAU, + 0xB7U,0xB0U,0xB9U,0xBEU,0xABU,0xACU,0xA5U,0xA2U, + 0x8FU,0x88U,0x81U,0x86U,0x93U,0x94U,0x9DU,0x9AU, + 0x27U,0x20U,0x29U,0x2EU,0x3BU,0x3CU,0x35U,0x32U, + 0x1FU,0x18U,0x11U,0x16U,0x03U,0x04U,0x0DU,0x0AU, + 0x57U,0x50U,0x59U,0x5EU,0x4BU,0x4CU,0x45U,0x42U, + 0x6FU,0x68U,0x61U,0x66U,0x73U,0x74U,0x7DU,0x7AU, + 0x89U,0x8EU,0x87U,0x80U,0x95U,0x92U,0x9BU,0x9CU, + 0xB1U,0xB6U,0xBFU,0xB8U,0xADU,0xAAU,0xA3U,0xA4U, + 0xF9U,0xFEU,0xF7U,0xF0U,0xE5U,0xE2U,0xEBU,0xECU, + 0xC1U,0xC6U,0xCFU,0xC8U,0xDDU,0xDAU,0xD3U,0xD4U, + 0x69U,0x6EU,0x67U,0x60U,0x75U,0x72U,0x7BU,0x7CU, + 0x51U,0x56U,0x5FU,0x58U,0x4DU,0x4AU,0x43U,0x44U, + 0x19U,0x1EU,0x17U,0x10U,0x05U,0x02U,0x0BU,0x0CU, + 0x21U,0x26U,0x2FU,0x28U,0x3DU,0x3AU,0x33U,0x34U, + 0x4EU,0x49U,0x40U,0x47U,0x52U,0x55U,0x5CU,0x5BU, + 0x76U,0x71U,0x78U,0x7FU,0x6AU,0x6DU,0x64U,0x63U, + 0x3EU,0x39U,0x30U,0x37U,0x22U,0x25U,0x2CU,0x2BU, + 0x06U,0x01U,0x08U,0x0FU,0x1AU,0x1DU,0x14U,0x13U, + 0xAEU,0xA9U,0xA0U,0xA7U,0xB2U,0xB5U,0xBCU,0xBBU, + 0x96U,0x91U,0x98U,0x9FU,0x8AU,0x8DU,0x84U,0x83U, + 0xDEU,0xD9U,0xD0U,0xD7U,0xC2U,0xC5U,0xCCU,0xCBU, + 0xE6U,0xE1U,0xE8U,0xEFU,0xFAU,0xFDU,0xF4U,0xF3U, + }; + + while (len > 0) { + crc = BAKELITE_CONST_8(table[*data ^ crc]); + data++; + len--; + } + return crc; +} + +/* CRC-16 polynomial: 0x18005, bit reverse algorithm */ +static inline uint16_t bakelite_crc16(const uint8_t *data, size_t len, uint16_t crc) { + static const uint16_t table[256] BAKELITE_CONST = { + 0x0000U,0xC0C1U,0xC181U,0x0140U,0xC301U,0x03C0U,0x0280U,0xC241U, + 0xC601U,0x06C0U,0x0780U,0xC741U,0x0500U,0xC5C1U,0xC481U,0x0440U, + 0xCC01U,0x0CC0U,0x0D80U,0xCD41U,0x0F00U,0xCFC1U,0xCE81U,0x0E40U, + 0x0A00U,0xCAC1U,0xCB81U,0x0B40U,0xC901U,0x09C0U,0x0880U,0xC841U, + 0xD801U,0x18C0U,0x1980U,0xD941U,0x1B00U,0xDBC1U,0xDA81U,0x1A40U, + 0x1E00U,0xDEC1U,0xDF81U,0x1F40U,0xDD01U,0x1DC0U,0x1C80U,0xDC41U, + 0x1400U,0xD4C1U,0xD581U,0x1540U,0xD701U,0x17C0U,0x1680U,0xD641U, + 0xD201U,0x12C0U,0x1380U,0xD341U,0x1100U,0xD1C1U,0xD081U,0x1040U, + 0xF001U,0x30C0U,0x3180U,0xF141U,0x3300U,0xF3C1U,0xF281U,0x3240U, + 0x3600U,0xF6C1U,0xF781U,0x3740U,0xF501U,0x35C0U,0x3480U,0xF441U, + 0x3C00U,0xFCC1U,0xFD81U,0x3D40U,0xFF01U,0x3FC0U,0x3E80U,0xFE41U, + 0xFA01U,0x3AC0U,0x3B80U,0xFB41U,0x3900U,0xF9C1U,0xF881U,0x3840U, + 0x2800U,0xE8C1U,0xE981U,0x2940U,0xEB01U,0x2BC0U,0x2A80U,0xEA41U, + 0xEE01U,0x2EC0U,0x2F80U,0xEF41U,0x2D00U,0xEDC1U,0xEC81U,0x2C40U, + 0xE401U,0x24C0U,0x2580U,0xE541U,0x2700U,0xE7C1U,0xE681U,0x2640U, + 0x2200U,0xE2C1U,0xE381U,0x2340U,0xE101U,0x21C0U,0x2080U,0xE041U, + 0xA001U,0x60C0U,0x6180U,0xA141U,0x6300U,0xA3C1U,0xA281U,0x6240U, + 0x6600U,0xA6C1U,0xA781U,0x6740U,0xA501U,0x65C0U,0x6480U,0xA441U, + 0x6C00U,0xACC1U,0xAD81U,0x6D40U,0xAF01U,0x6FC0U,0x6E80U,0xAE41U, + 0xAA01U,0x6AC0U,0x6B80U,0xAB41U,0x6900U,0xA9C1U,0xA881U,0x6840U, + 0x7800U,0xB8C1U,0xB981U,0x7940U,0xBB01U,0x7BC0U,0x7A80U,0xBA41U, + 0xBE01U,0x7EC0U,0x7F80U,0xBF41U,0x7D00U,0xBDC1U,0xBC81U,0x7C40U, + 0xB401U,0x74C0U,0x7580U,0xB541U,0x7700U,0xB7C1U,0xB681U,0x7640U, + 0x7200U,0xB2C1U,0xB381U,0x7340U,0xB101U,0x71C0U,0x7080U,0xB041U, + 0x5000U,0x90C1U,0x9181U,0x5140U,0x9301U,0x53C0U,0x5280U,0x9241U, + 0x9601U,0x56C0U,0x5780U,0x9741U,0x5500U,0x95C1U,0x9481U,0x5440U, + 0x9C01U,0x5CC0U,0x5D80U,0x9D41U,0x5F00U,0x9FC1U,0x9E81U,0x5E40U, + 0x5A00U,0x9AC1U,0x9B81U,0x5B40U,0x9901U,0x59C0U,0x5880U,0x9841U, + 0x8801U,0x48C0U,0x4980U,0x8941U,0x4B00U,0x8BC1U,0x8A81U,0x4A40U, + 0x4E00U,0x8EC1U,0x8F81U,0x4F40U,0x8D01U,0x4DC0U,0x4C80U,0x8C41U, + 0x4400U,0x84C1U,0x8581U,0x4540U,0x8701U,0x47C0U,0x4680U,0x8641U, + 0x8201U,0x42C0U,0x4380U,0x8341U,0x4100U,0x81C1U,0x8081U,0x4040U, + }; + + while (len > 0) { + crc = BAKELITE_CONST_16(table[*data ^ (uint8_t)crc]) ^ (crc >> 8); + data++; + len--; + } + return crc; +} + +/* CRC-32 polynomial: 0x104C11DB7, bit reverse algorithm */ +static inline uint32_t bakelite_crc32(const uint8_t *data, size_t len, uint32_t crc) { + static const uint32_t table[256] BAKELITE_CONST = { + 0x00000000U,0x77073096U,0xEE0E612CU,0x990951BAU, + 0x076DC419U,0x706AF48FU,0xE963A535U,0x9E6495A3U, + 0x0EDB8832U,0x79DCB8A4U,0xE0D5E91EU,0x97D2D988U, + 0x09B64C2BU,0x7EB17CBDU,0xE7B82D07U,0x90BF1D91U, + 0x1DB71064U,0x6AB020F2U,0xF3B97148U,0x84BE41DEU, + 0x1ADAD47DU,0x6DDDE4EBU,0xF4D4B551U,0x83D385C7U, + 0x136C9856U,0x646BA8C0U,0xFD62F97AU,0x8A65C9ECU, + 0x14015C4FU,0x63066CD9U,0xFA0F3D63U,0x8D080DF5U, + 0x3B6E20C8U,0x4C69105EU,0xD56041E4U,0xA2677172U, + 0x3C03E4D1U,0x4B04D447U,0xD20D85FDU,0xA50AB56BU, + 0x35B5A8FAU,0x42B2986CU,0xDBBBC9D6U,0xACBCF940U, + 0x32D86CE3U,0x45DF5C75U,0xDCD60DCFU,0xABD13D59U, + 0x26D930ACU,0x51DE003AU,0xC8D75180U,0xBFD06116U, + 0x21B4F4B5U,0x56B3C423U,0xCFBA9599U,0xB8BDA50FU, + 0x2802B89EU,0x5F058808U,0xC60CD9B2U,0xB10BE924U, + 0x2F6F7C87U,0x58684C11U,0xC1611DABU,0xB6662D3DU, + 0x76DC4190U,0x01DB7106U,0x98D220BCU,0xEFD5102AU, + 0x71B18589U,0x06B6B51FU,0x9FBFE4A5U,0xE8B8D433U, + 0x7807C9A2U,0x0F00F934U,0x9609A88EU,0xE10E9818U, + 0x7F6A0DBBU,0x086D3D2DU,0x91646C97U,0xE6635C01U, + 0x6B6B51F4U,0x1C6C6162U,0x856530D8U,0xF262004EU, + 0x6C0695EDU,0x1B01A57BU,0x8208F4C1U,0xF50FC457U, + 0x65B0D9C6U,0x12B7E950U,0x8BBEB8EAU,0xFCB9887CU, + 0x62DD1DDFU,0x15DA2D49U,0x8CD37CF3U,0xFBD44C65U, + 0x4DB26158U,0x3AB551CEU,0xA3BC0074U,0xD4BB30E2U, + 0x4ADFA541U,0x3DD895D7U,0xA4D1C46DU,0xD3D6F4FBU, + 0x4369E96AU,0x346ED9FCU,0xAD678846U,0xDA60B8D0U, + 0x44042D73U,0x33031DE5U,0xAA0A4C5FU,0xDD0D7CC9U, + 0x5005713CU,0x270241AAU,0xBE0B1010U,0xC90C2086U, + 0x5768B525U,0x206F85B3U,0xB966D409U,0xCE61E49FU, + 0x5EDEF90EU,0x29D9C998U,0xB0D09822U,0xC7D7A8B4U, + 0x59B33D17U,0x2EB40D81U,0xB7BD5C3BU,0xC0BA6CADU, + 0xEDB88320U,0x9ABFB3B6U,0x03B6E20CU,0x74B1D29AU, + 0xEAD54739U,0x9DD277AFU,0x04DB2615U,0x73DC1683U, + 0xE3630B12U,0x94643B84U,0x0D6D6A3EU,0x7A6A5AA8U, + 0xE40ECF0BU,0x9309FF9DU,0x0A00AE27U,0x7D079EB1U, + 0xF00F9344U,0x8708A3D2U,0x1E01F268U,0x6906C2FEU, + 0xF762575DU,0x806567CBU,0x196C3671U,0x6E6B06E7U, + 0xFED41B76U,0x89D32BE0U,0x10DA7A5AU,0x67DD4ACCU, + 0xF9B9DF6FU,0x8EBEEFF9U,0x17B7BE43U,0x60B08ED5U, + 0xD6D6A3E8U,0xA1D1937EU,0x38D8C2C4U,0x4FDFF252U, + 0xD1BB67F1U,0xA6BC5767U,0x3FB506DDU,0x48B2364BU, + 0xD80D2BDAU,0xAF0A1B4CU,0x36034AF6U,0x41047A60U, + 0xDF60EFC3U,0xA867DF55U,0x316E8EEFU,0x4669BE79U, + 0xCB61B38CU,0xBC66831AU,0x256FD2A0U,0x5268E236U, + 0xCC0C7795U,0xBB0B4703U,0x220216B9U,0x5505262FU, + 0xC5BA3BBEU,0xB2BD0B28U,0x2BB45A92U,0x5CB36A04U, + 0xC2D7FFA7U,0xB5D0CF31U,0x2CD99E8BU,0x5BDEAE1DU, + 0x9B64C2B0U,0xEC63F226U,0x756AA39CU,0x026D930AU, + 0x9C0906A9U,0xEB0E363FU,0x72076785U,0x05005713U, + 0x95BF4A82U,0xE2B87A14U,0x7BB12BAEU,0x0CB61B38U, + 0x92D28E9BU,0xE5D5BE0DU,0x7CDCEFB7U,0x0BDBDF21U, + 0x86D3D2D4U,0xF1D4E242U,0x68DDB3F8U,0x1FDA836EU, + 0x81BE16CDU,0xF6B9265BU,0x6FB077E1U,0x18B74777U, + 0x88085AE6U,0xFF0F6A70U,0x66063BCAU,0x11010B5CU, + 0x8F659EFFU,0xF862AE69U,0x616BFFD3U,0x166CCF45U, + 0xA00AE278U,0xD70DD2EEU,0x4E048354U,0x3903B3C2U, + 0xA7672661U,0xD06016F7U,0x4969474DU,0x3E6E77DBU, + 0xAED16A4AU,0xD9D65ADCU,0x40DF0B66U,0x37D83BF0U, + 0xA9BCAE53U,0xDEBB9EC5U,0x47B2CF7FU,0x30B5FFE9U, + 0xBDBDF21CU,0xCABAC28AU,0x53B39330U,0x24B4A3A6U, + 0xBAD03605U,0xCDD70693U,0x54DE5729U,0x23D967BFU, + 0xB3667A2EU,0xC4614AB8U,0x5D681B02U,0x2A6F2B94U, + 0xB40BBE37U,0xC30C8EA1U,0x5A05DF1BU,0x2D02EF8DU, + }; + + crc = crc ^ 0xFFFFFFFFU; + while (len > 0) { + crc = BAKELITE_CONST_32(table[*data ^ (uint8_t)crc]) ^ (crc >> 8); + data++; + len--; + } + crc = crc ^ 0xFFFFFFFFU; + return crc; +} + +/* CRC size constants */ +#define BAKELITE_CRC_NOOP_SIZE 0 +#define BAKELITE_CRC8_SIZE 1 +#define BAKELITE_CRC16_SIZE 2 +#define BAKELITE_CRC32_SIZE 4 + + +/* + * COBS Framing + */ +/* + * COBS encode/decode functions are Copyright (c) 2010 Craig McQueen + * Licensed under the MIT license (see end of file). + * Source: https://github.com/cmcqueen/cobs-c + */ + +/* COBS buffer size calculations */ +#define BAKELITE_COBS_ENCODE_DST_BUF_LEN_MAX(SRC_LEN) ((SRC_LEN) + (((SRC_LEN) + 253u) / 254u)) +#define BAKELITE_COBS_DECODE_DST_BUF_LEN_MAX(SRC_LEN) (((SRC_LEN) == 0) ? 0u : ((SRC_LEN) - 1u)) +#define BAKELITE_COBS_OVERHEAD(BUF_SIZE) (((BUF_SIZE) + 253u) / 254u) + +/* Calculate total buffer size needed for framer */ +#define BAKELITE_FRAMER_BUFFER_SIZE(MAX_MSG_SIZE, CRC_SIZE) \ + (BAKELITE_COBS_OVERHEAD((MAX_MSG_SIZE) + (CRC_SIZE)) + (MAX_MSG_SIZE) + (CRC_SIZE) + 1) + +/* Message offset in buffer (after COBS overhead) */ +#define BAKELITE_FRAMER_MESSAGE_OFFSET(MAX_MSG_SIZE, CRC_SIZE) \ + BAKELITE_COBS_OVERHEAD((MAX_MSG_SIZE) + (CRC_SIZE)) + +/* Decode state enum */ +typedef enum { + BAKELITE_DECODE_OK = 0, + BAKELITE_DECODE_NOT_READY, + BAKELITE_DECODE_FAILURE, + BAKELITE_DECODE_CRC_FAILURE, + BAKELITE_DECODE_BUFFER_OVERRUN +} Bakelite_DecodeState; + +/* COBS encode/decode status */ +typedef enum { + COBS_ENCODE_OK = 0x00, + COBS_ENCODE_NULL_POINTER = 0x01, + COBS_ENCODE_OUT_BUFFER_OVERFLOW = 0x02 +} Bakelite_CobsEncodeStatus; + +typedef enum { + COBS_DECODE_OK = 0x00, + COBS_DECODE_NULL_POINTER = 0x01, + COBS_DECODE_OUT_BUFFER_OVERFLOW = 0x02, + COBS_DECODE_ZERO_BYTE_IN_INPUT = 0x04, + COBS_DECODE_INPUT_TOO_SHORT = 0x08 +} Bakelite_CobsDecodeStatus; + +/* COBS encode/decode results */ +typedef struct { + size_t out_len; + int status; +} Bakelite_CobsEncodeResult; + +typedef struct { + size_t out_len; + int status; +} Bakelite_CobsDecodeResult; + +/* Framer result */ +typedef struct { + int status; + size_t length; + uint8_t *data; +} Bakelite_FramerResult; + +/* Decode result */ +typedef struct { + Bakelite_DecodeState status; + size_t length; + uint8_t *data; +} Bakelite_DecodeResult; + +/* CRC type enum */ +typedef enum { + BAKELITE_CRC_NONE = 0, + BAKELITE_CRC_8, + BAKELITE_CRC_16, + BAKELITE_CRC_32 +} Bakelite_CrcType; + +/* COBS Framer structure */ +typedef struct { + uint8_t *buffer; + size_t buffer_size; + size_t max_message_size; + size_t message_offset; + size_t crc_size; + Bakelite_CrcType crc_type; + uint8_t *read_pos; +} Bakelite_CobsFramer; + +/* Forward declarations */ +static Bakelite_CobsEncodeResult bakelite_cobs_encode(void *dst_buf_ptr, size_t dst_buf_len, + const void *src_ptr, size_t src_len); +static Bakelite_CobsDecodeResult bakelite_cobs_decode(void *dst_buf_ptr, size_t dst_buf_len, + const void *src_ptr, size_t src_len); + +/* Get CRC size for a CRC type */ +static inline size_t bakelite_crc_size(Bakelite_CrcType crc_type) { + switch (crc_type) { + case BAKELITE_CRC_8: return 1; + case BAKELITE_CRC_16: return 2; + case BAKELITE_CRC_32: return 4; + default: return 0; + } +} + +/* Initialize framer with user-provided buffer */ +static inline void bakelite_framer_init(Bakelite_CobsFramer *framer, + uint8_t *buffer, size_t buffer_size, + size_t max_message_size, + Bakelite_CrcType crc_type) { + framer->buffer = buffer; + framer->buffer_size = buffer_size; + framer->max_message_size = max_message_size; + framer->crc_type = crc_type; + framer->crc_size = bakelite_crc_size(crc_type); + framer->message_offset = BAKELITE_COBS_OVERHEAD(max_message_size + framer->crc_size); + framer->read_pos = buffer; +} + +/* Get pointer to message area in buffer */ +static inline uint8_t *bakelite_framer_buffer(Bakelite_CobsFramer *framer) { + return framer->buffer + framer->message_offset; +} + +/* Get usable buffer size for messages */ +static inline size_t bakelite_framer_buffer_size(Bakelite_CobsFramer *framer) { + return framer->max_message_size + 1; /* +1 for type byte */ +} + +/* Calculate and append CRC to data */ +static inline void bakelite_framer_append_crc(Bakelite_CobsFramer *framer, uint8_t *data, size_t length) { + switch (framer->crc_type) { + case BAKELITE_CRC_8: { + uint8_t crc = bakelite_crc8(data, length, 0); + memcpy(data + length, &crc, sizeof(crc)); + break; + } + case BAKELITE_CRC_16: { + uint16_t crc = bakelite_crc16(data, length, 0); + memcpy(data + length, &crc, sizeof(crc)); + break; + } + case BAKELITE_CRC_32: { + uint32_t crc = bakelite_crc32(data, length, 0); + memcpy(data + length, &crc, sizeof(crc)); + break; + } + default: + break; + } +} + +/* Verify CRC of data */ +static inline bool bakelite_framer_verify_crc(Bakelite_CobsFramer *framer, const uint8_t *data, size_t length) { + switch (framer->crc_type) { + case BAKELITE_CRC_8: { + uint8_t expected; + memcpy(&expected, data + length, sizeof(expected)); + return bakelite_crc8(data, length, 0) == expected; + } + case BAKELITE_CRC_16: { + uint16_t expected; + memcpy(&expected, data + length, sizeof(expected)); + return bakelite_crc16(data, length, 0) == expected; + } + case BAKELITE_CRC_32: { + uint32_t expected; + memcpy(&expected, data + length, sizeof(expected)); + return bakelite_crc32(data, length, 0) == expected; + } + default: + return true; + } +} + +/* Encode frame with data copy */ +static inline Bakelite_FramerResult bakelite_framer_encode_copy(Bakelite_CobsFramer *framer, + const uint8_t *data, size_t length) { + uint8_t *msg_start = framer->buffer + framer->message_offset; + memcpy(msg_start, data, length); + + if (framer->crc_size > 0) { + bakelite_framer_append_crc(framer, msg_start, length); + } + + Bakelite_CobsEncodeResult result = bakelite_cobs_encode( + framer->buffer, framer->buffer_size, + msg_start, length + framer->crc_size); + + if (result.status != 0) { + return (Bakelite_FramerResult){ 1, 0, NULL }; + } + + framer->buffer[result.out_len] = 0; /* Null terminator */ + return (Bakelite_FramerResult){ 0, result.out_len + 1, framer->buffer }; +} + +/* Encode frame (data already in buffer at message offset) */ +static inline Bakelite_FramerResult bakelite_framer_encode(Bakelite_CobsFramer *framer, size_t length) { + uint8_t *msg_start = framer->buffer + framer->message_offset; + + if (framer->crc_size > 0) { + bakelite_framer_append_crc(framer, msg_start, length); + } + + Bakelite_CobsEncodeResult result = bakelite_cobs_encode( + framer->buffer, framer->buffer_size, + msg_start, length + framer->crc_size); + + if (result.status != 0) { + return (Bakelite_FramerResult){ 1, 0, NULL }; + } + + framer->buffer[result.out_len] = 0; /* Null terminator */ + return (Bakelite_FramerResult){ 0, result.out_len + 1, framer->buffer }; +} + +/* Decode a complete frame */ +static inline Bakelite_DecodeResult bakelite_framer_decode_frame(Bakelite_CobsFramer *framer, size_t length) { + if (length == 1) { + return (Bakelite_DecodeResult){ BAKELITE_DECODE_FAILURE, 0, NULL }; + } + + length--; /* Discard null byte */ + + /* Decode in-place at buffer start */ + Bakelite_CobsDecodeResult result = bakelite_cobs_decode( + framer->buffer, framer->buffer_size, + framer->buffer, length); + + if (result.status != 0) { + return (Bakelite_DecodeResult){ BAKELITE_DECODE_FAILURE, 0, NULL }; + } + + /* Length of decoded data without CRC */ + length = result.out_len - framer->crc_size; + + if (framer->crc_size > 0) { + if (!bakelite_framer_verify_crc(framer, framer->buffer, length)) { + return (Bakelite_DecodeResult){ BAKELITE_DECODE_CRC_FAILURE, 0, NULL }; + } + } + + /* Move decoded data to message offset position for consistent buffer layout */ + if (framer->message_offset > 0) { + memmove(framer->buffer + framer->message_offset, framer->buffer, length); + } + + return (Bakelite_DecodeResult){ + BAKELITE_DECODE_OK, + length, + framer->buffer + framer->message_offset + }; +} + +/* Process a single byte from the stream */ +static inline Bakelite_DecodeResult bakelite_framer_read_byte(Bakelite_CobsFramer *framer, uint8_t byte) { + *framer->read_pos = byte; + size_t length = (size_t)(framer->read_pos - framer->buffer) + 1; + + if (byte == 0) { + framer->read_pos = framer->buffer; + return bakelite_framer_decode_frame(framer, length); + } else if (length == framer->buffer_size) { + framer->read_pos = framer->buffer; + return (Bakelite_DecodeResult){ BAKELITE_DECODE_BUFFER_OVERRUN, 0, NULL }; + } + + framer->read_pos++; + return (Bakelite_DecodeResult){ BAKELITE_DECODE_NOT_READY, 0, NULL }; +} + +/* + * COBS encode/decode implementation + * Copyright (c) 2010 Craig McQueen, MIT License + */ +static Bakelite_CobsEncodeResult bakelite_cobs_encode(void *dst_buf_ptr, size_t dst_buf_len, + const void *src_ptr, size_t src_len) { + Bakelite_CobsEncodeResult result = {0, COBS_ENCODE_OK}; + const uint8_t *src_read_ptr = (const uint8_t *)src_ptr; + const uint8_t *src_end_ptr = src_read_ptr + src_len; + uint8_t *dst_buf_start_ptr = (uint8_t *)dst_buf_ptr; + uint8_t *dst_buf_end_ptr = dst_buf_start_ptr + dst_buf_len; + uint8_t *dst_code_write_ptr = (uint8_t *)dst_buf_ptr; + uint8_t *dst_write_ptr = dst_code_write_ptr + 1; + uint8_t src_byte = 0; + uint8_t search_len = 1; + + if ((dst_buf_ptr == NULL) || (src_ptr == NULL)) { + result.status = COBS_ENCODE_NULL_POINTER; + return result; + } + + if (src_len != 0) { + for (;;) { + if (dst_write_ptr >= dst_buf_end_ptr) { + result.status |= COBS_ENCODE_OUT_BUFFER_OVERFLOW; + break; + } + + src_byte = *src_read_ptr++; + if (src_byte == 0) { + *dst_code_write_ptr = search_len; + dst_code_write_ptr = dst_write_ptr++; + search_len = 1; + if (src_read_ptr >= src_end_ptr) { + break; + } + } else { + *dst_write_ptr++ = src_byte; + search_len++; + if (src_read_ptr >= src_end_ptr) { + break; + } + if (search_len == 0xFF) { + *dst_code_write_ptr = search_len; + dst_code_write_ptr = dst_write_ptr++; + search_len = 1; + } + } + } + } + + if (dst_code_write_ptr >= dst_buf_end_ptr) { + result.status |= COBS_ENCODE_OUT_BUFFER_OVERFLOW; + dst_write_ptr = dst_buf_end_ptr; + } else { + *dst_code_write_ptr = search_len; + } + + result.out_len = (size_t)(dst_write_ptr - dst_buf_start_ptr); + return result; +} + +static Bakelite_CobsDecodeResult bakelite_cobs_decode(void *dst_buf_ptr, size_t dst_buf_len, + const void *src_ptr, size_t src_len) { + Bakelite_CobsDecodeResult result = {0, COBS_DECODE_OK}; + const uint8_t *src_read_ptr = (const uint8_t *)src_ptr; + const uint8_t *src_end_ptr = src_read_ptr + src_len; + uint8_t *dst_buf_start_ptr = (uint8_t *)dst_buf_ptr; + uint8_t *dst_buf_end_ptr = dst_buf_start_ptr + dst_buf_len; + uint8_t *dst_write_ptr = (uint8_t *)dst_buf_ptr; + size_t remaining_bytes; + uint8_t src_byte; + uint8_t i; + uint8_t len_code; + + if ((dst_buf_ptr == NULL) || (src_ptr == NULL)) { + result.status = COBS_DECODE_NULL_POINTER; + return result; + } + + if (src_len != 0) { + for (;;) { + len_code = *src_read_ptr++; + if (len_code == 0) { + result.status |= COBS_DECODE_ZERO_BYTE_IN_INPUT; + break; + } + len_code--; + + remaining_bytes = (size_t)(src_end_ptr - src_read_ptr); + if (len_code > remaining_bytes) { + result.status |= COBS_DECODE_INPUT_TOO_SHORT; + len_code = (uint8_t)remaining_bytes; + } + + remaining_bytes = (size_t)(dst_buf_end_ptr - dst_write_ptr); + if (len_code > remaining_bytes) { + result.status |= COBS_DECODE_OUT_BUFFER_OVERFLOW; + len_code = (uint8_t)remaining_bytes; + } + + for (i = len_code; i != 0; i--) { + src_byte = *src_read_ptr++; + if (src_byte == 0) { + result.status |= COBS_DECODE_ZERO_BYTE_IN_INPUT; + } + *dst_write_ptr++ = src_byte; + } + + if (src_read_ptr >= src_end_ptr) { + break; + } + + if (len_code != 0xFE) { + if (dst_write_ptr >= dst_buf_end_ptr) { + result.status |= COBS_DECODE_OUT_BUFFER_OVERFLOW; + break; + } + *dst_write_ptr++ = 0; + } + } + } + + result.out_len = (size_t)(dst_write_ptr - dst_buf_start_ptr); + return result; +} + +/* + * MIT License for COBS encode/decode functions: + * + * Copyright (c) 2010 Craig McQueen + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + + +/* +The MIT License +--------------- + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +#endif /* BAKELITE_H */ diff --git a/examples/chat/ctiny/client.c b/examples/chat/ctiny/client.c new file mode 100644 index 0000000..0be89a9 --- /dev/null +++ b/examples/chat/ctiny/client.c @@ -0,0 +1,98 @@ +#include "proto.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +static int sock_fd = -1; + +static int read_byte(void) { + uint8_t byte; + ssize_t n = recv(sock_fd, &byte, 1, MSG_DONTWAIT); + if (n == 1) return byte; + return -1; +} + +static size_t write_bytes(const uint8_t* data, size_t len) { + return send(sock_fd, data, len, 0); +} + +static int has_input(void) { + struct pollfd pfd = {STDIN_FILENO, POLLIN, 0}; + return poll(&pfd, 1, 0) > 0 && (pfd.revents & POLLIN); +} + +int main(void) { + sock_fd = socket(AF_INET, SOCK_STREAM, 0); + if (sock_fd < 0) { + perror("socket"); + return 1; + } + + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons(7032); + inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr); + + if (connect(sock_fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) { + perror("connect"); + return 1; + } + printf("Connected to server.\n"); + + Protocol proto; + Protocol_init(&proto, read_byte, write_bytes); + char my_name[32] = "client"; + char peer_name[32] = "server"; + + printf("> "); + fflush(stdout); + + while (1) { + Protocol_Message msg = Protocol_poll(&proto); + if (msg == Protocol_ChatMessage) { + ChatMessage* chat = Protocol_message_ChatMessage(&proto); + printf("\r%s > %s\n> ", chat->sender, chat->text); + fflush(stdout); + } else if (msg == Protocol_SetName) { + SetName* setName = Protocol_message_SetName(&proto); + printf("\r* %s is now known as %s\n> ", peer_name, setName->name); + fflush(stdout); + strncpy(peer_name, setName->name, sizeof(peer_name) - 1); + peer_name[sizeof(peer_name) - 1] = '\0'; + } + + if (has_input()) { + char line[300]; + if (!fgets(line, sizeof(line), stdin)) break; + line[strcspn(line, "\n")] = '\0'; + + if (strncmp(line, "/name ", 6) == 0) { + strncpy(my_name, line + 6, sizeof(my_name) - 1); + my_name[sizeof(my_name) - 1] = '\0'; + SetName* setName = Protocol_message_SetName(&proto); + strncpy(setName->name, my_name, sizeof(setName->name)); + Protocol_send_zerocopy_SetName(&proto); + } else if (strlen(line) > 0) { + ChatMessage* chat = Protocol_message_ChatMessage(&proto); + strncpy(chat->sender, my_name, sizeof(chat->sender)); + strncpy(chat->text, line, sizeof(chat->text)); + Protocol_send_zerocopy_ChatMessage(&proto); + } + + printf("> "); + fflush(stdout); + } + + usleep(1000); + } + + close(sock_fd); + return 0; +} diff --git a/examples/chat/ctiny/proto.h b/examples/chat/ctiny/proto.h new file mode 100644 index 0000000..513af28 --- /dev/null +++ b/examples/chat/ctiny/proto.h @@ -0,0 +1,216 @@ +#ifndef PROTOCOL_H +#define PROTOCOL_H + +#include "bakelite.h" + +/* 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 + +BAKELITE_STATIC_ASSERT(BAKELITE_UNALIGNED_OK, platform_requires_unpacked_mode); +/* Forward declarations */ +struct ChatMessage; +struct SetName; +/* Enums */ +/* Structs */ +typedef struct BAKELITE_PACKED { + char sender[32]; + char text[256]; +} ChatMessage; + +static inline int ChatMessage_pack(const ChatMessage *self, Bakelite_Buffer *buf) { + int rcode = 0; + if ((rcode = bakelite_write_string_fixed(buf, self->sender, 32)) != 0) return rcode; + if ((rcode = bakelite_write_string_fixed(buf, self->text, 256)) != 0) return rcode; + return rcode; +} + +static inline int ChatMessage_unpack(ChatMessage *self, Bakelite_Buffer *buf) { + int rcode = 0; + if ((rcode = bakelite_read_string_fixed(buf, self->sender, 32)) != 0) return rcode; + if ((rcode = bakelite_read_string_fixed(buf, self->text, 256)) != 0) return rcode; + return rcode; +} + +typedef struct BAKELITE_PACKED { + char name[32]; +} SetName; + +static inline int SetName_pack(const SetName *self, Bakelite_Buffer *buf) { + int rcode = 0; + if ((rcode = bakelite_write_string_fixed(buf, self->name, 32)) != 0) return rcode; + return rcode; +} + +static inline int SetName_unpack(SetName *self, Bakelite_Buffer *buf) { + int rcode = 0; + if ((rcode = bakelite_read_string_fixed(buf, self->name, 32)) != 0) return rcode; + return rcode; +} + +/* Protocol message IDs */ +typedef enum { + Protocol_NoMessage = -1, + Protocol_ChatMessage = 1, + Protocol_SetName = 2, +} Protocol_Message; + +/* Protocol buffer sizes */ +#define PROTOCOL_MAX_MESSAGE_SIZE 300 +#define PROTOCOL_CRC_SIZE 0 +#define PROTOCOL_BUFFER_SIZE BAKELITE_FRAMER_BUFFER_SIZE(PROTOCOL_MAX_MESSAGE_SIZE, PROTOCOL_CRC_SIZE) +#define PROTOCOL_MESSAGE_OFFSET BAKELITE_FRAMER_MESSAGE_OFFSET(PROTOCOL_MAX_MESSAGE_SIZE, PROTOCOL_CRC_SIZE) + +/* Protocol handler */ +typedef struct { + int (*read_byte)(void); + size_t (*write)(const uint8_t *data, size_t length); + Bakelite_CobsFramer framer; + uint8_t buffer[PROTOCOL_BUFFER_SIZE]; + Protocol_Message received_message; + size_t received_frame_length; +} Protocol; + +static inline void Protocol_init(Protocol *self, + int (*read_byte)(void), + size_t (*write)(const uint8_t *data, size_t length)) { + self->read_byte = read_byte; + self->write = write; + self->received_message = Protocol_NoMessage; + self->received_frame_length = 0; + bakelite_framer_init(&self->framer, self->buffer, PROTOCOL_BUFFER_SIZE, + PROTOCOL_MAX_MESSAGE_SIZE, BAKELITE_CRC_NONE); +} + +static inline Protocol_Message Protocol_poll(Protocol *self) { + int byte = self->read_byte(); + if (byte < 0) { + return Protocol_NoMessage; + } + + Bakelite_DecodeResult result = bakelite_framer_read_byte(&self->framer, (uint8_t)byte); + if (result.status == BAKELITE_DECODE_OK) { + if (result.length == 0) { + return Protocol_NoMessage; + } + + self->received_message = (Protocol_Message)result.data[0]; + self->received_frame_length = result.length - 1; + return self->received_message; + } + + return Protocol_NoMessage; +} + +/* Get pointer to message data in buffer (for zero-copy access) */ +static inline uint8_t *Protocol_buffer(Protocol *self) { + return bakelite_framer_buffer(&self->framer) + 1; +} + +/* Zero-copy message access (packed mode only) */ +static inline ChatMessage *Protocol_message_ChatMessage(Protocol *self) { + return (ChatMessage *)(bakelite_framer_buffer(&self->framer) + 1); +} + +static inline SetName *Protocol_message_SetName(Protocol *self) { + return (SetName *)(bakelite_framer_buffer(&self->framer) + 1); +} + +/* Zero-copy send functions (packed mode only) */ +static inline int Protocol_send_zerocopy_ChatMessage(Protocol *self) { + bakelite_framer_buffer(&self->framer)[0] = (uint8_t)Protocol_ChatMessage; + size_t frame_size = sizeof(ChatMessage) + 1; + Bakelite_FramerResult result = bakelite_framer_encode(&self->framer, frame_size); + + if (result.status != 0) { + return result.status; + } + + size_t ret = self->write(result.data, result.length); + return (ret == result.length) ? 0 : -1; +} + +static inline int Protocol_send_zerocopy_SetName(Protocol *self) { + bakelite_framer_buffer(&self->framer)[0] = (uint8_t)Protocol_SetName; + size_t frame_size = sizeof(SetName) + 1; + Bakelite_FramerResult result = bakelite_framer_encode(&self->framer, frame_size); + + if (result.status != 0) { + return result.status; + } + + size_t ret = self->write(result.data, result.length); + return (ret == result.length) ? 0 : -1; +} + +/* Copy-based send functions */ +static inline int Protocol_send_ChatMessage(Protocol *self, const ChatMessage *msg) { + uint8_t *msg_buf = bakelite_framer_buffer(&self->framer); + msg_buf[0] = (uint8_t)Protocol_ChatMessage; + + Bakelite_Buffer buf; + bakelite_buffer_init(&buf, msg_buf + 1, bakelite_framer_buffer_size(&self->framer) - 1); + ChatMessage_pack(msg, &buf); + + size_t frame_size = bakelite_buffer_pos(&buf) + 1; + Bakelite_FramerResult result = bakelite_framer_encode(&self->framer, frame_size); + + if (result.status != 0) { + return result.status; + } + + size_t ret = self->write(result.data, result.length); + return (ret == result.length) ? 0 : -1; +} + +static inline int Protocol_send_SetName(Protocol *self, const SetName *msg) { + uint8_t *msg_buf = bakelite_framer_buffer(&self->framer); + msg_buf[0] = (uint8_t)Protocol_SetName; + + Bakelite_Buffer buf; + bakelite_buffer_init(&buf, msg_buf + 1, bakelite_framer_buffer_size(&self->framer) - 1); + SetName_pack(msg, &buf); + + size_t frame_size = bakelite_buffer_pos(&buf) + 1; + Bakelite_FramerResult result = bakelite_framer_encode(&self->framer, frame_size); + + if (result.status != 0) { + return result.status; + } + + size_t ret = self->write(result.data, result.length); + return (ret == result.length) ? 0 : -1; +} + +/* Copy-based decode functions */ +static inline int Protocol_decode_ChatMessage(Protocol *self, ChatMessage *msg, + uint8_t *heap, size_t heap_size) { + if (self->received_message != Protocol_ChatMessage) { + return -1; + } + + Bakelite_Buffer buf; + bakelite_buffer_init_with_heap(&buf, + bakelite_framer_buffer(&self->framer) + 1, self->received_frame_length, + heap, heap_size); + return ChatMessage_unpack(msg, &buf); +} + +static inline int Protocol_decode_SetName(Protocol *self, SetName *msg, + uint8_t *heap, size_t heap_size) { + if (self->received_message != Protocol_SetName) { + return -1; + } + + Bakelite_Buffer buf; + bakelite_buffer_init_with_heap(&buf, + bakelite_framer_buffer(&self->framer) + 1, self->received_frame_length, + heap, heap_size); + return SetName_unpack(msg, &buf); +} + +#endif /* PROTOCOL_H */ diff --git a/examples/chat/ctiny/server.c b/examples/chat/ctiny/server.c new file mode 100644 index 0000000..8cb3ec9 --- /dev/null +++ b/examples/chat/ctiny/server.c @@ -0,0 +1,113 @@ +#include "proto.h" + +#include +#include +#include +#include +#include +#include +#include + +static int conn_fd = -1; + +static int read_byte(void) { + uint8_t byte; + ssize_t n = recv(conn_fd, &byte, 1, MSG_DONTWAIT); + if (n == 1) return byte; + return -1; +} + +static size_t write_bytes(const uint8_t* data, size_t len) { + return send(conn_fd, data, len, 0); +} + +static int has_input(void) { + struct pollfd pfd = {STDIN_FILENO, POLLIN, 0}; + return poll(&pfd, 1, 0) > 0 && (pfd.revents & POLLIN); +} + +int main(void) { + int listen_fd = socket(AF_INET, SOCK_STREAM, 0); + if (listen_fd < 0) { + perror("socket"); + return 1; + } + + int opt = 1; + setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); + + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = INADDR_ANY; + addr.sin_port = htons(7032); + + if (bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) { + perror("bind"); + return 1; + } + + if (listen(listen_fd, 1) < 0) { + perror("listen"); + return 1; + } + + printf("Listening on port 7032...\n"); + conn_fd = accept(listen_fd, NULL, NULL); + if (conn_fd < 0) { + perror("accept"); + return 1; + } + printf("Client connected.\n"); + + Protocol proto; + Protocol_init(&proto, read_byte, write_bytes); + char my_name[32] = "server"; + char peer_name[32] = "client"; + + printf("> "); + fflush(stdout); + + while (1) { + Protocol_Message msg = Protocol_poll(&proto); + if (msg == Protocol_ChatMessage) { + ChatMessage* chat = Protocol_message_ChatMessage(&proto); + printf("\r%s > %s\n> ", chat->sender, chat->text); + fflush(stdout); + } else if (msg == Protocol_SetName) { + SetName* setName = Protocol_message_SetName(&proto); + printf("\r* %s is now known as %s\n> ", peer_name, setName->name); + fflush(stdout); + strncpy(peer_name, setName->name, sizeof(peer_name) - 1); + peer_name[sizeof(peer_name) - 1] = '\0'; + } + + if (has_input()) { + char line[300]; + if (!fgets(line, sizeof(line), stdin)) break; + line[strcspn(line, "\n")] = '\0'; + + if (strncmp(line, "/name ", 6) == 0) { + strncpy(my_name, line + 6, sizeof(my_name) - 1); + my_name[sizeof(my_name) - 1] = '\0'; + SetName* setName = Protocol_message_SetName(&proto); + strncpy(setName->name, my_name, sizeof(setName->name)); + Protocol_send_zerocopy_SetName(&proto); + } else if (strlen(line) > 0) { + ChatMessage* chat = Protocol_message_ChatMessage(&proto); + strncpy(chat->sender, my_name, sizeof(chat->sender)); + strncpy(chat->text, line, sizeof(chat->text)); + Protocol_send_zerocopy_ChatMessage(&proto); + } + + printf("> "); + fflush(stdout); + } + + usleep(1000); + } + + close(conn_fd); + close(listen_fd); + return 0; +} diff --git a/mkdocs.yml b/mkdocs.yml index 12ad32f..8469f86 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -19,6 +19,7 @@ nav: # - Tutorial: tutorial.md - Protocol Spec: protocol.md - Languages: + - C (Tiny): ctiny.md - C++ (Tiny): cpptiny.md - Python: python.md - About: From 61404303d3acdecfd095800f0bc237c6808bb1c0 Mon Sep 17 00:00:00 2001 From: Emma Powers Date: Sun, 30 Nov 2025 11:59:07 -0500 Subject: [PATCH 6/6] Add ctiny test suite and refactor shared runtime code - Add comprehensive ctiny tests (35 tests): serialization, framing, protocol - Extract shared COBS/CRC implementations to runtimes/common/ - Fix ctiny array codegen using GCC statement-expression extension - Fix ctiny enum unpacking (use temp var to avoid partial writes) - Update cpptiny/ctiny to use shared common code --- bakelite/generator/cpptiny.py | 11 +- bakelite/generator/ctiny.py | 82 +- bakelite/generator/runtimes/common/cobs.h | 223 +++ bakelite/generator/runtimes/common/crc.h | 231 +++ bakelite/generator/runtimes/cpptiny/cobs.h | 233 +-- bakelite/generator/runtimes/cpptiny/crc.h | 247 +--- .../generator/runtimes/cpptiny/declarations.h | 33 +- bakelite/generator/runtimes/ctiny/cobs.h | 192 +-- bakelite/generator/runtimes/ctiny/crc.h | 191 +-- .../generator/templates/cpptiny-bakelite.h.j2 | 20 +- .../generator/templates/ctiny-bakelite.h.j2 | 12 + bakelite/tests/generator/.gitignore | 5 + bakelite/tests/generator/Makefile | 37 +- bakelite/tests/generator/ctiny-framing.c | 400 ++++++ bakelite/tests/generator/ctiny-protocol.c | 247 ++++ .../tests/generator/ctiny-serialization.c | 237 +++ bakelite/tests/generator/greatest.h | 1266 +++++++++++++++++ .../tests/generator/struct-ctiny.bakelite | 70 + 18 files changed, 2869 insertions(+), 868 deletions(-) create mode 100644 bakelite/generator/runtimes/common/cobs.h create mode 100644 bakelite/generator/runtimes/common/crc.h create mode 100644 bakelite/tests/generator/ctiny-framing.c create mode 100644 bakelite/tests/generator/ctiny-protocol.c create mode 100644 bakelite/tests/generator/ctiny-serialization.c create mode 100644 bakelite/tests/generator/greatest.h create mode 100644 bakelite/tests/generator/struct-ctiny.bakelite diff --git a/bakelite/generator/cpptiny.py b/bakelite/generator/cpptiny.py index 945a981..acab0c7 100644 --- a/bakelite/generator/cpptiny.py +++ b/bakelite/generator/cpptiny.py @@ -206,12 +206,15 @@ def _read_type(member: ProtoStructMember) -> str: def runtime() -> str: """Generate the C++ runtime support code.""" + runtimes_dir = os.path.join(os.path.dirname(__file__), "runtimes") def include(filename: str) -> str: - with open( - os.path.join(os.path.dirname(__file__), "runtimes", "cpptiny", filename), - encoding="utf-8", - ) as f: + # Support both cpptiny/ and common/ subdirectories + if filename.startswith("common/"): + filepath = os.path.join(runtimes_dir, filename) + else: + filepath = os.path.join(runtimes_dir, "cpptiny", filename) + with open(filepath, encoding="utf-8") as f: return f.read() runtime_template = env.get_template("cpptiny-bakelite.h.j2") diff --git a/bakelite/generator/ctiny.py b/bakelite/generator/ctiny.py index 54f014e..945c38b 100644 --- a/bakelite/generator/ctiny.py +++ b/bakelite/generator/ctiny.py @@ -114,32 +114,38 @@ def render( enums_types = {enum.name: enum for enum in enums} structs_types = {struct.name: struct for struct in structs} - def _write_type(member: ProtoStructMember) -> str: + def _write_type(member: ProtoStructMember, is_array_element: bool = False) -> str: if member.array_size is not None: - # Array handling + # Array handling - use GCC statement-expression extension ({ ... }) if member.array_size > 0: # Fixed-size array tmp_member = copy(member) tmp_member.array_size = None tmp_member.name = f"self->{member.name}[i]" - inner_write = _write_type(tmp_member) - return f"""for (uint32_t i = 0; i < {member.array_size}; i++) {{ + inner_write = _write_type(tmp_member, is_array_element=True) + return f"""({{ for (uint32_t i = 0; i < {member.array_size}; i++) {{ if ((rcode = {inner_write}) != 0) return rcode; - }} - rcode = 0""" + }} 0; }})""" # Variable-length array tmp_member = copy(member) tmp_member.array_size = None - tmp_member.name = f"(({_map_type(member.type)}*)self->{member.name}.data)[i]" - inner_write = _write_type(tmp_member) - return f"""bakelite_write_uint8(buf, self->{member.name}.size); + c_type = _map_type(member.type) + if member.type.name in structs_types: + # For struct arrays, access as struct pointers + tmp_member.name = f"(({c_type}*)self->{member.name}.data)[i]" + else: + tmp_member.name = f"(({c_type}*)self->{member.name}.data)[i]" + inner_write = _write_type(tmp_member, is_array_element=True) + return f"""({{ if ((rcode = bakelite_write_uint8(buf, self->{member.name}.size)) != 0) return rcode; for (uint8_t i = 0; i < self->{member.name}.size; i++) {{ if ((rcode = {inner_write}) != 0) return rcode; - }} - rcode = 0""" + }} 0; }})""" - # Use self-> prefix if not already present (for nested arrays, name includes self->) - name = member.name if member.name.startswith("self->") else f"self->{member.name}" + # Use self-> prefix if not already present (for nested arrays, name includes self-> or cast) + name = member.name + needs_prefix = not (name.startswith("self->") or name.startswith("(")) + if needs_prefix: + name = f"self->{name}" if member.type.name in enums_types: underlying = SERIALIZER_SUFFIX_MAP[enums_types[member.type.name].type.name] @@ -159,43 +165,50 @@ def _write_type(member: ProtoStructMember) -> str: return f"bakelite_write_string(buf, {name})" raise RuntimeError(f"Unknown type {member.type.name}") - def _read_type(member: ProtoStructMember) -> str: + def _read_type(member: ProtoStructMember, is_array_element: bool = False) -> str: if member.array_size is not None: - # Array handling + # Array handling - use GCC statement-expression extension ({ ... }) if member.array_size > 0: # Fixed-size array tmp_member = copy(member) tmp_member.array_size = None tmp_member.name = f"self->{member.name}[i]" - inner_read = _read_type(tmp_member) - return f"""for (uint32_t i = 0; i < {member.array_size}; i++) {{ + inner_read = _read_type(tmp_member, is_array_element=True) + return f"""({{ for (uint32_t i = 0; i < {member.array_size}; i++) {{ if ((rcode = {inner_read}) != 0) return rcode; - }} - rcode = 0""" + }} 0; }})""" # Variable-length array tmp_member = copy(member) tmp_member.array_size = None - tmp_member.name = f"(({_map_type(member.type)}*)self->{member.name}.data)[i]" - inner_read = _read_type(tmp_member) - return f"""{{ + c_type = _map_type(member.type) + if member.type.name in structs_types: + # For struct arrays, access as struct pointers + tmp_member.name = f"(({c_type}*)self->{member.name}.data)[i]" + else: + tmp_member.name = f"(({c_type}*)self->{member.name}.data)[i]" + inner_read = _read_type(tmp_member, is_array_element=True) + return f"""({{ uint8_t arr_size; if ((rcode = bakelite_read_uint8(buf, &arr_size)) != 0) return rcode; self->{member.name}.size = arr_size; - self->{member.name}.data = bakelite_buffer_alloc(buf, arr_size * sizeof({_map_type(member.type)})); + self->{member.name}.data = bakelite_buffer_alloc(buf, arr_size * sizeof({c_type})); if (self->{member.name}.data == NULL) return BAKELITE_ERR_ALLOC; for (uint8_t i = 0; i < arr_size; i++) {{ if ((rcode = {inner_read}) != 0) return rcode; - }} - }} - rcode = 0""" + }} 0; }})""" - # Use self-> prefix if not already present (for nested arrays, name includes self->) - name = member.name if member.name.startswith("self->") else f"self->{member.name}" + # Use self-> prefix if not already present (for nested arrays, name includes self-> or cast) + name = member.name + needs_prefix = not (name.startswith("self->") or name.startswith("(")) + if needs_prefix: + name = f"self->{name}" if member.type.name in enums_types: underlying = SERIALIZER_SUFFIX_MAP[enums_types[member.type.name].type.name] underlying_type = _map_type(enums_types[member.type.name].type) - return f"bakelite_read_{underlying}(buf, ({underlying_type}*)&{name})" + enum_type = member.type.name + # Use a temporary variable to avoid writing only the first byte of the enum + return f"""({{ {underlying_type} tmp; if ((rcode = bakelite_read_{underlying}(buf, &tmp)) != 0) return rcode; {name} = ({enum_type})tmp; 0; }})""" if member.type.name in structs_types: return f"{member.type.name}_unpack(&{name}, buf)" if member.type.name in SERIALIZER_SUFFIX_MAP: @@ -270,12 +283,15 @@ def _read_type(member: ProtoStructMember) -> str: def runtime() -> str: """Generate the C runtime support code.""" + runtimes_dir = os.path.join(os.path.dirname(__file__), "runtimes") def include(filename: str) -> str: - with open( - os.path.join(os.path.dirname(__file__), "runtimes", "ctiny", filename), - encoding="utf-8", - ) as f: + # Support both ctiny/ and common/ subdirectories + if filename.startswith("common/"): + filepath = os.path.join(runtimes_dir, filename) + else: + filepath = os.path.join(runtimes_dir, "ctiny", filename) + with open(filepath, encoding="utf-8") as f: return f.read() runtime_template = env.get_template("ctiny-bakelite.h.j2") diff --git a/bakelite/generator/runtimes/common/cobs.h b/bakelite/generator/runtimes/common/cobs.h new file mode 100644 index 0000000..c34c797 --- /dev/null +++ b/bakelite/generator/runtimes/common/cobs.h @@ -0,0 +1,223 @@ +/* + * Bakelite COBS Functions (C99) + * + * COBS encode/decode functions are Copyright (c) 2010 Craig McQueen + * Licensed under the MIT license (see end of file). + * Source: https://github.com/cmcqueen/cobs-c + */ + +#ifndef BAKELITE_COMMON_COBS_H +#define BAKELITE_COMMON_COBS_H + +#include +#include + +/* COBS buffer size calculations */ +#define BAKELITE_COBS_ENCODE_DST_BUF_LEN_MAX(SRC_LEN) ((SRC_LEN) + (((SRC_LEN) + 253u) / 254u)) +#define BAKELITE_COBS_DECODE_DST_BUF_LEN_MAX(SRC_LEN) (((SRC_LEN) == 0) ? 0u : ((SRC_LEN) - 1u)) +#define BAKELITE_COBS_OVERHEAD(BUF_SIZE) (((BUF_SIZE) + 253u) / 254u) +#define BAKELITE_COBS_ENCODE_SRC_OFFSET(SRC_LEN) (((SRC_LEN) + 253u) / 254u) + +/* Legacy macro names for backward compatibility */ +#define COBS_ENCODE_DST_BUF_LEN_MAX(SRC_LEN) BAKELITE_COBS_ENCODE_DST_BUF_LEN_MAX(SRC_LEN) +#define COBS_DECODE_DST_BUF_LEN_MAX(SRC_LEN) BAKELITE_COBS_DECODE_DST_BUF_LEN_MAX(SRC_LEN) +#define COBS_ENCODE_SRC_OFFSET(SRC_LEN) BAKELITE_COBS_ENCODE_SRC_OFFSET(SRC_LEN) + +/* COBS encode/decode status */ +typedef enum { + BAKELITE_COBS_ENCODE_OK = 0x00, + BAKELITE_COBS_ENCODE_NULL_POINTER = 0x01, + BAKELITE_COBS_ENCODE_OUT_BUFFER_OVERFLOW = 0x02 +} Bakelite_CobsEncodeStatus; + +typedef enum { + BAKELITE_COBS_DECODE_OK = 0x00, + BAKELITE_COBS_DECODE_NULL_POINTER = 0x01, + BAKELITE_COBS_DECODE_OUT_BUFFER_OVERFLOW = 0x02, + BAKELITE_COBS_DECODE_ZERO_BYTE_IN_INPUT = 0x04, + BAKELITE_COBS_DECODE_INPUT_TOO_SHORT = 0x08 +} Bakelite_CobsDecodeStatus; + +/* COBS encode/decode results */ +typedef struct { + size_t out_len; + int status; +} Bakelite_CobsEncodeResult; + +typedef struct { + size_t out_len; + int status; +} Bakelite_CobsDecodeResult; + +/* + * COBS-encode a string of input bytes. + * + * dst_buf_ptr: The buffer into which the result will be written + * dst_buf_len: Length of the buffer into which the result will be written + * src_ptr: The byte string to be encoded + * src_len Length of the byte string to be encoded + * + * returns: A struct containing the success status of the encoding + * operation and the length of the result (that was written to + * dst_buf_ptr) + */ +static inline Bakelite_CobsEncodeResult bakelite_cobs_encode(void *dst_buf_ptr, size_t dst_buf_len, + const void *src_ptr, size_t src_len) { + Bakelite_CobsEncodeResult result = {0, BAKELITE_COBS_ENCODE_OK}; + const uint8_t *src_read_ptr = (const uint8_t *)src_ptr; + const uint8_t *src_end_ptr = src_read_ptr + src_len; + uint8_t *dst_buf_start_ptr = (uint8_t *)dst_buf_ptr; + uint8_t *dst_buf_end_ptr = dst_buf_start_ptr + dst_buf_len; + uint8_t *dst_code_write_ptr = (uint8_t *)dst_buf_ptr; + uint8_t *dst_write_ptr = dst_code_write_ptr + 1; + uint8_t src_byte = 0; + uint8_t search_len = 1; + + if ((dst_buf_ptr == NULL) || (src_ptr == NULL)) { + result.status = BAKELITE_COBS_ENCODE_NULL_POINTER; + return result; + } + + if (src_len != 0) { + for (;;) { + if (dst_write_ptr >= dst_buf_end_ptr) { + result.status |= BAKELITE_COBS_ENCODE_OUT_BUFFER_OVERFLOW; + break; + } + + src_byte = *src_read_ptr++; + if (src_byte == 0) { + *dst_code_write_ptr = search_len; + dst_code_write_ptr = dst_write_ptr++; + search_len = 1; + if (src_read_ptr >= src_end_ptr) { + break; + } + } else { + *dst_write_ptr++ = src_byte; + search_len++; + if (src_read_ptr >= src_end_ptr) { + break; + } + if (search_len == 0xFF) { + *dst_code_write_ptr = search_len; + dst_code_write_ptr = dst_write_ptr++; + search_len = 1; + } + } + } + } + + if (dst_code_write_ptr >= dst_buf_end_ptr) { + result.status |= BAKELITE_COBS_ENCODE_OUT_BUFFER_OVERFLOW; + dst_write_ptr = dst_buf_end_ptr; + } else { + *dst_code_write_ptr = search_len; + } + + result.out_len = (size_t)(dst_write_ptr - dst_buf_start_ptr); + return result; +} + +/* + * Decode a COBS byte string. + * + * dst_buf_ptr: The buffer into which the result will be written + * dst_buf_len: Length of the buffer into which the result will be written + * src_ptr: The byte string to be decoded + * src_len Length of the byte string to be decoded + * + * returns: A struct containing the success status of the decoding + * operation and the length of the result (that was written to + * dst_buf_ptr) + */ +static inline Bakelite_CobsDecodeResult bakelite_cobs_decode(void *dst_buf_ptr, size_t dst_buf_len, + const void *src_ptr, size_t src_len) { + Bakelite_CobsDecodeResult result = {0, BAKELITE_COBS_DECODE_OK}; + const uint8_t *src_read_ptr = (const uint8_t *)src_ptr; + const uint8_t *src_end_ptr = src_read_ptr + src_len; + uint8_t *dst_buf_start_ptr = (uint8_t *)dst_buf_ptr; + uint8_t *dst_buf_end_ptr = dst_buf_start_ptr + dst_buf_len; + uint8_t *dst_write_ptr = (uint8_t *)dst_buf_ptr; + size_t remaining_bytes; + uint8_t src_byte; + uint8_t i; + uint8_t len_code; + + if ((dst_buf_ptr == NULL) || (src_ptr == NULL)) { + result.status = BAKELITE_COBS_DECODE_NULL_POINTER; + return result; + } + + if (src_len != 0) { + for (;;) { + len_code = *src_read_ptr++; + if (len_code == 0) { + result.status |= BAKELITE_COBS_DECODE_ZERO_BYTE_IN_INPUT; + break; + } + len_code--; + + remaining_bytes = (size_t)(src_end_ptr - src_read_ptr); + if (len_code > remaining_bytes) { + result.status |= BAKELITE_COBS_DECODE_INPUT_TOO_SHORT; + len_code = (uint8_t)remaining_bytes; + } + + remaining_bytes = (size_t)(dst_buf_end_ptr - dst_write_ptr); + if (len_code > remaining_bytes) { + result.status |= BAKELITE_COBS_DECODE_OUT_BUFFER_OVERFLOW; + len_code = (uint8_t)remaining_bytes; + } + + for (i = len_code; i != 0; i--) { + src_byte = *src_read_ptr++; + if (src_byte == 0) { + result.status |= BAKELITE_COBS_DECODE_ZERO_BYTE_IN_INPUT; + } + *dst_write_ptr++ = src_byte; + } + + if (src_read_ptr >= src_end_ptr) { + break; + } + + if (len_code != 0xFE) { + if (dst_write_ptr >= dst_buf_end_ptr) { + result.status |= BAKELITE_COBS_DECODE_OUT_BUFFER_OVERFLOW; + break; + } + *dst_write_ptr++ = 0; + } + } + } + + result.out_len = (size_t)(dst_write_ptr - dst_buf_start_ptr); + return result; +} + +/* + * MIT License for COBS encode/decode functions: + * + * Copyright (c) 2010 Craig McQueen + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#endif /* BAKELITE_COMMON_COBS_H */ diff --git a/bakelite/generator/runtimes/common/crc.h b/bakelite/generator/runtimes/common/crc.h new file mode 100644 index 0000000..2c082f8 --- /dev/null +++ b/bakelite/generator/runtimes/common/crc.h @@ -0,0 +1,231 @@ +/* + * Bakelite CRC Functions (C99) + * Copyright (c) Emma Powers 2021-2024 + * + * Platform-independent CRC-8, CRC-16, and CRC-32 implementations + * with flash storage support for embedded platforms. + */ + +#ifndef BAKELITE_COMMON_CRC_H +#define BAKELITE_COMMON_CRC_H + +#include +#include + +/* + * Flash storage attribute macros for embedded platforms. + * + * On most platforms, const variables are automatically placed in flash. + * AVR requires PROGMEM and special read functions. + * ESP32/ESP8266 use ICACHE_RODATA_ATTR but can read directly. + */ +#if defined(__AVR__) + #include + #define BAKELITE_FLASH PROGMEM + #define BAKELITE_FLASH_READ_8(x) pgm_read_byte(&(x)) + #define BAKELITE_FLASH_READ_16(x) pgm_read_word(&(x)) + #define BAKELITE_FLASH_READ_32(x) pgm_read_dword(&(x)) +#elif defined(ESP32) || defined(ESP8266) || defined(ARDUINO_ARCH_ESP32) || defined(ARDUINO_ARCH_ESP8266) + #ifdef ESP8266 + #include + #endif + #define BAKELITE_FLASH ICACHE_RODATA_ATTR + #define BAKELITE_FLASH_READ_8(x) (x) + #define BAKELITE_FLASH_READ_16(x) (x) + #define BAKELITE_FLASH_READ_32(x) (x) +#elif defined(__XC8) + /* Microchip XC8 for 8-bit PIC */ + #define BAKELITE_FLASH __rom + #define BAKELITE_FLASH_READ_8(x) (x) + #define BAKELITE_FLASH_READ_16(x) (x) + #define BAKELITE_FLASH_READ_32(x) (x) +#elif defined(__XC16) || defined(__XC32) + /* Microchip XC16/XC32 - const is sufficient */ + #define BAKELITE_FLASH + #define BAKELITE_FLASH_READ_8(x) (x) + #define BAKELITE_FLASH_READ_16(x) (x) + #define BAKELITE_FLASH_READ_32(x) (x) +#else + /* Default: const variables are in flash (ARM Cortex-M, etc.) */ + #define BAKELITE_FLASH + #define BAKELITE_FLASH_READ_8(x) (x) + #define BAKELITE_FLASH_READ_16(x) (x) + #define BAKELITE_FLASH_READ_32(x) (x) +#endif + +/* CRC size constants */ +#define BAKELITE_CRC_NOOP_SIZE 0 +#define BAKELITE_CRC8_SIZE 1 +#define BAKELITE_CRC16_SIZE 2 +#define BAKELITE_CRC32_SIZE 4 + +/* CRC-8 polynomial: 0x107 */ +static inline uint8_t bakelite_crc8(const uint8_t *data, size_t len, uint8_t crc) { + static const uint8_t table[256] BAKELITE_FLASH = { + 0x00U,0x07U,0x0EU,0x09U,0x1CU,0x1BU,0x12U,0x15U, + 0x38U,0x3FU,0x36U,0x31U,0x24U,0x23U,0x2AU,0x2DU, + 0x70U,0x77U,0x7EU,0x79U,0x6CU,0x6BU,0x62U,0x65U, + 0x48U,0x4FU,0x46U,0x41U,0x54U,0x53U,0x5AU,0x5DU, + 0xE0U,0xE7U,0xEEU,0xE9U,0xFCU,0xFBU,0xF2U,0xF5U, + 0xD8U,0xDFU,0xD6U,0xD1U,0xC4U,0xC3U,0xCAU,0xCDU, + 0x90U,0x97U,0x9EU,0x99U,0x8CU,0x8BU,0x82U,0x85U, + 0xA8U,0xAFU,0xA6U,0xA1U,0xB4U,0xB3U,0xBAU,0xBDU, + 0xC7U,0xC0U,0xC9U,0xCEU,0xDBU,0xDCU,0xD5U,0xD2U, + 0xFFU,0xF8U,0xF1U,0xF6U,0xE3U,0xE4U,0xEDU,0xEAU, + 0xB7U,0xB0U,0xB9U,0xBEU,0xABU,0xACU,0xA5U,0xA2U, + 0x8FU,0x88U,0x81U,0x86U,0x93U,0x94U,0x9DU,0x9AU, + 0x27U,0x20U,0x29U,0x2EU,0x3BU,0x3CU,0x35U,0x32U, + 0x1FU,0x18U,0x11U,0x16U,0x03U,0x04U,0x0DU,0x0AU, + 0x57U,0x50U,0x59U,0x5EU,0x4BU,0x4CU,0x45U,0x42U, + 0x6FU,0x68U,0x61U,0x66U,0x73U,0x74U,0x7DU,0x7AU, + 0x89U,0x8EU,0x87U,0x80U,0x95U,0x92U,0x9BU,0x9CU, + 0xB1U,0xB6U,0xBFU,0xB8U,0xADU,0xAAU,0xA3U,0xA4U, + 0xF9U,0xFEU,0xF7U,0xF0U,0xE5U,0xE2U,0xEBU,0xECU, + 0xC1U,0xC6U,0xCFU,0xC8U,0xDDU,0xDAU,0xD3U,0xD4U, + 0x69U,0x6EU,0x67U,0x60U,0x75U,0x72U,0x7BU,0x7CU, + 0x51U,0x56U,0x5FU,0x58U,0x4DU,0x4AU,0x43U,0x44U, + 0x19U,0x1EU,0x17U,0x10U,0x05U,0x02U,0x0BU,0x0CU, + 0x21U,0x26U,0x2FU,0x28U,0x3DU,0x3AU,0x33U,0x34U, + 0x4EU,0x49U,0x40U,0x47U,0x52U,0x55U,0x5CU,0x5BU, + 0x76U,0x71U,0x78U,0x7FU,0x6AU,0x6DU,0x64U,0x63U, + 0x3EU,0x39U,0x30U,0x37U,0x22U,0x25U,0x2CU,0x2BU, + 0x06U,0x01U,0x08U,0x0FU,0x1AU,0x1DU,0x14U,0x13U, + 0xAEU,0xA9U,0xA0U,0xA7U,0xB2U,0xB5U,0xBCU,0xBBU, + 0x96U,0x91U,0x98U,0x9FU,0x8AU,0x8DU,0x84U,0x83U, + 0xDEU,0xD9U,0xD0U,0xD7U,0xC2U,0xC5U,0xCCU,0xCBU, + 0xE6U,0xE1U,0xE8U,0xEFU,0xFAU,0xFDU,0xF4U,0xF3U, + }; + + while (len > 0) { + crc = BAKELITE_FLASH_READ_8(table[*data ^ crc]); + data++; + len--; + } + return crc; +} + +/* CRC-16 polynomial: 0x18005, bit reverse algorithm */ +static inline uint16_t bakelite_crc16(const uint8_t *data, size_t len, uint16_t crc) { + static const uint16_t table[256] BAKELITE_FLASH = { + 0x0000U,0xC0C1U,0xC181U,0x0140U,0xC301U,0x03C0U,0x0280U,0xC241U, + 0xC601U,0x06C0U,0x0780U,0xC741U,0x0500U,0xC5C1U,0xC481U,0x0440U, + 0xCC01U,0x0CC0U,0x0D80U,0xCD41U,0x0F00U,0xCFC1U,0xCE81U,0x0E40U, + 0x0A00U,0xCAC1U,0xCB81U,0x0B40U,0xC901U,0x09C0U,0x0880U,0xC841U, + 0xD801U,0x18C0U,0x1980U,0xD941U,0x1B00U,0xDBC1U,0xDA81U,0x1A40U, + 0x1E00U,0xDEC1U,0xDF81U,0x1F40U,0xDD01U,0x1DC0U,0x1C80U,0xDC41U, + 0x1400U,0xD4C1U,0xD581U,0x1540U,0xD701U,0x17C0U,0x1680U,0xD641U, + 0xD201U,0x12C0U,0x1380U,0xD341U,0x1100U,0xD1C1U,0xD081U,0x1040U, + 0xF001U,0x30C0U,0x3180U,0xF141U,0x3300U,0xF3C1U,0xF281U,0x3240U, + 0x3600U,0xF6C1U,0xF781U,0x3740U,0xF501U,0x35C0U,0x3480U,0xF441U, + 0x3C00U,0xFCC1U,0xFD81U,0x3D40U,0xFF01U,0x3FC0U,0x3E80U,0xFE41U, + 0xFA01U,0x3AC0U,0x3B80U,0xFB41U,0x3900U,0xF9C1U,0xF881U,0x3840U, + 0x2800U,0xE8C1U,0xE981U,0x2940U,0xEB01U,0x2BC0U,0x2A80U,0xEA41U, + 0xEE01U,0x2EC0U,0x2F80U,0xEF41U,0x2D00U,0xEDC1U,0xEC81U,0x2C40U, + 0xE401U,0x24C0U,0x2580U,0xE541U,0x2700U,0xE7C1U,0xE681U,0x2640U, + 0x2200U,0xE2C1U,0xE381U,0x2340U,0xE101U,0x21C0U,0x2080U,0xE041U, + 0xA001U,0x60C0U,0x6180U,0xA141U,0x6300U,0xA3C1U,0xA281U,0x6240U, + 0x6600U,0xA6C1U,0xA781U,0x6740U,0xA501U,0x65C0U,0x6480U,0xA441U, + 0x6C00U,0xACC1U,0xAD81U,0x6D40U,0xAF01U,0x6FC0U,0x6E80U,0xAE41U, + 0xAA01U,0x6AC0U,0x6B80U,0xAB41U,0x6900U,0xA9C1U,0xA881U,0x6840U, + 0x7800U,0xB8C1U,0xB981U,0x7940U,0xBB01U,0x7BC0U,0x7A80U,0xBA41U, + 0xBE01U,0x7EC0U,0x7F80U,0xBF41U,0x7D00U,0xBDC1U,0xBC81U,0x7C40U, + 0xB401U,0x74C0U,0x7580U,0xB541U,0x7700U,0xB7C1U,0xB681U,0x7640U, + 0x7200U,0xB2C1U,0xB381U,0x7340U,0xB101U,0x71C0U,0x7080U,0xB041U, + 0x5000U,0x90C1U,0x9181U,0x5140U,0x9301U,0x53C0U,0x5280U,0x9241U, + 0x9601U,0x56C0U,0x5780U,0x9741U,0x5500U,0x95C1U,0x9481U,0x5440U, + 0x9C01U,0x5CC0U,0x5D80U,0x9D41U,0x5F00U,0x9FC1U,0x9E81U,0x5E40U, + 0x5A00U,0x9AC1U,0x9B81U,0x5B40U,0x9901U,0x59C0U,0x5880U,0x9841U, + 0x8801U,0x48C0U,0x4980U,0x8941U,0x4B00U,0x8BC1U,0x8A81U,0x4A40U, + 0x4E00U,0x8EC1U,0x8F81U,0x4F40U,0x8D01U,0x4DC0U,0x4C80U,0x8C41U, + 0x4400U,0x84C1U,0x8581U,0x4540U,0x8701U,0x47C0U,0x4680U,0x8641U, + 0x8201U,0x42C0U,0x4380U,0x8341U,0x4100U,0x81C1U,0x8081U,0x4040U, + }; + + while (len > 0) { + crc = BAKELITE_FLASH_READ_16(table[*data ^ (uint8_t)crc]) ^ (crc >> 8); + data++; + len--; + } + return crc; +} + +/* CRC-32 polynomial: 0x104C11DB7, bit reverse algorithm */ +static inline uint32_t bakelite_crc32(const uint8_t *data, size_t len, uint32_t crc) { + static const uint32_t table[256] BAKELITE_FLASH = { + 0x00000000U,0x77073096U,0xEE0E612CU,0x990951BAU, + 0x076DC419U,0x706AF48FU,0xE963A535U,0x9E6495A3U, + 0x0EDB8832U,0x79DCB8A4U,0xE0D5E91EU,0x97D2D988U, + 0x09B64C2BU,0x7EB17CBDU,0xE7B82D07U,0x90BF1D91U, + 0x1DB71064U,0x6AB020F2U,0xF3B97148U,0x84BE41DEU, + 0x1ADAD47DU,0x6DDDE4EBU,0xF4D4B551U,0x83D385C7U, + 0x136C9856U,0x646BA8C0U,0xFD62F97AU,0x8A65C9ECU, + 0x14015C4FU,0x63066CD9U,0xFA0F3D63U,0x8D080DF5U, + 0x3B6E20C8U,0x4C69105EU,0xD56041E4U,0xA2677172U, + 0x3C03E4D1U,0x4B04D447U,0xD20D85FDU,0xA50AB56BU, + 0x35B5A8FAU,0x42B2986CU,0xDBBBC9D6U,0xACBCF940U, + 0x32D86CE3U,0x45DF5C75U,0xDCD60DCFU,0xABD13D59U, + 0x26D930ACU,0x51DE003AU,0xC8D75180U,0xBFD06116U, + 0x21B4F4B5U,0x56B3C423U,0xCFBA9599U,0xB8BDA50FU, + 0x2802B89EU,0x5F058808U,0xC60CD9B2U,0xB10BE924U, + 0x2F6F7C87U,0x58684C11U,0xC1611DABU,0xB6662D3DU, + 0x76DC4190U,0x01DB7106U,0x98D220BCU,0xEFD5102AU, + 0x71B18589U,0x06B6B51FU,0x9FBFE4A5U,0xE8B8D433U, + 0x7807C9A2U,0x0F00F934U,0x9609A88EU,0xE10E9818U, + 0x7F6A0DBBU,0x086D3D2DU,0x91646C97U,0xE6635C01U, + 0x6B6B51F4U,0x1C6C6162U,0x856530D8U,0xF262004EU, + 0x6C0695EDU,0x1B01A57BU,0x8208F4C1U,0xF50FC457U, + 0x65B0D9C6U,0x12B7E950U,0x8BBEB8EAU,0xFCB9887CU, + 0x62DD1DDFU,0x15DA2D49U,0x8CD37CF3U,0xFBD44C65U, + 0x4DB26158U,0x3AB551CEU,0xA3BC0074U,0xD4BB30E2U, + 0x4ADFA541U,0x3DD895D7U,0xA4D1C46DU,0xD3D6F4FBU, + 0x4369E96AU,0x346ED9FCU,0xAD678846U,0xDA60B8D0U, + 0x44042D73U,0x33031DE5U,0xAA0A4C5FU,0xDD0D7CC9U, + 0x5005713CU,0x270241AAU,0xBE0B1010U,0xC90C2086U, + 0x5768B525U,0x206F85B3U,0xB966D409U,0xCE61E49FU, + 0x5EDEF90EU,0x29D9C998U,0xB0D09822U,0xC7D7A8B4U, + 0x59B33D17U,0x2EB40D81U,0xB7BD5C3BU,0xC0BA6CADU, + 0xEDB88320U,0x9ABFB3B6U,0x03B6E20CU,0x74B1D29AU, + 0xEAD54739U,0x9DD277AFU,0x04DB2615U,0x73DC1683U, + 0xE3630B12U,0x94643B84U,0x0D6D6A3EU,0x7A6A5AA8U, + 0xE40ECF0BU,0x9309FF9DU,0x0A00AE27U,0x7D079EB1U, + 0xF00F9344U,0x8708A3D2U,0x1E01F268U,0x6906C2FEU, + 0xF762575DU,0x806567CBU,0x196C3671U,0x6E6B06E7U, + 0xFED41B76U,0x89D32BE0U,0x10DA7A5AU,0x67DD4ACCU, + 0xF9B9DF6FU,0x8EBEEFF9U,0x17B7BE43U,0x60B08ED5U, + 0xD6D6A3E8U,0xA1D1937EU,0x38D8C2C4U,0x4FDFF252U, + 0xD1BB67F1U,0xA6BC5767U,0x3FB506DDU,0x48B2364BU, + 0xD80D2BDAU,0xAF0A1B4CU,0x36034AF6U,0x41047A60U, + 0xDF60EFC3U,0xA867DF55U,0x316E8EEFU,0x4669BE79U, + 0xCB61B38CU,0xBC66831AU,0x256FD2A0U,0x5268E236U, + 0xCC0C7795U,0xBB0B4703U,0x220216B9U,0x5505262FU, + 0xC5BA3BBEU,0xB2BD0B28U,0x2BB45A92U,0x5CB36A04U, + 0xC2D7FFA7U,0xB5D0CF31U,0x2CD99E8BU,0x5BDEAE1DU, + 0x9B64C2B0U,0xEC63F226U,0x756AA39CU,0x026D930AU, + 0x9C0906A9U,0xEB0E363FU,0x72076785U,0x05005713U, + 0x95BF4A82U,0xE2B87A14U,0x7BB12BAEU,0x0CB61B38U, + 0x92D28E9BU,0xE5D5BE0DU,0x7CDCEFB7U,0x0BDBDF21U, + 0x86D3D2D4U,0xF1D4E242U,0x68DDB3F8U,0x1FDA836EU, + 0x81BE16CDU,0xF6B9265BU,0x6FB077E1U,0x18B74777U, + 0x88085AE6U,0xFF0F6A70U,0x66063BCAU,0x11010B5CU, + 0x8F659EFFU,0xF862AE69U,0x616BFFD3U,0x166CCF45U, + 0xA00AE278U,0xD70DD2EEU,0x4E048354U,0x3903B3C2U, + 0xA7672661U,0xD06016F7U,0x4969474DU,0x3E6E77DBU, + 0xAED16A4AU,0xD9D65ADCU,0x40DF0B66U,0x37D83BF0U, + 0xA9BCAE53U,0xDEBB9EC5U,0x47B2CF7FU,0x30B5FFE9U, + 0xBDBDF21CU,0xCABAC28AU,0x53B39330U,0x24B4A3A6U, + 0xBAD03605U,0xCDD70693U,0x54DE5729U,0x23D967BFU, + 0xB3667A2EU,0xC4614AB8U,0x5D681B02U,0x2A6F2B94U, + 0xB40BBE37U,0xC30C8EA1U,0x5A05DF1BU,0x2D02EF8DU, + }; + + crc = crc ^ 0xFFFFFFFFU; + while (len > 0) { + crc = BAKELITE_FLASH_READ_32(table[*data ^ (uint8_t)crc]) ^ (crc >> 8); + data++; + len--; + } + crc = crc ^ 0xFFFFFFFFU; + return crc; +} + +#endif /* BAKELITE_COMMON_CRC_H */ diff --git a/bakelite/generator/runtimes/cpptiny/cobs.h b/bakelite/generator/runtimes/cpptiny/cobs.h index 164541d..689d676 100644 --- a/bakelite/generator/runtimes/cpptiny/cobs.h +++ b/bakelite/generator/runtimes/cpptiny/cobs.h @@ -1,3 +1,7 @@ +/* + * C++ COBS Framer using common C99 implementation + */ + enum class CobsDecodeState { Decoded, NotReady, @@ -64,7 +68,7 @@ class CobsFramer { memcpy(msgStart + length, (void *)&crc_val, sizeof(crc_val)); } - auto result = cobs_encode((void *)m_buffer, sizeof(m_buffer), + auto result = bakelite_cobs_encode((void *)m_buffer, sizeof(m_buffer), (void *)msgStart, length + C::size()); if(result.status != 0) { return { 1, 0, nullptr }; @@ -100,7 +104,7 @@ class CobsFramer { length--; // Discard null byte // Decode in-place at buffer start - auto result = cobs_decode((void *)m_buffer, sizeof(m_buffer), (void *)m_buffer, length); + auto result = bakelite_cobs_decode((void *)m_buffer, sizeof(m_buffer), (void *)m_buffer, length); if(result.status != 0) { return { CobsDecodeState::DecodeFailure, 0, nullptr }; } @@ -146,228 +150,3 @@ class CobsFramer { 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 - ***************/ - -#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)) -#define COBS_ENCODE_SRC_OFFSET(SRC_LEN) (((SRC_LEN) + 253u)/254u) - -typedef enum -{ - COBS_ENCODE_OK = 0x00, - COBS_ENCODE_NULL_POINTER = 0x01, - COBS_ENCODE_OUT_BUFFER_OVERFLOW = 0x02 -} cobs_encode_status; - -struct cobs_encode_result -{ - size_t out_len; - int status; -}; - -typedef enum -{ - COBS_DECODE_OK = 0x00, - COBS_DECODE_NULL_POINTER = 0x01, - COBS_DECODE_OUT_BUFFER_OVERFLOW = 0x02, - COBS_DECODE_ZERO_BYTE_IN_INPUT = 0x04, - COBS_DECODE_INPUT_TOO_SHORT = 0x08 -} cobs_decode_status; - -/* COBS-encode a string of input bytes. -* -* dst_buf_ptr: The buffer into which the result will be written -* dst_buf_len: Length of the buffer into which the result will be written -* src_ptr: The byte string to be encoded -* src_len Length of the byte string to be encoded -* -* returns: A struct containing the success status of the encoding -* operation and the length of the result (that was written to -* dst_buf_ptr) -*/ -static cobs_encode_result cobs_encode(void *dst_buf_ptr, size_t dst_buf_len, - const void *src_ptr, size_t src_len) -{ - cobs_encode_result result = {0, COBS_ENCODE_OK}; - const uint8_t *src_read_ptr = (uint8_t *)src_ptr; - const uint8_t *src_end_ptr = (uint8_t *)src_read_ptr + src_len; - uint8_t *dst_buf_start_ptr = (uint8_t *)dst_buf_ptr; - uint8_t *dst_buf_end_ptr = dst_buf_start_ptr + dst_buf_len; - uint8_t *dst_code_write_ptr = (uint8_t *)dst_buf_ptr; - uint8_t *dst_write_ptr = dst_code_write_ptr + 1; - uint8_t src_byte = 0; - uint8_t search_len = 1; - - /* First, do a NULL pointer check and return immediately if it fails. */ - if ((dst_buf_ptr == NULL) || (src_ptr == NULL)) - { - result.status = COBS_ENCODE_NULL_POINTER; - return result; - } - - if (src_len != 0) - { - /* Iterate over the source bytes */ - for (;;) - { - /* Check for running out of output buffer space */ - if (dst_write_ptr >= dst_buf_end_ptr) - { - result.status |= COBS_ENCODE_OUT_BUFFER_OVERFLOW; - break; - } - - src_byte = *src_read_ptr++; - if (src_byte == 0) - { - /* We found a zero byte */ - *dst_code_write_ptr = search_len; - dst_code_write_ptr = dst_write_ptr++; - search_len = 1; - if (src_read_ptr >= src_end_ptr) - { - break; - } - } - else - { - /* Copy the non-zero byte to the destination buffer */ - *dst_write_ptr++ = src_byte; - search_len++; - if (src_read_ptr >= src_end_ptr) - { - break; - } - if (search_len == 0xFF) - { - /* We have a long string of non-zero bytes, so we need - * to write out a length code of 0xFF. */ - *dst_code_write_ptr = search_len; - dst_code_write_ptr = dst_write_ptr++; - search_len = 1; - } - } - } - } - - /* We've reached the end of the source data (or possibly run out of output buffer) - * Finalise the remaining output. In particular, write the code (length) byte. - * Update the pointer to calculate the final output length. - */ - if (dst_code_write_ptr >= dst_buf_end_ptr) - { - /* We've run out of output buffer to write the code byte. */ - result.status |= COBS_ENCODE_OUT_BUFFER_OVERFLOW; - dst_write_ptr = dst_buf_end_ptr; - } - else - { - /* Write the last code (length) byte. */ - *dst_code_write_ptr = search_len; - } - - /* Calculate the output length, from the value of dst_code_write_ptr */ - result.out_len = dst_write_ptr - dst_buf_start_ptr; - - return result; -} - -/* Decode a COBS byte string. -* -* dst_buf_ptr: The buffer into which the result will be written -* dst_buf_len: Length of the buffer into which the result will be written -* src_ptr: The byte string to be decoded -* src_len Length of the byte string to be decoded -* -* returns: A struct containing the success status of the decoding -* operation and the length of the result (that was written to -* dst_buf_ptr) -*/ -static cobs_decode_result cobs_decode(void *dst_buf_ptr, size_t dst_buf_len, - const void *src_ptr, size_t src_len) -{ - cobs_decode_result result = {0, COBS_DECODE_OK}; - const uint8_t *src_read_ptr = (uint8_t *)src_ptr; - const uint8_t *src_end_ptr = (uint8_t *)src_read_ptr + src_len; - uint8_t *dst_buf_start_ptr = (uint8_t *)dst_buf_ptr; - uint8_t *dst_buf_end_ptr = dst_buf_start_ptr + dst_buf_len; - uint8_t *dst_write_ptr = (uint8_t *)dst_buf_ptr; - size_t remaining_bytes; - uint8_t src_byte; - uint8_t i; - uint8_t len_code; - - /* First, do a NULL pointer check and return immediately if it fails. */ - if ((dst_buf_ptr == NULL) || (src_ptr == NULL)) - { - result.status = COBS_DECODE_NULL_POINTER; - return result; - } - - if (src_len != 0) - { - for (;;) - { - len_code = *src_read_ptr++; - if (len_code == 0) - { - result.status |= COBS_DECODE_ZERO_BYTE_IN_INPUT; - break; - } - len_code--; - - /* Check length code against remaining input bytes */ - remaining_bytes = src_end_ptr - src_read_ptr; - if (len_code > remaining_bytes) - { - result.status |= COBS_DECODE_INPUT_TOO_SHORT; - len_code = remaining_bytes; - } - - /* Check length code against remaining output buffer space */ - remaining_bytes = dst_buf_end_ptr - dst_write_ptr; - if (len_code > remaining_bytes) - { - result.status |= COBS_DECODE_OUT_BUFFER_OVERFLOW; - len_code = remaining_bytes; - } - - for (i = len_code; i != 0; i--) - { - src_byte = *src_read_ptr++; - if (src_byte == 0) - { - result.status |= COBS_DECODE_ZERO_BYTE_IN_INPUT; - } - *dst_write_ptr++ = src_byte; - } - - if (src_read_ptr >= src_end_ptr) - { - break; - } - - /* Add a zero to the end */ - if (len_code != 0xFE) - { - if (dst_write_ptr >= dst_buf_end_ptr) - { - result.status |= COBS_DECODE_OUT_BUFFER_OVERFLOW; - break; - } - *dst_write_ptr++ = 0; - } - } - } - - result.out_len = dst_write_ptr - dst_buf_start_ptr; - - return result; -} diff --git a/bakelite/generator/runtimes/cpptiny/crc.h b/bakelite/generator/runtimes/cpptiny/crc.h index 48a415a..ab0f301 100644 --- a/bakelite/generator/runtimes/cpptiny/crc.h +++ b/bakelite/generator/runtimes/cpptiny/crc.h @@ -1,3 +1,7 @@ +/* + * C++ CRC wrapper classes using common C99 implementation + */ + class CrcNoop { public: constexpr static size_t size() { @@ -12,8 +16,8 @@ class CrcNoop { } }; -template -class Crc { +template +class Crc8Impl { public: constexpr static size_t size() { return sizeof(CrcType); @@ -24,227 +28,48 @@ class Crc { } void update(const char *data, size_t length) { - CrcFunc fn; - m_lastVal = fn(data, length, m_lastVal); + m_lastVal = bakelite_crc8((const uint8_t *)data, length, m_lastVal); } private: CrcType m_lastVal = 0; }; -struct crc8_fn; -struct crc16_fn; -struct crc32_fn; - - -using Crc8 = Crc; -using Crc16 = Crc; -using Crc32 = Crc; - -/* - * Auto generated CRC functions - */ - -// The CRC lookup tables are stored as const variables. On many platforms, -// const variables are stored in flash memroy. On AVR though, they are -// loaded into RAM on startup. A special PROGMEM macro is available on AVRs -// to indicate constants should be stored in program memory (flash). -// So if this macro is available, use it and assume we're on an AVR. -#ifdef PROGMEM - #define BAKELITE_CONST PROGMEM - // PROGMEM variables need to be accessed using pgm_read_* functions. - #define BAKELITE_CONST_8(x) pgm_read_byte(&(x)) - #define BAKELITE_CONST_16(x) pgm_read_dword(&(x)) - #define BAKELITE_CONST_32(x) ((uint32_t)pgm_read_dword((char *)&(x) + 2) << 16 | pgm_read_dword(&(x))) -#else - #define BAKELITE_CONST - #define BAKELITE_CONST_8(x) x - #define BAKELITE_CONST_16(x) x - #define BAKELITE_CONST_32(x) x -#endif - -// Automatically generated CRC function -// polynomial: 0x107 -struct crc8_fn { - uint8_t operator()(const char *data, int len, uint8_t crc) { - const unsigned char *uData = (unsigned char *)data; +template +class Crc16Impl { +public: + constexpr static size_t size() { + return sizeof(CrcType); + } - static const uint8_t table[256] BAKELITE_CONST = { - 0x00U,0x07U,0x0EU,0x09U,0x1CU,0x1BU,0x12U,0x15U, - 0x38U,0x3FU,0x36U,0x31U,0x24U,0x23U,0x2AU,0x2DU, - 0x70U,0x77U,0x7EU,0x79U,0x6CU,0x6BU,0x62U,0x65U, - 0x48U,0x4FU,0x46U,0x41U,0x54U,0x53U,0x5AU,0x5DU, - 0xE0U,0xE7U,0xEEU,0xE9U,0xFCU,0xFBU,0xF2U,0xF5U, - 0xD8U,0xDFU,0xD6U,0xD1U,0xC4U,0xC3U,0xCAU,0xCDU, - 0x90U,0x97U,0x9EU,0x99U,0x8CU,0x8BU,0x82U,0x85U, - 0xA8U,0xAFU,0xA6U,0xA1U,0xB4U,0xB3U,0xBAU,0xBDU, - 0xC7U,0xC0U,0xC9U,0xCEU,0xDBU,0xDCU,0xD5U,0xD2U, - 0xFFU,0xF8U,0xF1U,0xF6U,0xE3U,0xE4U,0xEDU,0xEAU, - 0xB7U,0xB0U,0xB9U,0xBEU,0xABU,0xACU,0xA5U,0xA2U, - 0x8FU,0x88U,0x81U,0x86U,0x93U,0x94U,0x9DU,0x9AU, - 0x27U,0x20U,0x29U,0x2EU,0x3BU,0x3CU,0x35U,0x32U, - 0x1FU,0x18U,0x11U,0x16U,0x03U,0x04U,0x0DU,0x0AU, - 0x57U,0x50U,0x59U,0x5EU,0x4BU,0x4CU,0x45U,0x42U, - 0x6FU,0x68U,0x61U,0x66U,0x73U,0x74U,0x7DU,0x7AU, - 0x89U,0x8EU,0x87U,0x80U,0x95U,0x92U,0x9BU,0x9CU, - 0xB1U,0xB6U,0xBFU,0xB8U,0xADU,0xAAU,0xA3U,0xA4U, - 0xF9U,0xFEU,0xF7U,0xF0U,0xE5U,0xE2U,0xEBU,0xECU, - 0xC1U,0xC6U,0xCFU,0xC8U,0xDDU,0xDAU,0xD3U,0xD4U, - 0x69U,0x6EU,0x67U,0x60U,0x75U,0x72U,0x7BU,0x7CU, - 0x51U,0x56U,0x5FU,0x58U,0x4DU,0x4AU,0x43U,0x44U, - 0x19U,0x1EU,0x17U,0x10U,0x05U,0x02U,0x0BU,0x0CU, - 0x21U,0x26U,0x2FU,0x28U,0x3DU,0x3AU,0x33U,0x34U, - 0x4EU,0x49U,0x40U,0x47U,0x52U,0x55U,0x5CU,0x5BU, - 0x76U,0x71U,0x78U,0x7FU,0x6AU,0x6DU,0x64U,0x63U, - 0x3EU,0x39U,0x30U,0x37U,0x22U,0x25U,0x2CU,0x2BU, - 0x06U,0x01U,0x08U,0x0FU,0x1AU,0x1DU,0x14U,0x13U, - 0xAEU,0xA9U,0xA0U,0xA7U,0xB2U,0xB5U,0xBCU,0xBBU, - 0x96U,0x91U,0x98U,0x9FU,0x8AU,0x8DU,0x84U,0x83U, - 0xDEU,0xD9U,0xD0U,0xD7U,0xC2U,0xC5U,0xCCU,0xCBU, - 0xE6U,0xE1U,0xE8U,0xEFU,0xFAU,0xFDU,0xF4U,0xF3U, - }; + CrcType value() const { + return m_lastVal; + } - while (len > 0) - { - crc = BAKELITE_CONST_8(table[*uData ^ (uint8_t)crc]); - uData++; - len--; - } - return crc; + void update(const char *data, size_t length) { + m_lastVal = bakelite_crc16((const uint8_t *)data, length, m_lastVal); } +private: + CrcType m_lastVal = 0; }; -// Automatically generated CRC function -// polynomial: 0x18005, bit reverse algorithm -struct crc16_fn { - uint16_t operator()(const char *data, int len, uint16_t crc) { - const unsigned char *uData = (unsigned char *)data; +template +class Crc32Impl { +public: + constexpr static size_t size() { + return sizeof(CrcType); + } - static const uint16_t table[256] BAKELITE_CONST = { - 0x0000U,0xC0C1U,0xC181U,0x0140U,0xC301U,0x03C0U,0x0280U,0xC241U, - 0xC601U,0x06C0U,0x0780U,0xC741U,0x0500U,0xC5C1U,0xC481U,0x0440U, - 0xCC01U,0x0CC0U,0x0D80U,0xCD41U,0x0F00U,0xCFC1U,0xCE81U,0x0E40U, - 0x0A00U,0xCAC1U,0xCB81U,0x0B40U,0xC901U,0x09C0U,0x0880U,0xC841U, - 0xD801U,0x18C0U,0x1980U,0xD941U,0x1B00U,0xDBC1U,0xDA81U,0x1A40U, - 0x1E00U,0xDEC1U,0xDF81U,0x1F40U,0xDD01U,0x1DC0U,0x1C80U,0xDC41U, - 0x1400U,0xD4C1U,0xD581U,0x1540U,0xD701U,0x17C0U,0x1680U,0xD641U, - 0xD201U,0x12C0U,0x1380U,0xD341U,0x1100U,0xD1C1U,0xD081U,0x1040U, - 0xF001U,0x30C0U,0x3180U,0xF141U,0x3300U,0xF3C1U,0xF281U,0x3240U, - 0x3600U,0xF6C1U,0xF781U,0x3740U,0xF501U,0x35C0U,0x3480U,0xF441U, - 0x3C00U,0xFCC1U,0xFD81U,0x3D40U,0xFF01U,0x3FC0U,0x3E80U,0xFE41U, - 0xFA01U,0x3AC0U,0x3B80U,0xFB41U,0x3900U,0xF9C1U,0xF881U,0x3840U, - 0x2800U,0xE8C1U,0xE981U,0x2940U,0xEB01U,0x2BC0U,0x2A80U,0xEA41U, - 0xEE01U,0x2EC0U,0x2F80U,0xEF41U,0x2D00U,0xEDC1U,0xEC81U,0x2C40U, - 0xE401U,0x24C0U,0x2580U,0xE541U,0x2700U,0xE7C1U,0xE681U,0x2640U, - 0x2200U,0xE2C1U,0xE381U,0x2340U,0xE101U,0x21C0U,0x2080U,0xE041U, - 0xA001U,0x60C0U,0x6180U,0xA141U,0x6300U,0xA3C1U,0xA281U,0x6240U, - 0x6600U,0xA6C1U,0xA781U,0x6740U,0xA501U,0x65C0U,0x6480U,0xA441U, - 0x6C00U,0xACC1U,0xAD81U,0x6D40U,0xAF01U,0x6FC0U,0x6E80U,0xAE41U, - 0xAA01U,0x6AC0U,0x6B80U,0xAB41U,0x6900U,0xA9C1U,0xA881U,0x6840U, - 0x7800U,0xB8C1U,0xB981U,0x7940U,0xBB01U,0x7BC0U,0x7A80U,0xBA41U, - 0xBE01U,0x7EC0U,0x7F80U,0xBF41U,0x7D00U,0xBDC1U,0xBC81U,0x7C40U, - 0xB401U,0x74C0U,0x7580U,0xB541U,0x7700U,0xB7C1U,0xB681U,0x7640U, - 0x7200U,0xB2C1U,0xB381U,0x7340U,0xB101U,0x71C0U,0x7080U,0xB041U, - 0x5000U,0x90C1U,0x9181U,0x5140U,0x9301U,0x53C0U,0x5280U,0x9241U, - 0x9601U,0x56C0U,0x5780U,0x9741U,0x5500U,0x95C1U,0x9481U,0x5440U, - 0x9C01U,0x5CC0U,0x5D80U,0x9D41U,0x5F00U,0x9FC1U,0x9E81U,0x5E40U, - 0x5A00U,0x9AC1U,0x9B81U,0x5B40U,0x9901U,0x59C0U,0x5880U,0x9841U, - 0x8801U,0x48C0U,0x4980U,0x8941U,0x4B00U,0x8BC1U,0x8A81U,0x4A40U, - 0x4E00U,0x8EC1U,0x8F81U,0x4F40U,0x8D01U,0x4DC0U,0x4C80U,0x8C41U, - 0x4400U,0x84C1U,0x8581U,0x4540U,0x8701U,0x47C0U,0x4680U,0x8641U, - 0x8201U,0x42C0U,0x4380U,0x8341U,0x4100U,0x81C1U,0x8081U,0x4040U, - }; + CrcType value() const { + return m_lastVal; + } - while (len > 0) - { - crc = BAKELITE_CONST_16(table[*uData ^ (uint8_t)crc]) ^ (crc >> 8); - uData++; - len--; - } - return crc; + void update(const char *data, size_t length) { + m_lastVal = bakelite_crc32((const uint8_t *)data, length, m_lastVal); } +private: + CrcType m_lastVal = 0; }; -// Automatically generated CRC function -// polynomial: 0x104C11DB7, bit reverse algorithm -struct crc32_fn { - uint32_t operator()(const char *data, int len, uint32_t crc) { - const unsigned char *uData = (unsigned char *)data; - - static const uint32_t table[256] BAKELITE_CONST = { - 0x00000000U,0x77073096U,0xEE0E612CU,0x990951BAU, - 0x076DC419U,0x706AF48FU,0xE963A535U,0x9E6495A3U, - 0x0EDB8832U,0x79DCB8A4U,0xE0D5E91EU,0x97D2D988U, - 0x09B64C2BU,0x7EB17CBDU,0xE7B82D07U,0x90BF1D91U, - 0x1DB71064U,0x6AB020F2U,0xF3B97148U,0x84BE41DEU, - 0x1ADAD47DU,0x6DDDE4EBU,0xF4D4B551U,0x83D385C7U, - 0x136C9856U,0x646BA8C0U,0xFD62F97AU,0x8A65C9ECU, - 0x14015C4FU,0x63066CD9U,0xFA0F3D63U,0x8D080DF5U, - 0x3B6E20C8U,0x4C69105EU,0xD56041E4U,0xA2677172U, - 0x3C03E4D1U,0x4B04D447U,0xD20D85FDU,0xA50AB56BU, - 0x35B5A8FAU,0x42B2986CU,0xDBBBC9D6U,0xACBCF940U, - 0x32D86CE3U,0x45DF5C75U,0xDCD60DCFU,0xABD13D59U, - 0x26D930ACU,0x51DE003AU,0xC8D75180U,0xBFD06116U, - 0x21B4F4B5U,0x56B3C423U,0xCFBA9599U,0xB8BDA50FU, - 0x2802B89EU,0x5F058808U,0xC60CD9B2U,0xB10BE924U, - 0x2F6F7C87U,0x58684C11U,0xC1611DABU,0xB6662D3DU, - 0x76DC4190U,0x01DB7106U,0x98D220BCU,0xEFD5102AU, - 0x71B18589U,0x06B6B51FU,0x9FBFE4A5U,0xE8B8D433U, - 0x7807C9A2U,0x0F00F934U,0x9609A88EU,0xE10E9818U, - 0x7F6A0DBBU,0x086D3D2DU,0x91646C97U,0xE6635C01U, - 0x6B6B51F4U,0x1C6C6162U,0x856530D8U,0xF262004EU, - 0x6C0695EDU,0x1B01A57BU,0x8208F4C1U,0xF50FC457U, - 0x65B0D9C6U,0x12B7E950U,0x8BBEB8EAU,0xFCB9887CU, - 0x62DD1DDFU,0x15DA2D49U,0x8CD37CF3U,0xFBD44C65U, - 0x4DB26158U,0x3AB551CEU,0xA3BC0074U,0xD4BB30E2U, - 0x4ADFA541U,0x3DD895D7U,0xA4D1C46DU,0xD3D6F4FBU, - 0x4369E96AU,0x346ED9FCU,0xAD678846U,0xDA60B8D0U, - 0x44042D73U,0x33031DE5U,0xAA0A4C5FU,0xDD0D7CC9U, - 0x5005713CU,0x270241AAU,0xBE0B1010U,0xC90C2086U, - 0x5768B525U,0x206F85B3U,0xB966D409U,0xCE61E49FU, - 0x5EDEF90EU,0x29D9C998U,0xB0D09822U,0xC7D7A8B4U, - 0x59B33D17U,0x2EB40D81U,0xB7BD5C3BU,0xC0BA6CADU, - 0xEDB88320U,0x9ABFB3B6U,0x03B6E20CU,0x74B1D29AU, - 0xEAD54739U,0x9DD277AFU,0x04DB2615U,0x73DC1683U, - 0xE3630B12U,0x94643B84U,0x0D6D6A3EU,0x7A6A5AA8U, - 0xE40ECF0BU,0x9309FF9DU,0x0A00AE27U,0x7D079EB1U, - 0xF00F9344U,0x8708A3D2U,0x1E01F268U,0x6906C2FEU, - 0xF762575DU,0x806567CBU,0x196C3671U,0x6E6B06E7U, - 0xFED41B76U,0x89D32BE0U,0x10DA7A5AU,0x67DD4ACCU, - 0xF9B9DF6FU,0x8EBEEFF9U,0x17B7BE43U,0x60B08ED5U, - 0xD6D6A3E8U,0xA1D1937EU,0x38D8C2C4U,0x4FDFF252U, - 0xD1BB67F1U,0xA6BC5767U,0x3FB506DDU,0x48B2364BU, - 0xD80D2BDAU,0xAF0A1B4CU,0x36034AF6U,0x41047A60U, - 0xDF60EFC3U,0xA867DF55U,0x316E8EEFU,0x4669BE79U, - 0xCB61B38CU,0xBC66831AU,0x256FD2A0U,0x5268E236U, - 0xCC0C7795U,0xBB0B4703U,0x220216B9U,0x5505262FU, - 0xC5BA3BBEU,0xB2BD0B28U,0x2BB45A92U,0x5CB36A04U, - 0xC2D7FFA7U,0xB5D0CF31U,0x2CD99E8BU,0x5BDEAE1DU, - 0x9B64C2B0U,0xEC63F226U,0x756AA39CU,0x026D930AU, - 0x9C0906A9U,0xEB0E363FU,0x72076785U,0x05005713U, - 0x95BF4A82U,0xE2B87A14U,0x7BB12BAEU,0x0CB61B38U, - 0x92D28E9BU,0xE5D5BE0DU,0x7CDCEFB7U,0x0BDBDF21U, - 0x86D3D2D4U,0xF1D4E242U,0x68DDB3F8U,0x1FDA836EU, - 0x81BE16CDU,0xF6B9265BU,0x6FB077E1U,0x18B74777U, - 0x88085AE6U,0xFF0F6A70U,0x66063BCAU,0x11010B5CU, - 0x8F659EFFU,0xF862AE69U,0x616BFFD3U,0x166CCF45U, - 0xA00AE278U,0xD70DD2EEU,0x4E048354U,0x3903B3C2U, - 0xA7672661U,0xD06016F7U,0x4969474DU,0x3E6E77DBU, - 0xAED16A4AU,0xD9D65ADCU,0x40DF0B66U,0x37D83BF0U, - 0xA9BCAE53U,0xDEBB9EC5U,0x47B2CF7FU,0x30B5FFE9U, - 0xBDBDF21CU,0xCABAC28AU,0x53B39330U,0x24B4A3A6U, - 0xBAD03605U,0xCDD70693U,0x54DE5729U,0x23D967BFU, - 0xB3667A2EU,0xC4614AB8U,0x5D681B02U,0x2A6F2B94U, - 0xB40BBE37U,0xC30C8EA1U,0x5A05DF1BU,0x2D02EF8DU, - }; - - crc = crc ^ 0xFFFFFFFFU; - while (len > 0) - { - crc = BAKELITE_CONST_32(table[*uData ^ (uint8_t)crc]) ^ (crc >> 8); - uData++; - len--; - } - crc = crc ^ 0xFFFFFFFFU; - return crc; - } -}; \ No newline at end of file +using Crc8 = Crc8Impl; +using Crc16 = Crc16Impl; +using Crc32 = Crc32Impl; diff --git a/bakelite/generator/runtimes/cpptiny/declarations.h b/bakelite/generator/runtimes/cpptiny/declarations.h index 90289a1..1882f01 100644 --- a/bakelite/generator/runtimes/cpptiny/declarations.h +++ b/bakelite/generator/runtimes/cpptiny/declarations.h @@ -1,11 +1,24 @@ -// Pre-declarations of COBS functions -struct cobs_encode_result; -struct cobs_decode_result -{ - size_t out_len; - int status; +// C++ wrapper types and functions for COBS encode/decode +// These wrap the common C99 implementation for use in the Bakelite namespace + +struct cobs_encode_result { + size_t out_len; + int status; }; -static cobs_encode_result cobs_encode(void *dst_buf_ptr, size_t dst_buf_len, - const void *src_ptr, size_t src_len); -static cobs_decode_result cobs_decode(void *dst_buf_ptr, size_t dst_buf_len, - const void *src_ptr, size_t src_len); \ No newline at end of file + +struct cobs_decode_result { + size_t out_len; + int status; +}; + +static inline cobs_encode_result cobs_encode(void *dst_buf_ptr, size_t dst_buf_len, + const void *src_ptr, size_t src_len) { + Bakelite_CobsEncodeResult c_result = bakelite_cobs_encode(dst_buf_ptr, dst_buf_len, src_ptr, src_len); + return {c_result.out_len, c_result.status}; +} + +static inline cobs_decode_result cobs_decode(void *dst_buf_ptr, size_t dst_buf_len, + const void *src_ptr, size_t src_len) { + Bakelite_CobsDecodeResult c_result = bakelite_cobs_decode(dst_buf_ptr, dst_buf_len, src_ptr, src_len); + return {c_result.out_len, c_result.status}; +} diff --git a/bakelite/generator/runtimes/ctiny/cobs.h b/bakelite/generator/runtimes/ctiny/cobs.h index 3635e97..a8ff9f9 100644 --- a/bakelite/generator/runtimes/ctiny/cobs.h +++ b/bakelite/generator/runtimes/ctiny/cobs.h @@ -1,14 +1,7 @@ /* - * COBS encode/decode functions are Copyright (c) 2010 Craig McQueen - * Licensed under the MIT license (see end of file). - * Source: https://github.com/cmcqueen/cobs-c + * ctiny COBS Framer - uses common C99 COBS implementation */ -/* COBS buffer size calculations */ -#define BAKELITE_COBS_ENCODE_DST_BUF_LEN_MAX(SRC_LEN) ((SRC_LEN) + (((SRC_LEN) + 253u) / 254u)) -#define BAKELITE_COBS_DECODE_DST_BUF_LEN_MAX(SRC_LEN) (((SRC_LEN) == 0) ? 0u : ((SRC_LEN) - 1u)) -#define BAKELITE_COBS_OVERHEAD(BUF_SIZE) (((BUF_SIZE) + 253u) / 254u) - /* Calculate total buffer size needed for framer */ #define BAKELITE_FRAMER_BUFFER_SIZE(MAX_MSG_SIZE, CRC_SIZE) \ (BAKELITE_COBS_OVERHEAD((MAX_MSG_SIZE) + (CRC_SIZE)) + (MAX_MSG_SIZE) + (CRC_SIZE) + 1) @@ -26,32 +19,6 @@ typedef enum { BAKELITE_DECODE_BUFFER_OVERRUN } Bakelite_DecodeState; -/* COBS encode/decode status */ -typedef enum { - COBS_ENCODE_OK = 0x00, - COBS_ENCODE_NULL_POINTER = 0x01, - COBS_ENCODE_OUT_BUFFER_OVERFLOW = 0x02 -} Bakelite_CobsEncodeStatus; - -typedef enum { - COBS_DECODE_OK = 0x00, - COBS_DECODE_NULL_POINTER = 0x01, - COBS_DECODE_OUT_BUFFER_OVERFLOW = 0x02, - COBS_DECODE_ZERO_BYTE_IN_INPUT = 0x04, - COBS_DECODE_INPUT_TOO_SHORT = 0x08 -} Bakelite_CobsDecodeStatus; - -/* COBS encode/decode results */ -typedef struct { - size_t out_len; - int status; -} Bakelite_CobsEncodeResult; - -typedef struct { - size_t out_len; - int status; -} Bakelite_CobsDecodeResult; - /* Framer result */ typedef struct { int status; @@ -85,12 +52,6 @@ typedef struct { uint8_t *read_pos; } Bakelite_CobsFramer; -/* Forward declarations */ -static Bakelite_CobsEncodeResult bakelite_cobs_encode(void *dst_buf_ptr, size_t dst_buf_len, - const void *src_ptr, size_t src_len); -static Bakelite_CobsDecodeResult bakelite_cobs_decode(void *dst_buf_ptr, size_t dst_buf_len, - const void *src_ptr, size_t src_len); - /* Get CRC size for a CRC type */ static inline size_t bakelite_crc_size(Bakelite_CrcType crc_type) { switch (crc_type) { @@ -267,154 +228,3 @@ static inline Bakelite_DecodeResult bakelite_framer_read_byte(Bakelite_CobsFrame framer->read_pos++; return (Bakelite_DecodeResult){ BAKELITE_DECODE_NOT_READY, 0, NULL }; } - -/* - * COBS encode/decode implementation - * Copyright (c) 2010 Craig McQueen, MIT License - */ -static Bakelite_CobsEncodeResult bakelite_cobs_encode(void *dst_buf_ptr, size_t dst_buf_len, - const void *src_ptr, size_t src_len) { - Bakelite_CobsEncodeResult result = {0, COBS_ENCODE_OK}; - const uint8_t *src_read_ptr = (const uint8_t *)src_ptr; - const uint8_t *src_end_ptr = src_read_ptr + src_len; - uint8_t *dst_buf_start_ptr = (uint8_t *)dst_buf_ptr; - uint8_t *dst_buf_end_ptr = dst_buf_start_ptr + dst_buf_len; - uint8_t *dst_code_write_ptr = (uint8_t *)dst_buf_ptr; - uint8_t *dst_write_ptr = dst_code_write_ptr + 1; - uint8_t src_byte = 0; - uint8_t search_len = 1; - - if ((dst_buf_ptr == NULL) || (src_ptr == NULL)) { - result.status = COBS_ENCODE_NULL_POINTER; - return result; - } - - if (src_len != 0) { - for (;;) { - if (dst_write_ptr >= dst_buf_end_ptr) { - result.status |= COBS_ENCODE_OUT_BUFFER_OVERFLOW; - break; - } - - src_byte = *src_read_ptr++; - if (src_byte == 0) { - *dst_code_write_ptr = search_len; - dst_code_write_ptr = dst_write_ptr++; - search_len = 1; - if (src_read_ptr >= src_end_ptr) { - break; - } - } else { - *dst_write_ptr++ = src_byte; - search_len++; - if (src_read_ptr >= src_end_ptr) { - break; - } - if (search_len == 0xFF) { - *dst_code_write_ptr = search_len; - dst_code_write_ptr = dst_write_ptr++; - search_len = 1; - } - } - } - } - - if (dst_code_write_ptr >= dst_buf_end_ptr) { - result.status |= COBS_ENCODE_OUT_BUFFER_OVERFLOW; - dst_write_ptr = dst_buf_end_ptr; - } else { - *dst_code_write_ptr = search_len; - } - - result.out_len = (size_t)(dst_write_ptr - dst_buf_start_ptr); - return result; -} - -static Bakelite_CobsDecodeResult bakelite_cobs_decode(void *dst_buf_ptr, size_t dst_buf_len, - const void *src_ptr, size_t src_len) { - Bakelite_CobsDecodeResult result = {0, COBS_DECODE_OK}; - const uint8_t *src_read_ptr = (const uint8_t *)src_ptr; - const uint8_t *src_end_ptr = src_read_ptr + src_len; - uint8_t *dst_buf_start_ptr = (uint8_t *)dst_buf_ptr; - uint8_t *dst_buf_end_ptr = dst_buf_start_ptr + dst_buf_len; - uint8_t *dst_write_ptr = (uint8_t *)dst_buf_ptr; - size_t remaining_bytes; - uint8_t src_byte; - uint8_t i; - uint8_t len_code; - - if ((dst_buf_ptr == NULL) || (src_ptr == NULL)) { - result.status = COBS_DECODE_NULL_POINTER; - return result; - } - - if (src_len != 0) { - for (;;) { - len_code = *src_read_ptr++; - if (len_code == 0) { - result.status |= COBS_DECODE_ZERO_BYTE_IN_INPUT; - break; - } - len_code--; - - remaining_bytes = (size_t)(src_end_ptr - src_read_ptr); - if (len_code > remaining_bytes) { - result.status |= COBS_DECODE_INPUT_TOO_SHORT; - len_code = (uint8_t)remaining_bytes; - } - - remaining_bytes = (size_t)(dst_buf_end_ptr - dst_write_ptr); - if (len_code > remaining_bytes) { - result.status |= COBS_DECODE_OUT_BUFFER_OVERFLOW; - len_code = (uint8_t)remaining_bytes; - } - - for (i = len_code; i != 0; i--) { - src_byte = *src_read_ptr++; - if (src_byte == 0) { - result.status |= COBS_DECODE_ZERO_BYTE_IN_INPUT; - } - *dst_write_ptr++ = src_byte; - } - - if (src_read_ptr >= src_end_ptr) { - break; - } - - if (len_code != 0xFE) { - if (dst_write_ptr >= dst_buf_end_ptr) { - result.status |= COBS_DECODE_OUT_BUFFER_OVERFLOW; - break; - } - *dst_write_ptr++ = 0; - } - } - } - - result.out_len = (size_t)(dst_write_ptr - dst_buf_start_ptr); - return result; -} - -/* - * MIT License for COBS encode/decode functions: - * - * Copyright (c) 2010 Craig McQueen - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ diff --git a/bakelite/generator/runtimes/ctiny/crc.h b/bakelite/generator/runtimes/ctiny/crc.h index ce04a0d..8078ff9 100644 --- a/bakelite/generator/runtimes/ctiny/crc.h +++ b/bakelite/generator/runtimes/ctiny/crc.h @@ -1,193 +1,12 @@ /* - * The CRC lookup tables are stored as const variables. On many platforms, - * const variables are stored in flash memory. On AVR though, they are - * loaded into RAM on startup. A special PROGMEM macro is available on AVRs - * to indicate constants should be stored in program memory (flash). - * So if this macro is available, use it and assume we're on an AVR. + * ctiny CRC - uses common C99 implementation + * This file is intentionally minimal as the implementation is in common/crc.h */ -#ifdef PROGMEM - #define BAKELITE_CONST PROGMEM - #define BAKELITE_CONST_8(x) pgm_read_byte(&(x)) - #define BAKELITE_CONST_16(x) pgm_read_word(&(x)) - #define BAKELITE_CONST_32(x) ((uint32_t)pgm_read_dword((char *)&(x) + 2) << 16 | pgm_read_dword(&(x))) -#else - #define BAKELITE_CONST - #define BAKELITE_CONST_8(x) (x) - #define BAKELITE_CONST_16(x) (x) - #define BAKELITE_CONST_32(x) (x) -#endif - -/* CRC-8 polynomial: 0x107 */ -static inline uint8_t bakelite_crc8(const uint8_t *data, size_t len, uint8_t crc) { - static const uint8_t table[256] BAKELITE_CONST = { - 0x00U,0x07U,0x0EU,0x09U,0x1CU,0x1BU,0x12U,0x15U, - 0x38U,0x3FU,0x36U,0x31U,0x24U,0x23U,0x2AU,0x2DU, - 0x70U,0x77U,0x7EU,0x79U,0x6CU,0x6BU,0x62U,0x65U, - 0x48U,0x4FU,0x46U,0x41U,0x54U,0x53U,0x5AU,0x5DU, - 0xE0U,0xE7U,0xEEU,0xE9U,0xFCU,0xFBU,0xF2U,0xF5U, - 0xD8U,0xDFU,0xD6U,0xD1U,0xC4U,0xC3U,0xCAU,0xCDU, - 0x90U,0x97U,0x9EU,0x99U,0x8CU,0x8BU,0x82U,0x85U, - 0xA8U,0xAFU,0xA6U,0xA1U,0xB4U,0xB3U,0xBAU,0xBDU, - 0xC7U,0xC0U,0xC9U,0xCEU,0xDBU,0xDCU,0xD5U,0xD2U, - 0xFFU,0xF8U,0xF1U,0xF6U,0xE3U,0xE4U,0xEDU,0xEAU, - 0xB7U,0xB0U,0xB9U,0xBEU,0xABU,0xACU,0xA5U,0xA2U, - 0x8FU,0x88U,0x81U,0x86U,0x93U,0x94U,0x9DU,0x9AU, - 0x27U,0x20U,0x29U,0x2EU,0x3BU,0x3CU,0x35U,0x32U, - 0x1FU,0x18U,0x11U,0x16U,0x03U,0x04U,0x0DU,0x0AU, - 0x57U,0x50U,0x59U,0x5EU,0x4BU,0x4CU,0x45U,0x42U, - 0x6FU,0x68U,0x61U,0x66U,0x73U,0x74U,0x7DU,0x7AU, - 0x89U,0x8EU,0x87U,0x80U,0x95U,0x92U,0x9BU,0x9CU, - 0xB1U,0xB6U,0xBFU,0xB8U,0xADU,0xAAU,0xA3U,0xA4U, - 0xF9U,0xFEU,0xF7U,0xF0U,0xE5U,0xE2U,0xEBU,0xECU, - 0xC1U,0xC6U,0xCFU,0xC8U,0xDDU,0xDAU,0xD3U,0xD4U, - 0x69U,0x6EU,0x67U,0x60U,0x75U,0x72U,0x7BU,0x7CU, - 0x51U,0x56U,0x5FU,0x58U,0x4DU,0x4AU,0x43U,0x44U, - 0x19U,0x1EU,0x17U,0x10U,0x05U,0x02U,0x0BU,0x0CU, - 0x21U,0x26U,0x2FU,0x28U,0x3DU,0x3AU,0x33U,0x34U, - 0x4EU,0x49U,0x40U,0x47U,0x52U,0x55U,0x5CU,0x5BU, - 0x76U,0x71U,0x78U,0x7FU,0x6AU,0x6DU,0x64U,0x63U, - 0x3EU,0x39U,0x30U,0x37U,0x22U,0x25U,0x2CU,0x2BU, - 0x06U,0x01U,0x08U,0x0FU,0x1AU,0x1DU,0x14U,0x13U, - 0xAEU,0xA9U,0xA0U,0xA7U,0xB2U,0xB5U,0xBCU,0xBBU, - 0x96U,0x91U,0x98U,0x9FU,0x8AU,0x8DU,0x84U,0x83U, - 0xDEU,0xD9U,0xD0U,0xD7U,0xC2U,0xC5U,0xCCU,0xCBU, - 0xE6U,0xE1U,0xE8U,0xEFU,0xFAU,0xFDU,0xF4U,0xF3U, - }; - - while (len > 0) { - crc = BAKELITE_CONST_8(table[*data ^ crc]); - data++; - len--; - } - return crc; -} - -/* CRC-16 polynomial: 0x18005, bit reverse algorithm */ -static inline uint16_t bakelite_crc16(const uint8_t *data, size_t len, uint16_t crc) { - static const uint16_t table[256] BAKELITE_CONST = { - 0x0000U,0xC0C1U,0xC181U,0x0140U,0xC301U,0x03C0U,0x0280U,0xC241U, - 0xC601U,0x06C0U,0x0780U,0xC741U,0x0500U,0xC5C1U,0xC481U,0x0440U, - 0xCC01U,0x0CC0U,0x0D80U,0xCD41U,0x0F00U,0xCFC1U,0xCE81U,0x0E40U, - 0x0A00U,0xCAC1U,0xCB81U,0x0B40U,0xC901U,0x09C0U,0x0880U,0xC841U, - 0xD801U,0x18C0U,0x1980U,0xD941U,0x1B00U,0xDBC1U,0xDA81U,0x1A40U, - 0x1E00U,0xDEC1U,0xDF81U,0x1F40U,0xDD01U,0x1DC0U,0x1C80U,0xDC41U, - 0x1400U,0xD4C1U,0xD581U,0x1540U,0xD701U,0x17C0U,0x1680U,0xD641U, - 0xD201U,0x12C0U,0x1380U,0xD341U,0x1100U,0xD1C1U,0xD081U,0x1040U, - 0xF001U,0x30C0U,0x3180U,0xF141U,0x3300U,0xF3C1U,0xF281U,0x3240U, - 0x3600U,0xF6C1U,0xF781U,0x3740U,0xF501U,0x35C0U,0x3480U,0xF441U, - 0x3C00U,0xFCC1U,0xFD81U,0x3D40U,0xFF01U,0x3FC0U,0x3E80U,0xFE41U, - 0xFA01U,0x3AC0U,0x3B80U,0xFB41U,0x3900U,0xF9C1U,0xF881U,0x3840U, - 0x2800U,0xE8C1U,0xE981U,0x2940U,0xEB01U,0x2BC0U,0x2A80U,0xEA41U, - 0xEE01U,0x2EC0U,0x2F80U,0xEF41U,0x2D00U,0xEDC1U,0xEC81U,0x2C40U, - 0xE401U,0x24C0U,0x2580U,0xE541U,0x2700U,0xE7C1U,0xE681U,0x2640U, - 0x2200U,0xE2C1U,0xE381U,0x2340U,0xE101U,0x21C0U,0x2080U,0xE041U, - 0xA001U,0x60C0U,0x6180U,0xA141U,0x6300U,0xA3C1U,0xA281U,0x6240U, - 0x6600U,0xA6C1U,0xA781U,0x6740U,0xA501U,0x65C0U,0x6480U,0xA441U, - 0x6C00U,0xACC1U,0xAD81U,0x6D40U,0xAF01U,0x6FC0U,0x6E80U,0xAE41U, - 0xAA01U,0x6AC0U,0x6B80U,0xAB41U,0x6900U,0xA9C1U,0xA881U,0x6840U, - 0x7800U,0xB8C1U,0xB981U,0x7940U,0xBB01U,0x7BC0U,0x7A80U,0xBA41U, - 0xBE01U,0x7EC0U,0x7F80U,0xBF41U,0x7D00U,0xBDC1U,0xBC81U,0x7C40U, - 0xB401U,0x74C0U,0x7580U,0xB541U,0x7700U,0xB7C1U,0xB681U,0x7640U, - 0x7200U,0xB2C1U,0xB381U,0x7340U,0xB101U,0x71C0U,0x7080U,0xB041U, - 0x5000U,0x90C1U,0x9181U,0x5140U,0x9301U,0x53C0U,0x5280U,0x9241U, - 0x9601U,0x56C0U,0x5780U,0x9741U,0x5500U,0x95C1U,0x9481U,0x5440U, - 0x9C01U,0x5CC0U,0x5D80U,0x9D41U,0x5F00U,0x9FC1U,0x9E81U,0x5E40U, - 0x5A00U,0x9AC1U,0x9B81U,0x5B40U,0x9901U,0x59C0U,0x5880U,0x9841U, - 0x8801U,0x48C0U,0x4980U,0x8941U,0x4B00U,0x8BC1U,0x8A81U,0x4A40U, - 0x4E00U,0x8EC1U,0x8F81U,0x4F40U,0x8D01U,0x4DC0U,0x4C80U,0x8C41U, - 0x4400U,0x84C1U,0x8581U,0x4540U,0x8701U,0x47C0U,0x4680U,0x8641U, - 0x8201U,0x42C0U,0x4380U,0x8341U,0x4100U,0x81C1U,0x8081U,0x4040U, - }; - while (len > 0) { - crc = BAKELITE_CONST_16(table[*data ^ (uint8_t)crc]) ^ (crc >> 8); - data++; - len--; - } - return crc; -} - -/* CRC-32 polynomial: 0x104C11DB7, bit reverse algorithm */ -static inline uint32_t bakelite_crc32(const uint8_t *data, size_t len, uint32_t crc) { - static const uint32_t table[256] BAKELITE_CONST = { - 0x00000000U,0x77073096U,0xEE0E612CU,0x990951BAU, - 0x076DC419U,0x706AF48FU,0xE963A535U,0x9E6495A3U, - 0x0EDB8832U,0x79DCB8A4U,0xE0D5E91EU,0x97D2D988U, - 0x09B64C2BU,0x7EB17CBDU,0xE7B82D07U,0x90BF1D91U, - 0x1DB71064U,0x6AB020F2U,0xF3B97148U,0x84BE41DEU, - 0x1ADAD47DU,0x6DDDE4EBU,0xF4D4B551U,0x83D385C7U, - 0x136C9856U,0x646BA8C0U,0xFD62F97AU,0x8A65C9ECU, - 0x14015C4FU,0x63066CD9U,0xFA0F3D63U,0x8D080DF5U, - 0x3B6E20C8U,0x4C69105EU,0xD56041E4U,0xA2677172U, - 0x3C03E4D1U,0x4B04D447U,0xD20D85FDU,0xA50AB56BU, - 0x35B5A8FAU,0x42B2986CU,0xDBBBC9D6U,0xACBCF940U, - 0x32D86CE3U,0x45DF5C75U,0xDCD60DCFU,0xABD13D59U, - 0x26D930ACU,0x51DE003AU,0xC8D75180U,0xBFD06116U, - 0x21B4F4B5U,0x56B3C423U,0xCFBA9599U,0xB8BDA50FU, - 0x2802B89EU,0x5F058808U,0xC60CD9B2U,0xB10BE924U, - 0x2F6F7C87U,0x58684C11U,0xC1611DABU,0xB6662D3DU, - 0x76DC4190U,0x01DB7106U,0x98D220BCU,0xEFD5102AU, - 0x71B18589U,0x06B6B51FU,0x9FBFE4A5U,0xE8B8D433U, - 0x7807C9A2U,0x0F00F934U,0x9609A88EU,0xE10E9818U, - 0x7F6A0DBBU,0x086D3D2DU,0x91646C97U,0xE6635C01U, - 0x6B6B51F4U,0x1C6C6162U,0x856530D8U,0xF262004EU, - 0x6C0695EDU,0x1B01A57BU,0x8208F4C1U,0xF50FC457U, - 0x65B0D9C6U,0x12B7E950U,0x8BBEB8EAU,0xFCB9887CU, - 0x62DD1DDFU,0x15DA2D49U,0x8CD37CF3U,0xFBD44C65U, - 0x4DB26158U,0x3AB551CEU,0xA3BC0074U,0xD4BB30E2U, - 0x4ADFA541U,0x3DD895D7U,0xA4D1C46DU,0xD3D6F4FBU, - 0x4369E96AU,0x346ED9FCU,0xAD678846U,0xDA60B8D0U, - 0x44042D73U,0x33031DE5U,0xAA0A4C5FU,0xDD0D7CC9U, - 0x5005713CU,0x270241AAU,0xBE0B1010U,0xC90C2086U, - 0x5768B525U,0x206F85B3U,0xB966D409U,0xCE61E49FU, - 0x5EDEF90EU,0x29D9C998U,0xB0D09822U,0xC7D7A8B4U, - 0x59B33D17U,0x2EB40D81U,0xB7BD5C3BU,0xC0BA6CADU, - 0xEDB88320U,0x9ABFB3B6U,0x03B6E20CU,0x74B1D29AU, - 0xEAD54739U,0x9DD277AFU,0x04DB2615U,0x73DC1683U, - 0xE3630B12U,0x94643B84U,0x0D6D6A3EU,0x7A6A5AA8U, - 0xE40ECF0BU,0x9309FF9DU,0x0A00AE27U,0x7D079EB1U, - 0xF00F9344U,0x8708A3D2U,0x1E01F268U,0x6906C2FEU, - 0xF762575DU,0x806567CBU,0x196C3671U,0x6E6B06E7U, - 0xFED41B76U,0x89D32BE0U,0x10DA7A5AU,0x67DD4ACCU, - 0xF9B9DF6FU,0x8EBEEFF9U,0x17B7BE43U,0x60B08ED5U, - 0xD6D6A3E8U,0xA1D1937EU,0x38D8C2C4U,0x4FDFF252U, - 0xD1BB67F1U,0xA6BC5767U,0x3FB506DDU,0x48B2364BU, - 0xD80D2BDAU,0xAF0A1B4CU,0x36034AF6U,0x41047A60U, - 0xDF60EFC3U,0xA867DF55U,0x316E8EEFU,0x4669BE79U, - 0xCB61B38CU,0xBC66831AU,0x256FD2A0U,0x5268E236U, - 0xCC0C7795U,0xBB0B4703U,0x220216B9U,0x5505262FU, - 0xC5BA3BBEU,0xB2BD0B28U,0x2BB45A92U,0x5CB36A04U, - 0xC2D7FFA7U,0xB5D0CF31U,0x2CD99E8BU,0x5BDEAE1DU, - 0x9B64C2B0U,0xEC63F226U,0x756AA39CU,0x026D930AU, - 0x9C0906A9U,0xEB0E363FU,0x72076785U,0x05005713U, - 0x95BF4A82U,0xE2B87A14U,0x7BB12BAEU,0x0CB61B38U, - 0x92D28E9BU,0xE5D5BE0DU,0x7CDCEFB7U,0x0BDBDF21U, - 0x86D3D2D4U,0xF1D4E242U,0x68DDB3F8U,0x1FDA836EU, - 0x81BE16CDU,0xF6B9265BU,0x6FB077E1U,0x18B74777U, - 0x88085AE6U,0xFF0F6A70U,0x66063BCAU,0x11010B5CU, - 0x8F659EFFU,0xF862AE69U,0x616BFFD3U,0x166CCF45U, - 0xA00AE278U,0xD70DD2EEU,0x4E048354U,0x3903B3C2U, - 0xA7672661U,0xD06016F7U,0x4969474DU,0x3E6E77DBU, - 0xAED16A4AU,0xD9D65ADCU,0x40DF0B66U,0x37D83BF0U, - 0xA9BCAE53U,0xDEBB9EC5U,0x47B2CF7FU,0x30B5FFE9U, - 0xBDBDF21CU,0xCABAC28AU,0x53B39330U,0x24B4A3A6U, - 0xBAD03605U,0xCDD70693U,0x54DE5729U,0x23D967BFU, - 0xB3667A2EU,0xC4614AB8U,0x5D681B02U,0x2A6F2B94U, - 0xB40BBE37U,0xC30C8EA1U,0x5A05DF1BU,0x2D02EF8DU, - }; - - crc = crc ^ 0xFFFFFFFFU; - while (len > 0) { - crc = BAKELITE_CONST_32(table[*data ^ (uint8_t)crc]) ^ (crc >> 8); - data++; - len--; - } - crc = crc ^ 0xFFFFFFFFU; - return crc; -} - -/* CRC size constants */ +/* CRC size constants (re-exported for convenience) */ +#ifndef BAKELITE_CRC_NOOP_SIZE #define BAKELITE_CRC_NOOP_SIZE 0 #define BAKELITE_CRC8_SIZE 1 #define BAKELITE_CRC16_SIZE 2 #define BAKELITE_CRC32_SIZE 4 +#endif diff --git a/bakelite/generator/templates/cpptiny-bakelite.h.j2 b/bakelite/generator/templates/cpptiny-bakelite.h.j2 index 4b96999..c1cce9d 100644 --- a/bakelite/generator/templates/cpptiny-bakelite.h.j2 +++ b/bakelite/generator/templates/cpptiny-bakelite.h.j2 @@ -12,11 +12,27 @@ #include #include #include +#include #ifdef __AVR__ #include #endif +/* + * Common C99 implementations (CRC and COBS) + */ +#ifdef __cplusplus +extern "C" { +#endif + +{{include('common/crc.h')}} + +{{include('common/cobs.h')}} + +#ifdef __cplusplus +} +#endif + namespace Bakelite { /* * @@ -47,7 +63,7 @@ namespace Bakelite { {{include('cobs.h')}} } -/* +/* The MIT License --------------- @@ -67,7 +83,7 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +THE SOFTWARE. */ #endif // __BAKELITE_H__ diff --git a/bakelite/generator/templates/ctiny-bakelite.h.j2 b/bakelite/generator/templates/ctiny-bakelite.h.j2 index 9ebeebd..87af91d 100644 --- a/bakelite/generator/templates/ctiny-bakelite.h.j2 +++ b/bakelite/generator/templates/ctiny-bakelite.h.j2 @@ -9,10 +9,22 @@ #ifndef BAKELITE_H #define BAKELITE_H +#include +#include +#include +#include + #ifdef __AVR__ #include #endif +/* + * Common C99 implementations (CRC and COBS) + */ +{{include('common/crc.h')}} + +{{include('common/cobs.h')}} + /* * Type Definitions */ diff --git a/bakelite/tests/generator/.gitignore b/bakelite/tests/generator/.gitignore index 73926c8..9ceaa31 100644 --- a/bakelite/tests/generator/.gitignore +++ b/bakelite/tests/generator/.gitignore @@ -3,3 +3,8 @@ cpptiny bakelite.h proto.h cpptiny.dSYM/ +struct-ctiny.h +ctiny +bakelite-ctiny.h +proto-ctiny.h +ctiny.dSYM/ diff --git a/bakelite/tests/generator/Makefile b/bakelite/tests/generator/Makefile index 525d3be..1284e11 100644 --- a/bakelite/tests/generator/Makefile +++ b/bakelite/tests/generator/Makefile @@ -1,5 +1,6 @@ -INCLUDEPATH=../../generator/runtimes/cpptiny/ +CPPTINY_INCLUDEPATH=../../generator/runtimes/cpptiny/ +CTINY_INCLUDEPATH=../../generator/runtimes/ctiny/ UNAME_S := $(shell uname -s) ifeq ($(UNAME_S),Linux) @@ -10,14 +11,16 @@ ifeq ($(UNAME_S),Darwin) endif ifdef CI -CI_FLAGS = +CI_FLAGS = else CI_FLAGS = -ggdb -fsanitize=address -fno-omit-frame-pointer ${FLAGS} endif -test: cpptiny +test: cpptiny ctiny ./cpptiny + ./ctiny +# C++ (cpptiny) tests cpptiny: cpptiny-serialization.cpp cpptiny-framing.cpp cpptiny-protocol.cpp bakelite.h struct.h proto.h gcc cpptiny-serialization.cpp cpptiny-framing.cpp cpptiny-protocol.cpp ${CI_FLAGS} -lstdc++ -std=c++14 -lm -o cpptiny @@ -30,5 +33,31 @@ proto.h: proto.bakelite 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 +bakelite.h: ${CPPTINY_INCLUDEPATH}/serializer.h ${CPPTINY_INCLUDEPATH}/cobs.h ${CPPTINY_INCLUDEPATH}/crc.h ${CPPTINY_INCLUDEPATH}/declarations.h pixi run -- bakelite runtime -l cpptiny -o bakelite.h + +# C99 (ctiny) tests +ctiny: ctiny-serialization.c ctiny-framing.c ctiny-protocol.c bakelite-ctiny.h struct-ctiny.h proto-ctiny.h + gcc ctiny-serialization.c ctiny-framing.c ctiny-protocol.c ${CI_FLAGS} -std=c99 -lm -o ctiny + +.PHONY: struct-ctiny.h +struct-ctiny.h: struct-ctiny.bakelite bakelite-ctiny.h + pixi run -- bakelite gen -l ctiny -i struct-ctiny.bakelite -o struct-ctiny.h + sed -i.bak 's|#include "bakelite.h"|#include "bakelite-ctiny.h"|g' struct-ctiny.h && rm -f struct-ctiny.h.bak + +.PHONY: proto-ctiny.h +proto-ctiny.h: proto.bakelite bakelite-ctiny.h + pixi run -- bakelite gen -l ctiny -i proto.bakelite -o proto-ctiny.h + sed -i.bak 's|#include "bakelite.h"|#include "bakelite-ctiny.h"|g' proto-ctiny.h && rm -f proto-ctiny.h.bak + +.PHONY: bakelite-ctiny.h +bakelite-ctiny.h: ${CTINY_INCLUDEPATH}/serializer.h ${CTINY_INCLUDEPATH}/cobs.h ${CTINY_INCLUDEPATH}/crc.h ${CTINY_INCLUDEPATH}/types.h ${CTINY_INCLUDEPATH}/stream.h + pixi run -- bakelite runtime -l ctiny -o bakelite-ctiny.h + +clean: + rm -f cpptiny ctiny *.h + +clean-ctiny: + rm -f ctiny struct-ctiny.h proto-ctiny.h bakelite-ctiny.h + +rebuild-ctiny: clean-ctiny ctiny diff --git a/bakelite/tests/generator/ctiny-framing.c b/bakelite/tests/generator/ctiny-framing.c new file mode 100644 index 0000000..6137c6a --- /dev/null +++ b/bakelite/tests/generator/ctiny-framing.c @@ -0,0 +1,400 @@ +#include +#include +#include "bakelite-ctiny.h" +#include "greatest.h" + +/* Helper to convert buffer to hex string */ +static void hex_string(const uint8_t *data, size_t length, char *out) { + static const char hex[] = "0123456789abcdef"; + for (size_t i = 0; i < length; i++) { + out[i * 2] = hex[(data[i] >> 4) & 0x0f]; + out[i * 2 + 1] = hex[data[i] & 0x0f]; + } + out[length * 2] = '\0'; +} + +/* Helper to write a complete frame byte-by-byte */ +static Bakelite_DecodeResult write_frame(Bakelite_CobsFramer *framer, const uint8_t *data, + size_t length, Bakelite_DecodeState expected_state) { + Bakelite_DecodeResult result; + for (size_t i = 0; i < length - 1; i++) { + result = bakelite_framer_read_byte(framer, data[i]); + if (result.status != BAKELITE_DECODE_NOT_READY) { + return result; + } + } + result = bakelite_framer_read_byte(framer, data[length - 1]); + return result; +} + +/* COBS encode tests */ +TEST encode_frame(void) { + const size_t buffSize = 259; + uint8_t buffer[buffSize + BAKELITE_COBS_ENCODE_SRC_OFFSET(buffSize)]; + memset(buffer, 0xFF, sizeof(buffer)); + uint8_t *srcPtr = buffer + BAKELITE_COBS_ENCODE_SRC_OFFSET(buffSize); + + /* 256 bytes of 0xEE with 0x00 at start and end, plus 0xAA 0xBB */ + srcPtr[0] = 0x00; + for (int i = 1; i <= 254; i++) { + srcPtr[i] = 0xEE; + } + srcPtr[255] = 0x00; + srcPtr[256] = 0xAA; + srcPtr[257] = 0xBB; + + Bakelite_CobsEncodeResult result = bakelite_cobs_encode(buffer, sizeof(buffer), srcPtr, 258); + ASSERT_EQ(result.status, 0); + ASSERT_EQ(result.out_len, 260); + + memset(buffer + result.out_len, 0xFF, sizeof(buffer) - result.out_len); + char hex[600]; + hex_string(buffer, sizeof(buffer), hex); + ASSERT_STR_EQ(hex, "01ffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0103aabbff"); + PASS(); +} + +TEST encode_frame_one_byte(void) { + const size_t buffSize = 259; + uint8_t buffer[buffSize + BAKELITE_COBS_ENCODE_SRC_OFFSET(buffSize)]; + memset(buffer, 0xFF, sizeof(buffer)); + uint8_t *srcPtr = buffer + BAKELITE_COBS_ENCODE_SRC_OFFSET(buffSize); + srcPtr[0] = 0x22; + + Bakelite_CobsEncodeResult result = bakelite_cobs_encode(buffer, sizeof(buffer), srcPtr, 1); + ASSERT_EQ(result.status, 0); + ASSERT_EQ(result.out_len, 2); + + char hex[16]; + hex_string(buffer, result.out_len, hex); + ASSERT_STR_EQ(hex, "0222"); + PASS(); +} + +TEST decode_frame(void) { + uint8_t buffer[260]; + memset(buffer, 0xFF, sizeof(buffer)); + + buffer[0] = 0x01; + buffer[1] = 0xFF; + for (int i = 0; i < 254; i++) { + buffer[i + 2] = 0xEE; + } + buffer[256] = 0x01; + buffer[257] = 0x03; + buffer[258] = 0xAA; + buffer[259] = 0xBB; + + Bakelite_CobsDecodeResult result = bakelite_cobs_decode(buffer, sizeof(buffer), buffer, sizeof(buffer)); + ASSERT_EQ(result.status, 0); + ASSERT_EQ(result.out_len, 258); + + memset(buffer + result.out_len, 0xFF, sizeof(buffer) - result.out_len); + char hex[600]; + hex_string(buffer, sizeof(buffer), hex); + ASSERT_STR_EQ(hex, "00eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee00aabbffff"); + PASS(); +} + +/* Framer encode tests */ +TEST framer_encode(void) { + uint8_t buffer[BAKELITE_FRAMER_BUFFER_SIZE(256, 0)]; + Bakelite_CobsFramer framer; + bakelite_framer_init(&framer, buffer, sizeof(buffer), 256, BAKELITE_CRC_NONE); + + Bakelite_FramerResult result = bakelite_framer_encode_copy(&framer, (const uint8_t *)"\x11\x22\x33\x44", 4); + ASSERT_EQ(result.status, 0); + ASSERT_EQ(result.length, 6); + + char hex[64]; + hex_string(result.data, result.length, hex); + ASSERT_STR_EQ(hex, "051122334400"); + PASS(); +} + +TEST framer_encode_zero_length(void) { + uint8_t buffer[BAKELITE_FRAMER_BUFFER_SIZE(256, 0)]; + Bakelite_CobsFramer framer; + bakelite_framer_init(&framer, buffer, sizeof(buffer), 256, BAKELITE_CRC_NONE); + + Bakelite_FramerResult result = bakelite_framer_encode_copy(&framer, (const uint8_t *)"", 0); + ASSERT_EQ(result.status, 0); + ASSERT_EQ(result.length, 2); + + char hex[16]; + hex_string(result.data, result.length, hex); + ASSERT_STR_EQ(hex, "0100"); + PASS(); +} + +TEST framer_decode(void) { + uint8_t buffer[BAKELITE_FRAMER_BUFFER_SIZE(256, 0)]; + Bakelite_CobsFramer framer; + bakelite_framer_init(&framer, buffer, sizeof(buffer), 256, BAKELITE_CRC_NONE); + + Bakelite_DecodeResult result = write_frame(&framer, (const uint8_t *)"\x05\x11\x22\x33\x44\x00", 6, BAKELITE_DECODE_OK); + ASSERT_EQ(result.status, BAKELITE_DECODE_OK); + ASSERT_EQ(result.length, 4); + + char hex[64]; + hex_string(result.data, result.length, hex); + ASSERT_STR_EQ(hex, "11223344"); + PASS(); +} + +TEST framer_encode_one_byte(void) { + uint8_t buffer[BAKELITE_FRAMER_BUFFER_SIZE(256, 0)]; + Bakelite_CobsFramer framer; + bakelite_framer_init(&framer, buffer, sizeof(buffer), 256, BAKELITE_CRC_NONE); + + Bakelite_FramerResult result = bakelite_framer_encode_copy(&framer, (const uint8_t *)"\x22", 1); + ASSERT_EQ(result.status, 0); + ASSERT_EQ(result.length, 3); + + char hex[16]; + hex_string(result.data, result.length, hex); + ASSERT_STR_EQ(hex, "022200"); + PASS(); +} + +TEST framer_decode_zero_length(void) { + uint8_t buffer[BAKELITE_FRAMER_BUFFER_SIZE(256, 0)]; + Bakelite_CobsFramer framer; + bakelite_framer_init(&framer, buffer, sizeof(buffer), 256, BAKELITE_CRC_NONE); + + Bakelite_DecodeResult result = bakelite_framer_read_byte(&framer, 0x01); + ASSERT_EQ(result.status, BAKELITE_DECODE_NOT_READY); + result = bakelite_framer_read_byte(&framer, 0x00); + ASSERT_EQ(result.status, BAKELITE_DECODE_OK); + ASSERT_EQ(result.length, 0); + PASS(); +} + +TEST framer_decode_more_bytes(void) { + uint8_t buffer[BAKELITE_FRAMER_BUFFER_SIZE(256, 0)]; + Bakelite_CobsFramer framer; + bakelite_framer_init(&framer, buffer, sizeof(buffer), 256, BAKELITE_CRC_NONE); + + Bakelite_DecodeResult result = write_frame(&framer, (const uint8_t *)"\x05\x11\x22\x33\x44\x00", 6, BAKELITE_DECODE_OK); + ASSERT_EQ(result.length, 4); + + result = write_frame(&framer, (const uint8_t *)"\x05\x11\x22\x33\x44\x00", 6, BAKELITE_DECODE_OK); + ASSERT_EQ(result.length, 4); + + char hex[64]; + hex_string(result.data, result.length, hex); + ASSERT_STR_EQ(hex, "11223344"); + + result = bakelite_framer_read_byte(&framer, 0x05); + ASSERT_EQ(result.status, BAKELITE_DECODE_NOT_READY); + PASS(); +} + +TEST framer_decode_two_null_bytes(void) { + uint8_t buffer[BAKELITE_FRAMER_BUFFER_SIZE(256, 0)]; + Bakelite_CobsFramer framer; + bakelite_framer_init(&framer, buffer, sizeof(buffer), 256, BAKELITE_CRC_NONE); + + Bakelite_DecodeResult result = write_frame(&framer, (const uint8_t *)"\x05\x11\x22\x33\x44\x00", 6, BAKELITE_DECODE_OK); + ASSERT_EQ(result.length, 4); + + char hex[64]; + hex_string(result.data, result.length, hex); + ASSERT_STR_EQ(hex, "11223344"); + + result = bakelite_framer_read_byte(&framer, 0x00); + ASSERT_EQ(result.status, BAKELITE_DECODE_FAILURE); + PASS(); +} + +TEST framer_buffer_overrun(void) { + uint8_t buffer[BAKELITE_FRAMER_BUFFER_SIZE(2, 0)]; + Bakelite_CobsFramer framer; + bakelite_framer_init(&framer, buffer, sizeof(buffer), 2, BAKELITE_CRC_NONE); + + Bakelite_DecodeResult result = bakelite_framer_read_byte(&framer, 0x05); + ASSERT_EQ(result.status, BAKELITE_DECODE_NOT_READY); + result = bakelite_framer_read_byte(&framer, 0x11); + ASSERT_EQ(result.status, BAKELITE_DECODE_NOT_READY); + result = bakelite_framer_read_byte(&framer, 0x22); + ASSERT_EQ(result.status, BAKELITE_DECODE_NOT_READY); + result = bakelite_framer_read_byte(&framer, 0x33); + ASSERT_EQ(result.status, BAKELITE_DECODE_BUFFER_OVERRUN); + PASS(); +} + +TEST framer_decode_failure(void) { + uint8_t buffer[BAKELITE_FRAMER_BUFFER_SIZE(256, 0)]; + Bakelite_CobsFramer framer; + bakelite_framer_init(&framer, buffer, sizeof(buffer), 256, BAKELITE_CRC_NONE); + + Bakelite_DecodeResult result = write_frame(&framer, (const uint8_t *)"\x01\x11\x22\x33\x44\x00", 6, BAKELITE_DECODE_FAILURE); + ASSERT_EQ(result.status, BAKELITE_DECODE_FAILURE); + PASS(); +} + +TEST framer_decode_failure_2(void) { + uint8_t buffer[BAKELITE_FRAMER_BUFFER_SIZE(256, 0)]; + Bakelite_CobsFramer framer; + bakelite_framer_init(&framer, buffer, sizeof(buffer), 256, BAKELITE_CRC_NONE); + + Bakelite_DecodeResult result = write_frame(&framer, (const uint8_t *)"\x10\x11\x22\x33\x44\x00", 6, BAKELITE_DECODE_FAILURE); + ASSERT_EQ(result.status, BAKELITE_DECODE_FAILURE); + PASS(); +} + +TEST framer_roundtrip(void) { + uint8_t buffer[BAKELITE_FRAMER_BUFFER_SIZE(256, 0)]; + Bakelite_CobsFramer framer; + bakelite_framer_init(&framer, buffer, sizeof(buffer), 256, BAKELITE_CRC_NONE); + + Bakelite_FramerResult encodeResult = bakelite_framer_encode_copy(&framer, (const uint8_t *)"\x11\x22\x33\x44", 4); + ASSERT_EQ(encodeResult.status, 0); + + for (size_t i = 0; i < encodeResult.length - 1; i++) { + Bakelite_DecodeResult decodeResult = bakelite_framer_read_byte(&framer, encodeResult.data[i]); + ASSERT_EQ(decodeResult.status, BAKELITE_DECODE_NOT_READY); + } + + Bakelite_DecodeResult decodeResult = bakelite_framer_read_byte(&framer, encodeResult.data[encodeResult.length - 1]); + ASSERT_EQ(decodeResult.status, BAKELITE_DECODE_OK); + ASSERT_EQ(decodeResult.length, 4); + + char hex[64]; + hex_string(decodeResult.data, decodeResult.length, hex); + ASSERT_STR_EQ(hex, "11223344"); + PASS(); +} + +/* CRC encode tests */ +TEST framer_encode_crc8(void) { + uint8_t buffer[BAKELITE_FRAMER_BUFFER_SIZE(256, 1)]; + Bakelite_CobsFramer framer; + bakelite_framer_init(&framer, buffer, sizeof(buffer), 256, BAKELITE_CRC_8); + + Bakelite_FramerResult result = bakelite_framer_encode_copy(&framer, (const uint8_t *)"\x11\x22\x33\x44", 4); + ASSERT_EQ(result.status, 0); + ASSERT_EQ(result.length, 7); + + char hex[64]; + hex_string(result.data, result.length, hex); + ASSERT_STR_EQ(hex, "0611223344f900"); + PASS(); +} + +TEST framer_encode_crc16(void) { + uint8_t buffer[BAKELITE_FRAMER_BUFFER_SIZE(256, 2)]; + Bakelite_CobsFramer framer; + bakelite_framer_init(&framer, buffer, sizeof(buffer), 256, BAKELITE_CRC_16); + + Bakelite_FramerResult result = bakelite_framer_encode_copy(&framer, (const uint8_t *)"\x11\x22\x33\x44", 4); + ASSERT_EQ(result.status, 0); + ASSERT_EQ(result.length, 8); + + char hex[64]; + hex_string(result.data, result.length, hex); + ASSERT_STR_EQ(hex, "0711223344b1f500"); + PASS(); +} + +TEST framer_encode_crc32(void) { + uint8_t buffer[BAKELITE_FRAMER_BUFFER_SIZE(256, 4)]; + Bakelite_CobsFramer framer; + bakelite_framer_init(&framer, buffer, sizeof(buffer), 256, BAKELITE_CRC_32); + + Bakelite_FramerResult result = bakelite_framer_encode_copy(&framer, (const uint8_t *)"\x11\x22\x33\x44", 4); + ASSERT_EQ(result.status, 0); + ASSERT_EQ(result.length, 10); + + char hex[64]; + hex_string(result.data, result.length, hex); + ASSERT_STR_EQ(hex, "0911223344d19df27700"); + PASS(); +} + +/* CRC decode tests */ +TEST framer_decode_crc8(void) { + uint8_t buffer[BAKELITE_FRAMER_BUFFER_SIZE(256, 1)]; + Bakelite_CobsFramer framer; + bakelite_framer_init(&framer, buffer, sizeof(buffer), 256, BAKELITE_CRC_8); + + Bakelite_DecodeResult result = write_frame(&framer, (const uint8_t *)"\x06\x11\x22\x33\x44\xf9\x00", 7, BAKELITE_DECODE_OK); + ASSERT_EQ(result.status, BAKELITE_DECODE_OK); + PASS(); +} + +TEST framer_decode_crc8_failure(void) { + uint8_t buffer[BAKELITE_FRAMER_BUFFER_SIZE(256, 1)]; + Bakelite_CobsFramer framer; + bakelite_framer_init(&framer, buffer, sizeof(buffer), 256, BAKELITE_CRC_8); + + Bakelite_DecodeResult result = write_frame(&framer, (const uint8_t *)"\x06\xFF\x22\x33\x44\xf9\x00", 7, BAKELITE_DECODE_CRC_FAILURE); + ASSERT_EQ(result.status, BAKELITE_DECODE_CRC_FAILURE); + PASS(); +} + +TEST framer_decode_crc16(void) { + uint8_t buffer[BAKELITE_FRAMER_BUFFER_SIZE(256, 2)]; + Bakelite_CobsFramer framer; + bakelite_framer_init(&framer, buffer, sizeof(buffer), 256, BAKELITE_CRC_16); + + Bakelite_DecodeResult result = write_frame(&framer, (const uint8_t *)"\x07\x11\x22\x33\x44\xb1\xf5\x00", 8, BAKELITE_DECODE_OK); + ASSERT_EQ(result.status, BAKELITE_DECODE_OK); + PASS(); +} + +TEST framer_decode_crc16_failure(void) { + uint8_t buffer[BAKELITE_FRAMER_BUFFER_SIZE(256, 2)]; + Bakelite_CobsFramer framer; + bakelite_framer_init(&framer, buffer, sizeof(buffer), 256, BAKELITE_CRC_16); + + Bakelite_DecodeResult result = write_frame(&framer, (const uint8_t *)"\x07\xFF\x22\x33\x44\xb1\xf5\x00", 8, BAKELITE_DECODE_CRC_FAILURE); + ASSERT_EQ(result.status, BAKELITE_DECODE_CRC_FAILURE); + PASS(); +} + +TEST framer_decode_crc32(void) { + uint8_t buffer[BAKELITE_FRAMER_BUFFER_SIZE(256, 4)]; + Bakelite_CobsFramer framer; + bakelite_framer_init(&framer, buffer, sizeof(buffer), 256, BAKELITE_CRC_32); + + Bakelite_DecodeResult result = write_frame(&framer, (const uint8_t *)"\x09\x11\x22\x33\x44\xd1\x9d\xf2\x77\x00", 10, BAKELITE_DECODE_OK); + ASSERT_EQ(result.status, BAKELITE_DECODE_OK); + PASS(); +} + +TEST framer_decode_crc32_failure(void) { + uint8_t buffer[BAKELITE_FRAMER_BUFFER_SIZE(256, 4)]; + Bakelite_CobsFramer framer; + bakelite_framer_init(&framer, buffer, sizeof(buffer), 256, BAKELITE_CRC_32); + + Bakelite_DecodeResult result = write_frame(&framer, (const uint8_t *)"\x09\xFF\x22\x33\x44\xd1\x9d\xf2\x77\x00", 10, BAKELITE_DECODE_CRC_FAILURE); + ASSERT_EQ(result.status, BAKELITE_DECODE_CRC_FAILURE); + PASS(); +} + +SUITE(framing) { + RUN_TEST(encode_frame); + RUN_TEST(encode_frame_one_byte); + RUN_TEST(decode_frame); + RUN_TEST(framer_encode); + RUN_TEST(framer_encode_zero_length); + RUN_TEST(framer_decode); + RUN_TEST(framer_encode_one_byte); + RUN_TEST(framer_decode_zero_length); + RUN_TEST(framer_decode_more_bytes); + RUN_TEST(framer_decode_two_null_bytes); + RUN_TEST(framer_buffer_overrun); + RUN_TEST(framer_decode_failure); + RUN_TEST(framer_decode_failure_2); + RUN_TEST(framer_roundtrip); + RUN_TEST(framer_encode_crc8); + RUN_TEST(framer_encode_crc16); + RUN_TEST(framer_encode_crc32); + RUN_TEST(framer_decode_crc8); + RUN_TEST(framer_decode_crc8_failure); + RUN_TEST(framer_decode_crc16); + RUN_TEST(framer_decode_crc16_failure); + RUN_TEST(framer_decode_crc32); + RUN_TEST(framer_decode_crc32_failure); +} diff --git a/bakelite/tests/generator/ctiny-protocol.c b/bakelite/tests/generator/ctiny-protocol.c new file mode 100644 index 0000000..5504426 --- /dev/null +++ b/bakelite/tests/generator/ctiny-protocol.c @@ -0,0 +1,247 @@ +#include +#include +#include "proto-ctiny.h" +#include "greatest.h" + +/* Declare suites from other files */ +SUITE_EXTERN(serialization); +SUITE_EXTERN(framing); + +/* Helper to convert buffer to hex string */ +static void hex_string(const uint8_t *data, size_t length, char *out) { + static const char hex[] = "0123456789abcdef"; + for (size_t i = 0; i < length; i++) { + out[i * 2] = hex[(data[i] >> 4) & 0x0f]; + out[i * 2 + 1] = hex[data[i] & 0x0f]; + } + out[length * 2] = '\0'; +} + +/* Test stream for simulating I/O */ +typedef struct { + uint8_t *buffer; + size_t size; + size_t pos; + bool blocking; +} TestStream; + +static TestStream test_stream; + +static int stream_read(void) { + if (test_stream.blocking) { + return -1; + } + if (test_stream.pos >= test_stream.size) { + return -2; + } + return test_stream.buffer[test_stream.pos++]; +} + +static size_t stream_write(const uint8_t *data, size_t length) { + size_t end_pos = test_stream.pos + length; + if (end_pos > test_stream.size) { + return 0; + } + memcpy(test_stream.buffer + test_stream.pos, data, length); + test_stream.pos += length; + return length; +} + +static void stream_init(uint8_t *buffer, size_t size) { + test_stream.buffer = buffer; + test_stream.size = size; + test_stream.pos = 0; + test_stream.blocking = false; +} + +static void stream_reset(void) { + memset(test_stream.buffer, 0, test_stream.size); + test_stream.pos = 0; + test_stream.blocking = false; +} + +TEST proto_send_message(void) { + uint8_t data[256]; + stream_init(data, 256); + + Protocol protocol; + Protocol_init(&protocol, stream_read, stream_write); + + Ack ack = { .code = 0x22 }; + Protocol_send_Ack(&protocol, &ack); + + ASSERT_EQ(test_stream.pos, 5); + char hex[64]; + hex_string(data, test_stream.pos, hex); + ASSERT_STR_EQ(hex, "040222c400"); + + size_t length = test_stream.pos; + test_stream.pos = 0; + + for (; test_stream.pos < length - 1;) { + ASSERT_EQ(Protocol_poll(&protocol), Protocol_NoMessage); + } + Protocol_Message msg = Protocol_poll(&protocol); + ASSERT_EQ(msg, Protocol_Ack); + + Ack result; + ASSERT_EQ(Protocol_decode_Ack(&protocol, &result, NULL, 0), 0); + ASSERT_EQ(result.code, 0x22); + PASS(); +} + +TEST proto_send_larger_message(void) { + uint8_t data[256]; + stream_init(data, 256); + + Protocol protocol; + Protocol_init(&protocol, stream_read, stream_write); + + TestMessage input = { + .a = 0x22, + .b = -1234, + .status = false, + .message = "Hello World!" + }; + Protocol_send_TestMessage(&protocol, &input); + + ASSERT_EQ(test_stream.pos, 26); + char hex[64]; + hex_string(data, test_stream.pos, hex); + ASSERT_STR_EQ(hex, "0701222efbffff0d48656c6c6f20576f726c6421010101026200"); + + size_t length = test_stream.pos; + test_stream.pos = 0; + + for (; test_stream.pos < length - 1;) { + ASSERT_EQ(Protocol_poll(&protocol), Protocol_NoMessage); + } + Protocol_Message msg = Protocol_poll(&protocol); + ASSERT_EQ(msg, Protocol_TestMessage); + + TestMessage result; + ASSERT_EQ(Protocol_decode_TestMessage(&protocol, &result, NULL, 0), 0); + ASSERT_EQ(result.a, 0x22); + ASSERT_EQ(result.b, -1234); + ASSERT_EQ(result.status, false); + ASSERT_STR_EQ(result.message, "Hello World!"); + PASS(); +} + +TEST proto_decode_wrong_message(void) { + uint8_t data[256]; + stream_init(data, 256); + + Protocol protocol; + Protocol_init(&protocol, stream_read, stream_write); + + Ack ack = { .code = 0x22 }; + Protocol_send_Ack(&protocol, &ack); + + ASSERT_EQ(test_stream.pos, 5); + char hex[64]; + hex_string(data, test_stream.pos, hex); + ASSERT_STR_EQ(hex, "040222c400"); + + size_t length = test_stream.pos; + test_stream.pos = 0; + + for (; test_stream.pos < length - 1;) { + ASSERT_EQ(Protocol_poll(&protocol), Protocol_NoMessage); + } + Protocol_Message msg = Protocol_poll(&protocol); + ASSERT_EQ(msg, Protocol_Ack); + + /* Try to decode as wrong message type */ + TestMessage result; + ASSERT_EQ(Protocol_decode_TestMessage(&protocol, &result, NULL, 0), -1); + PASS(); +} + +TEST proto_receive_no_dynamic_memory(void) { + uint8_t data[256]; + stream_init(data, 256); + + Protocol protocol; + Protocol_init(&protocol, stream_read, stream_write); + + ArrayMessage msg; + int32_t numbers[3] = {1234, -1234, 456}; + msg.numbers.data = numbers; + msg.numbers.size = 3; + Protocol_send_ArrayMessage(&protocol, &msg); + + ASSERT_EQ(test_stream.pos, 17); + char hex[64]; + hex_string(data, test_stream.pos, hex); + ASSERT_STR_EQ(hex, "050303d20401072efbffffc8010102bb00"); + + size_t length = test_stream.pos; + test_stream.pos = 0; + + for (; test_stream.pos < length - 1;) { + ASSERT_EQ(Protocol_poll(&protocol), Protocol_NoMessage); + } + Protocol_Message msgId = Protocol_poll(&protocol); + ASSERT_EQ(msgId, Protocol_ArrayMessage); + + /* Decode without providing heap - should fail */ + ArrayMessage result; + ASSERT_EQ(Protocol_decode_ArrayMessage(&protocol, &result, NULL, 0), -4); + PASS(); +} + +TEST proto_receive_dynamic(void) { + uint8_t data[256]; + stream_init(data, 256); + + Protocol protocol; + Protocol_init(&protocol, stream_read, stream_write); + + ArrayMessage msg; + int32_t numbers[3] = {1234, -1234, 456}; + msg.numbers.data = numbers; + msg.numbers.size = 3; + Protocol_send_ArrayMessage(&protocol, &msg); + + ASSERT_EQ(test_stream.pos, 17); + char hex[64]; + hex_string(data, test_stream.pos, hex); + ASSERT_STR_EQ(hex, "050303d20401072efbffffc8010102bb00"); + + size_t length = test_stream.pos; + test_stream.pos = 0; + + for (; test_stream.pos < length - 1;) { + ASSERT_EQ(Protocol_poll(&protocol), Protocol_NoMessage); + } + Protocol_Message msgId = Protocol_poll(&protocol); + ASSERT_EQ(msgId, Protocol_ArrayMessage); + + ArrayMessage result; + uint8_t heap[64]; + ASSERT_EQ(Protocol_decode_ArrayMessage(&protocol, &result, heap, sizeof(heap)), 0); + ASSERT_EQ(result.numbers.size, 3); + ASSERT_EQ(((int32_t *)result.numbers.data)[0], 1234); + ASSERT_EQ(((int32_t *)result.numbers.data)[1], -1234); + ASSERT_EQ(((int32_t *)result.numbers.data)[2], 456); + PASS(); +} + +SUITE(protocol) { + RUN_TEST(proto_send_message); + RUN_TEST(proto_send_larger_message); + RUN_TEST(proto_decode_wrong_message); + RUN_TEST(proto_receive_no_dynamic_memory); + RUN_TEST(proto_receive_dynamic); +} + +GREATEST_MAIN_DEFS(); + +int main(int argc, char **argv) { + GREATEST_MAIN_BEGIN(); + RUN_SUITE(serialization); + RUN_SUITE(framing); + RUN_SUITE(protocol); + GREATEST_MAIN_END(); +} diff --git a/bakelite/tests/generator/ctiny-serialization.c b/bakelite/tests/generator/ctiny-serialization.c new file mode 100644 index 0000000..b6fc242 --- /dev/null +++ b/bakelite/tests/generator/ctiny-serialization.c @@ -0,0 +1,237 @@ +#include +#include +#include "struct-ctiny.h" + +#define GREATEST_ENABLE_FLOAT +#include "greatest.h" + +/* Helper to convert buffer to hex string */ +static void hex_string(const uint8_t *data, size_t length, char *out) { + static const char hex[] = "0123456789abcdef"; + for (size_t i = 0; i < length; i++) { + out[i * 2] = hex[(data[i] >> 4) & 0x0f]; + out[i * 2 + 1] = hex[data[i] & 0x0f]; + } + out[length * 2] = '\0'; +} + +TEST simple_struct(void) { + uint8_t data[256]; + uint8_t heap[256]; + Bakelite_Buffer buf; + bakelite_buffer_init_with_heap(&buf, data, 256, heap, 256); + + Ack t1 = { .code = 123 }; + ASSERT_EQ(Ack_pack(&t1, &buf), 0); + + ASSERT_EQ(bakelite_buffer_pos(&buf), 1); + char hex[512]; + hex_string(data, bakelite_buffer_pos(&buf), hex); + ASSERT_STR_EQ(hex, "7b"); + + Ack t2; + bakelite_buffer_seek(&buf, 0); + ASSERT_EQ(Ack_unpack(&t2, &buf), 0); + + ASSERT_EQ(t2.code, 123); + PASS(); +} + +TEST complex_struct(void) { + uint8_t data[256]; + uint8_t heap[256]; + Bakelite_Buffer buf; + bakelite_buffer_init_with_heap(&buf, data, 256, heap, 256); + + TestStruct t1 = { + .int1 = 5, + .int2 = -1234, + .uint1 = 31, + .uint2 = 1234, + .float1 = -1.23f, + .b1 = true, + .b2 = true, + .b3 = false, + .data = {1, 2, 3, 4}, + .str = "hey" + }; + ASSERT_EQ(TestStruct_pack(&t1, &buf), 0); + + ASSERT_EQ(bakelite_buffer_pos(&buf), 24); + char hex[512]; + hex_string(data, bakelite_buffer_pos(&buf), hex); + ASSERT_STR_EQ(hex, "052efbffff1fd204a4709dbf010100010203046865790000"); + + TestStruct t2; + bakelite_buffer_seek(&buf, 0); + ASSERT_EQ(TestStruct_unpack(&t2, &buf), 0); + + ASSERT_EQ(t2.int1, 5); + ASSERT_EQ(t2.int2, -1234); + ASSERT_EQ(t2.uint1, 31); + ASSERT_EQ(t2.uint2, 1234); + ASSERT_IN_RANGE(-1.23f, t2.float1, 0.001f); + ASSERT_EQ(t2.b1, true); + ASSERT_EQ(t2.b2, true); + ASSERT_EQ(t2.b3, false); + ASSERT_STR_EQ(t2.str, "hey"); + PASS(); +} + +TEST enum_struct(void) { + uint8_t data[256]; + uint8_t heap[256]; + Bakelite_Buffer buf; + bakelite_buffer_init_with_heap(&buf, data, 256, heap, 256); + + EnumStruct t1 = { + .direction = Direction_Left, + .speed = Speed_Fast + }; + ASSERT_EQ(EnumStruct_pack(&t1, &buf), 0); + + ASSERT_EQ(bakelite_buffer_pos(&buf), 2); + char hex[512]; + hex_string(data, bakelite_buffer_pos(&buf), hex); + ASSERT_STR_EQ(hex, "02ff"); + + EnumStruct t2; + bakelite_buffer_seek(&buf, 0); + ASSERT_EQ(EnumStruct_unpack(&t2, &buf), 0); + + ASSERT_EQ(t2.direction, Direction_Left); + ASSERT_EQ(t2.speed, Speed_Fast); + PASS(); +} + +TEST nested_struct(void) { + uint8_t data[256]; + uint8_t heap[256]; + Bakelite_Buffer buf; + bakelite_buffer_init_with_heap(&buf, data, 256, heap, 256); + + NestedStruct t1 = { + .a = { .b1 = true, .b2 = false }, + .b = { .num = 127 }, + .num = -4 + }; + ASSERT_EQ(NestedStruct_pack(&t1, &buf), 0); + + ASSERT_EQ(bakelite_buffer_pos(&buf), 4); + char hex[512]; + hex_string(data, bakelite_buffer_pos(&buf), hex); + ASSERT_STR_EQ(hex, "01007ffc"); + + NestedStruct t2; + bakelite_buffer_seek(&buf, 0); + ASSERT_EQ(NestedStruct_unpack(&t2, &buf), 0); + + ASSERT_EQ(t2.a.b1, true); + ASSERT_EQ(t2.a.b2, false); + ASSERT_EQ(t2.b.num, 127); + ASSERT_EQ(t2.num, -4); + PASS(); +} + +TEST deeply_nested_struct(void) { + uint8_t data[256]; + uint8_t heap[256]; + Bakelite_Buffer buf; + bakelite_buffer_init_with_heap(&buf, data, 256, heap, 256); + + DeeplyNestedStruct t1 = { + .c = { .a = { .b1 = false, .b2 = true } } + }; + ASSERT_EQ(DeeplyNestedStruct_pack(&t1, &buf), 0); + + ASSERT_EQ(bakelite_buffer_pos(&buf), 2); + char hex[512]; + hex_string(data, bakelite_buffer_pos(&buf), hex); + ASSERT_STR_EQ(hex, "0001"); + + DeeplyNestedStruct t2; + bakelite_buffer_seek(&buf, 0); + ASSERT_EQ(DeeplyNestedStruct_unpack(&t2, &buf), 0); + + ASSERT_EQ(t2.c.a.b1, false); + ASSERT_EQ(t2.c.a.b2, true); + PASS(); +} + +TEST array_struct(void) { + uint8_t data[256]; + uint8_t heap[256]; + Bakelite_Buffer buf; + bakelite_buffer_init_with_heap(&buf, data, 256, heap, 256); + + ArrayStruct t1 = { + .a = { Direction_Left, Direction_Right, Direction_Down }, + .b = { { .code = 127 }, { .code = 64 } }, + .c = { "abc", "def", "ghi" } + }; + ASSERT_EQ(ArrayStruct_pack(&t1, &buf), 0); + + ASSERT_EQ(bakelite_buffer_pos(&buf), 17); + char hex[512]; + hex_string(data, bakelite_buffer_pos(&buf), hex); + ASSERT_STR_EQ(hex, "0203017f40616263006465660067686900"); + + ArrayStruct t2; + bakelite_buffer_seek(&buf, 0); + ASSERT_EQ(ArrayStruct_unpack(&t2, &buf), 0); + + ASSERT_EQ(t2.a[0], Direction_Left); + ASSERT_EQ(t2.a[1], Direction_Right); + ASSERT_EQ(t2.a[2], Direction_Down); + ASSERT_EQ(t2.b[0].code, 127); + ASSERT_EQ(t2.b[1].code, 64); + ASSERT_STR_EQ(t2.c[0], "abc"); + ASSERT_STR_EQ(t2.c[1], "def"); + ASSERT_STR_EQ(t2.c[2], "ghi"); + PASS(); +} + +TEST variable_length_struct(void) { + uint8_t data[256]; + uint8_t heap[256]; + Bakelite_Buffer buf; + bakelite_buffer_init_with_heap(&buf, data, 256, heap, 256); + + uint8_t byteData[11] = { 0x68, 0x65, 0x6C, 0x6C, 0x6F, + 0, + 0x57, 0x6F, 0x72, 0x6C, 0x64 }; + uint8_t numbers[4] = { 1, 2, 3, 4 }; + + VariableLength t1; + t1.a.data = byteData; + t1.a.size = 11; + t1.b = "This is a test string!"; + t1.c.data = numbers; + t1.c.size = 4; + + ASSERT_EQ(VariableLength_pack(&t1, &buf), 0); + + /* Verify data was packed correctly */ + ASSERT(bakelite_buffer_pos(&buf) > 0); + + VariableLength t2; + bakelite_buffer_seek(&buf, 0); + ASSERT_EQ(VariableLength_unpack(&t2, &buf), 0); + + ASSERT_EQ(t2.a.size, 11); + ASSERT_MEM_EQ(t2.a.data, byteData, 11); + ASSERT_STR_EQ(t2.b, "This is a test string!"); + ASSERT_EQ(t2.c.size, 4); + ASSERT_MEM_EQ(t2.c.data, numbers, 4); + PASS(); +} + +SUITE(serialization) { + RUN_TEST(simple_struct); + RUN_TEST(complex_struct); + RUN_TEST(enum_struct); + RUN_TEST(nested_struct); + RUN_TEST(deeply_nested_struct); + RUN_TEST(array_struct); + RUN_TEST(variable_length_struct); +} diff --git a/bakelite/tests/generator/greatest.h b/bakelite/tests/generator/greatest.h new file mode 100644 index 0000000..af0c053 --- /dev/null +++ b/bakelite/tests/generator/greatest.h @@ -0,0 +1,1266 @@ +/* + * Copyright (c) 2011-2021 Scott Vokes + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef GREATEST_H +#define GREATEST_H + +#if defined(__cplusplus) && !defined(GREATEST_NO_EXTERN_CPLUSPLUS) +extern "C" { +#endif + +/* 1.5.0 */ +#define GREATEST_VERSION_MAJOR 1 +#define GREATEST_VERSION_MINOR 5 +#define GREATEST_VERSION_PATCH 0 + +/* A unit testing system for C, contained in 1 file. + * It doesn't use dynamic allocation or depend on anything + * beyond ANSI C89. + * + * An up-to-date version can be found at: + * https://github.com/silentbicycle/greatest/ + */ + + +/********************************************************************* + * Minimal test runner template + *********************************************************************/ +#if 0 + +#include "greatest.h" + +TEST foo_should_foo(void) { + PASS(); +} + +static void setup_cb(void *data) { + printf("setup callback for each test case\n"); +} + +static void teardown_cb(void *data) { + printf("teardown callback for each test case\n"); +} + +SUITE(suite) { + /* Optional setup/teardown callbacks which will be run before/after + * every test case. If using a test suite, they will be cleared when + * the suite finishes. */ + SET_SETUP(setup_cb, voidp_to_callback_data); + SET_TEARDOWN(teardown_cb, voidp_to_callback_data); + + RUN_TEST(foo_should_foo); +} + +/* Add definitions that need to be in the test runner's main file. */ +GREATEST_MAIN_DEFS(); + +/* Set up, run suite(s) of tests, report pass/fail/skip stats. */ +int run_tests(void) { + GREATEST_INIT(); /* init. greatest internals */ + /* List of suites to run (if any). */ + RUN_SUITE(suite); + + /* Tests can also be run directly, without using test suites. */ + RUN_TEST(foo_should_foo); + + GREATEST_PRINT_REPORT(); /* display results */ + return greatest_all_passed(); +} + +/* main(), for a standalone command-line test runner. + * This replaces run_tests above, and adds command line option + * handling and exiting with a pass/fail status. */ +int main(int argc, char **argv) { + GREATEST_MAIN_BEGIN(); /* init & parse command-line args */ + RUN_SUITE(suite); + GREATEST_MAIN_END(); /* display results */ +} + +#endif +/*********************************************************************/ + + +#include +#include +#include +#include + +/*********** + * Options * + ***********/ + +/* Default column width for non-verbose output. */ +#ifndef GREATEST_DEFAULT_WIDTH +#define GREATEST_DEFAULT_WIDTH 72 +#endif + +/* FILE *, for test logging. */ +#ifndef GREATEST_STDOUT +#define GREATEST_STDOUT stdout +#endif + +/* Remove GREATEST_ prefix from most commonly used symbols? */ +#ifndef GREATEST_USE_ABBREVS +#define GREATEST_USE_ABBREVS 1 +#endif + +/* Set to 0 to disable all use of setjmp/longjmp. */ +#ifndef GREATEST_USE_LONGJMP +#define GREATEST_USE_LONGJMP 0 +#endif + +/* Make it possible to replace fprintf with another + * function with the same interface. */ +#ifndef GREATEST_FPRINTF +#define GREATEST_FPRINTF fprintf +#endif + +#if GREATEST_USE_LONGJMP +#include +#endif + +/* Set to 0 to disable all use of time.h / clock(). */ +#ifndef GREATEST_USE_TIME +#define GREATEST_USE_TIME 1 +#endif + +#if GREATEST_USE_TIME +#include +#endif + +/* Floating point type, for ASSERT_IN_RANGE. */ +#ifndef GREATEST_FLOAT +#define GREATEST_FLOAT double +#define GREATEST_FLOAT_FMT "%g" +#endif + +/* Size of buffer for test name + optional '_' separator and suffix */ +#ifndef GREATEST_TESTNAME_BUF_SIZE +#define GREATEST_TESTNAME_BUF_SIZE 128 +#endif + + +/********* + * Types * + *********/ + +/* Info for the current running suite. */ +typedef struct greatest_suite_info { + unsigned int tests_run; + unsigned int passed; + unsigned int failed; + unsigned int skipped; + +#if GREATEST_USE_TIME + /* timers, pre/post running suite and individual tests */ + clock_t pre_suite; + clock_t post_suite; + clock_t pre_test; + clock_t post_test; +#endif +} greatest_suite_info; + +/* Type for a suite function. */ +typedef void greatest_suite_cb(void); + +/* Types for setup/teardown callbacks. If non-NULL, these will be run + * and passed the pointer to their additional data. */ +typedef void greatest_setup_cb(void *udata); +typedef void greatest_teardown_cb(void *udata); + +/* Type for an equality comparison between two pointers of the same type. + * Should return non-0 if equal, otherwise 0. + * UDATA is a closure value, passed through from ASSERT_EQUAL_T[m]. */ +typedef int greatest_equal_cb(const void *expd, const void *got, void *udata); + +/* Type for a callback that prints a value pointed to by T. + * Return value has the same meaning as printf's. + * UDATA is a closure value, passed through from ASSERT_EQUAL_T[m]. */ +typedef int greatest_printf_cb(const void *t, void *udata); + +/* Callbacks for an arbitrary type; needed for type-specific + * comparisons via GREATEST_ASSERT_EQUAL_T[m].*/ +typedef struct greatest_type_info { + greatest_equal_cb *equal; + greatest_printf_cb *print; +} greatest_type_info; + +typedef struct greatest_memory_cmp_env { + const unsigned char *exp; + const unsigned char *got; + size_t size; +} greatest_memory_cmp_env; + +/* Callbacks for string and raw memory types. */ +extern greatest_type_info greatest_type_info_string; +extern greatest_type_info greatest_type_info_memory; + +typedef enum { + GREATEST_FLAG_FIRST_FAIL = 0x01, + GREATEST_FLAG_LIST_ONLY = 0x02, + GREATEST_FLAG_ABORT_ON_FAIL = 0x04 +} greatest_flag_t; + +/* Internal state for a PRNG, used to shuffle test order. */ +struct greatest_prng { + unsigned char random_order; /* use random ordering? */ + unsigned char initialized; /* is random ordering initialized? */ + unsigned char pad_0[6]; + unsigned long state; /* PRNG state */ + unsigned long count; /* how many tests, this pass */ + unsigned long count_ceil; /* total number of tests */ + unsigned long count_run; /* total tests run */ + unsigned long a; /* LCG multiplier */ + unsigned long c; /* LCG increment */ + unsigned long m; /* LCG modulus, based on count_ceil */ +}; + +/* Struct containing all test runner state. */ +typedef struct greatest_run_info { + unsigned char flags; + unsigned char verbosity; + unsigned char running_test; /* guard for nested RUN_TEST calls */ + unsigned char exact_name_match; + + unsigned int tests_run; /* total test count */ + + /* currently running test suite */ + greatest_suite_info suite; + + /* overall pass/fail/skip counts */ + unsigned int passed; + unsigned int failed; + unsigned int skipped; + unsigned int assertions; + + /* info to print about the most recent failure */ + unsigned int fail_line; + unsigned int pad_1; + const char *fail_file; + const char *msg; + + /* current setup/teardown hooks and userdata */ + greatest_setup_cb *setup; + void *setup_udata; + greatest_teardown_cb *teardown; + void *teardown_udata; + + /* formatting info for ".....s...F"-style output */ + unsigned int col; + unsigned int width; + + /* only run a specific suite or test */ + const char *suite_filter; + const char *test_filter; + const char *test_exclude; + const char *name_suffix; /* print suffix with test name */ + char name_buf[GREATEST_TESTNAME_BUF_SIZE]; + + struct greatest_prng prng[2]; /* 0: suites, 1: tests */ + +#if GREATEST_USE_TIME + /* overall timers */ + clock_t begin; + clock_t end; +#endif + +#if GREATEST_USE_LONGJMP + int pad_jmp_buf; + unsigned char pad_2[4]; + jmp_buf jump_dest; +#endif +} greatest_run_info; + +struct greatest_report_t { + /* overall pass/fail/skip counts */ + unsigned int passed; + unsigned int failed; + unsigned int skipped; + unsigned int assertions; +}; + +/* Global var for the current testing context. + * Initialized by GREATEST_MAIN_DEFS(). */ +extern greatest_run_info greatest_info; + +/* Type for ASSERT_ENUM_EQ's ENUM_STR argument. */ +typedef const char *greatest_enum_str_fun(int value); + + +/********************** + * Exported functions * + **********************/ + +/* These are used internally by greatest macros. */ +int greatest_test_pre(const char *name); +void greatest_test_post(int res); +int greatest_do_assert_equal_t(const void *expd, const void *got, + greatest_type_info *type_info, void *udata); +void greatest_prng_init_first_pass(int id); +int greatest_prng_init_second_pass(int id, unsigned long seed); +void greatest_prng_step(int id); + +/* These are part of the public greatest API. */ +void GREATEST_SET_SETUP_CB(greatest_setup_cb *cb, void *udata); +void GREATEST_SET_TEARDOWN_CB(greatest_teardown_cb *cb, void *udata); +void GREATEST_INIT(void); +void GREATEST_PRINT_REPORT(void); +int greatest_all_passed(void); +void greatest_set_suite_filter(const char *filter); +void greatest_set_test_filter(const char *filter); +void greatest_set_test_exclude(const char *filter); +void greatest_set_exact_name_match(void); +void greatest_stop_at_first_fail(void); +void greatest_abort_on_fail(void); +void greatest_list_only(void); +void greatest_get_report(struct greatest_report_t *report); +unsigned int greatest_get_verbosity(void); +void greatest_set_verbosity(unsigned int verbosity); +void greatest_set_flag(greatest_flag_t flag); +void greatest_set_test_suffix(const char *suffix); + + +/******************** +* Language Support * +********************/ + +/* If __VA_ARGS__ (C99) is supported, allow parametric testing +* without needing to manually manage the argument struct. */ +#if (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 19901L) || \ + (defined(_MSC_VER) && _MSC_VER >= 1800) +#define GREATEST_VA_ARGS +#endif + + +/********** + * Macros * + **********/ + +/* Define a suite. (The duplication is intentional -- it eliminates + * a warning from -Wmissing-declarations.) */ +#define GREATEST_SUITE(NAME) void NAME(void); void NAME(void) + +/* Declare a suite, provided by another compilation unit. */ +#define GREATEST_SUITE_EXTERN(NAME) void NAME(void) + +/* Start defining a test function. + * The arguments are not included, to allow parametric testing. */ +#define GREATEST_TEST static enum greatest_test_res + +/* PASS/FAIL/SKIP result from a test. Used internally. */ +typedef enum greatest_test_res { + GREATEST_TEST_RES_PASS = 0, + GREATEST_TEST_RES_FAIL = -1, + GREATEST_TEST_RES_SKIP = 1 +} greatest_test_res; + +/* Run a suite. */ +#define GREATEST_RUN_SUITE(S_NAME) greatest_run_suite(S_NAME, #S_NAME) + +/* Run a test in the current suite. */ +#define GREATEST_RUN_TEST(TEST) \ + do { \ + if (greatest_test_pre(#TEST) == 1) { \ + enum greatest_test_res res = GREATEST_SAVE_CONTEXT(); \ + if (res == GREATEST_TEST_RES_PASS) { \ + res = TEST(); \ + } \ + greatest_test_post(res); \ + } \ + } while (0) + +/* Ignore a test, don't warn about it being unused. */ +#define GREATEST_IGNORE_TEST(TEST) (void)TEST + +/* Run a test in the current suite with one void * argument, + * which can be a pointer to a struct with multiple arguments. */ +#define GREATEST_RUN_TEST1(TEST, ENV) \ + do { \ + if (greatest_test_pre(#TEST) == 1) { \ + enum greatest_test_res res = GREATEST_SAVE_CONTEXT(); \ + if (res == GREATEST_TEST_RES_PASS) { \ + res = TEST(ENV); \ + } \ + greatest_test_post(res); \ + } \ + } while (0) + +#ifdef GREATEST_VA_ARGS +#define GREATEST_RUN_TESTp(TEST, ...) \ + do { \ + if (greatest_test_pre(#TEST) == 1) { \ + enum greatest_test_res res = GREATEST_SAVE_CONTEXT(); \ + if (res == GREATEST_TEST_RES_PASS) { \ + res = TEST(__VA_ARGS__); \ + } \ + greatest_test_post(res); \ + } \ + } while (0) +#endif + + +/* Check if the test runner is in verbose mode. */ +#define GREATEST_IS_VERBOSE() ((greatest_info.verbosity) > 0) +#define GREATEST_LIST_ONLY() \ + (greatest_info.flags & GREATEST_FLAG_LIST_ONLY) +#define GREATEST_FIRST_FAIL() \ + (greatest_info.flags & GREATEST_FLAG_FIRST_FAIL) +#define GREATEST_ABORT_ON_FAIL() \ + (greatest_info.flags & GREATEST_FLAG_ABORT_ON_FAIL) +#define GREATEST_FAILURE_ABORT() \ + (GREATEST_FIRST_FAIL() && \ + (greatest_info.suite.failed > 0 || greatest_info.failed > 0)) + +/* Message-less forms of tests defined below. */ +#define GREATEST_PASS() GREATEST_PASSm(NULL) +#define GREATEST_FAIL() GREATEST_FAILm(NULL) +#define GREATEST_SKIP() GREATEST_SKIPm(NULL) +#define GREATEST_ASSERT(COND) \ + GREATEST_ASSERTm(#COND, COND) +#define GREATEST_ASSERT_OR_LONGJMP(COND) \ + GREATEST_ASSERT_OR_LONGJMPm(#COND, COND) +#define GREATEST_ASSERT_FALSE(COND) \ + GREATEST_ASSERT_FALSEm(#COND, COND) +#define GREATEST_ASSERT_EQ(EXP, GOT) \ + GREATEST_ASSERT_EQm(#EXP " != " #GOT, EXP, GOT) +#define GREATEST_ASSERT_NEQ(EXP, GOT) \ + GREATEST_ASSERT_NEQm(#EXP " == " #GOT, EXP, GOT) +#define GREATEST_ASSERT_GT(EXP, GOT) \ + GREATEST_ASSERT_GTm(#EXP " <= " #GOT, EXP, GOT) +#define GREATEST_ASSERT_GTE(EXP, GOT) \ + GREATEST_ASSERT_GTEm(#EXP " < " #GOT, EXP, GOT) +#define GREATEST_ASSERT_LT(EXP, GOT) \ + GREATEST_ASSERT_LTm(#EXP " >= " #GOT, EXP, GOT) +#define GREATEST_ASSERT_LTE(EXP, GOT) \ + GREATEST_ASSERT_LTEm(#EXP " > " #GOT, EXP, GOT) +#define GREATEST_ASSERT_EQ_FMT(EXP, GOT, FMT) \ + GREATEST_ASSERT_EQ_FMTm(#EXP " != " #GOT, EXP, GOT, FMT) +#define GREATEST_ASSERT_IN_RANGE(EXP, GOT, TOL) \ + GREATEST_ASSERT_IN_RANGEm(#EXP " != " #GOT " +/- " #TOL, EXP, GOT, TOL) +#define GREATEST_ASSERT_EQUAL_T(EXP, GOT, TYPE_INFO, UDATA) \ + GREATEST_ASSERT_EQUAL_Tm(#EXP " != " #GOT, EXP, GOT, TYPE_INFO, UDATA) +#define GREATEST_ASSERT_STR_EQ(EXP, GOT) \ + GREATEST_ASSERT_STR_EQm(#EXP " != " #GOT, EXP, GOT) +#define GREATEST_ASSERT_STRN_EQ(EXP, GOT, SIZE) \ + GREATEST_ASSERT_STRN_EQm(#EXP " != " #GOT, EXP, GOT, SIZE) +#define GREATEST_ASSERT_MEM_EQ(EXP, GOT, SIZE) \ + GREATEST_ASSERT_MEM_EQm(#EXP " != " #GOT, EXP, GOT, SIZE) +#define GREATEST_ASSERT_ENUM_EQ(EXP, GOT, ENUM_STR) \ + GREATEST_ASSERT_ENUM_EQm(#EXP " != " #GOT, EXP, GOT, ENUM_STR) + +/* The following forms take an additional message argument first, + * to be displayed by the test runner. */ + +/* Fail if a condition is not true, with message. */ +#define GREATEST_ASSERTm(MSG, COND) \ + do { \ + greatest_info.assertions++; \ + if (!(COND)) { GREATEST_FAILm(MSG); } \ + } while (0) + +/* Fail if a condition is not true, longjmping out of test. */ +#define GREATEST_ASSERT_OR_LONGJMPm(MSG, COND) \ + do { \ + greatest_info.assertions++; \ + if (!(COND)) { GREATEST_FAIL_WITH_LONGJMPm(MSG); } \ + } while (0) + +/* Fail if a condition is not false, with message. */ +#define GREATEST_ASSERT_FALSEm(MSG, COND) \ + do { \ + greatest_info.assertions++; \ + if ((COND)) { GREATEST_FAILm(MSG); } \ + } while (0) + +/* Internal macro for relational assertions */ +#define GREATEST__REL(REL, MSG, EXP, GOT) \ + do { \ + greatest_info.assertions++; \ + if (!((EXP) REL (GOT))) { GREATEST_FAILm(MSG); } \ + } while (0) + +/* Fail if EXP is not ==, !=, >, <, >=, or <= to GOT. */ +#define GREATEST_ASSERT_EQm(MSG,E,G) GREATEST__REL(==, MSG,E,G) +#define GREATEST_ASSERT_NEQm(MSG,E,G) GREATEST__REL(!=, MSG,E,G) +#define GREATEST_ASSERT_GTm(MSG,E,G) GREATEST__REL(>, MSG,E,G) +#define GREATEST_ASSERT_GTEm(MSG,E,G) GREATEST__REL(>=, MSG,E,G) +#define GREATEST_ASSERT_LTm(MSG,E,G) GREATEST__REL(<, MSG,E,G) +#define GREATEST_ASSERT_LTEm(MSG,E,G) GREATEST__REL(<=, MSG,E,G) + +/* Fail if EXP != GOT (equality comparison by ==). + * Warning: FMT, EXP, and GOT will be evaluated more + * than once on failure. */ +#define GREATEST_ASSERT_EQ_FMTm(MSG, EXP, GOT, FMT) \ + do { \ + greatest_info.assertions++; \ + if ((EXP) != (GOT)) { \ + GREATEST_FPRINTF(GREATEST_STDOUT, "\nExpected: "); \ + GREATEST_FPRINTF(GREATEST_STDOUT, FMT, EXP); \ + GREATEST_FPRINTF(GREATEST_STDOUT, "\n Got: "); \ + GREATEST_FPRINTF(GREATEST_STDOUT, FMT, GOT); \ + GREATEST_FPRINTF(GREATEST_STDOUT, "\n"); \ + GREATEST_FAILm(MSG); \ + } \ + } while (0) + +/* Fail if EXP is not equal to GOT, printing enum IDs. */ +#define GREATEST_ASSERT_ENUM_EQm(MSG, EXP, GOT, ENUM_STR) \ + do { \ + int greatest_EXP = (int)(EXP); \ + int greatest_GOT = (int)(GOT); \ + greatest_enum_str_fun *greatest_ENUM_STR = ENUM_STR; \ + if (greatest_EXP != greatest_GOT) { \ + GREATEST_FPRINTF(GREATEST_STDOUT, "\nExpected: %s", \ + greatest_ENUM_STR(greatest_EXP)); \ + GREATEST_FPRINTF(GREATEST_STDOUT, "\n Got: %s\n", \ + greatest_ENUM_STR(greatest_GOT)); \ + GREATEST_FAILm(MSG); \ + } \ + } while (0) \ + +/* Fail if GOT not in range of EXP +|- TOL. */ +#define GREATEST_ASSERT_IN_RANGEm(MSG, EXP, GOT, TOL) \ + do { \ + GREATEST_FLOAT greatest_EXP = (EXP); \ + GREATEST_FLOAT greatest_GOT = (GOT); \ + GREATEST_FLOAT greatest_TOL = (TOL); \ + greatest_info.assertions++; \ + if ((greatest_EXP > greatest_GOT && \ + greatest_EXP - greatest_GOT > greatest_TOL) || \ + (greatest_EXP < greatest_GOT && \ + greatest_GOT - greatest_EXP > greatest_TOL)) { \ + GREATEST_FPRINTF(GREATEST_STDOUT, \ + "\nExpected: " GREATEST_FLOAT_FMT \ + " +/- " GREATEST_FLOAT_FMT \ + "\n Got: " GREATEST_FLOAT_FMT \ + "\n", \ + greatest_EXP, greatest_TOL, greatest_GOT); \ + GREATEST_FAILm(MSG); \ + } \ + } while (0) + +/* Fail if EXP is not equal to GOT, according to strcmp. */ +#define GREATEST_ASSERT_STR_EQm(MSG, EXP, GOT) \ + do { \ + GREATEST_ASSERT_EQUAL_Tm(MSG, EXP, GOT, \ + &greatest_type_info_string, NULL); \ + } while (0) \ + +/* Fail if EXP is not equal to GOT, according to strncmp. */ +#define GREATEST_ASSERT_STRN_EQm(MSG, EXP, GOT, SIZE) \ + do { \ + size_t size = SIZE; \ + GREATEST_ASSERT_EQUAL_Tm(MSG, EXP, GOT, \ + &greatest_type_info_string, &size); \ + } while (0) \ + +/* Fail if EXP is not equal to GOT, according to memcmp. */ +#define GREATEST_ASSERT_MEM_EQm(MSG, EXP, GOT, SIZE) \ + do { \ + greatest_memory_cmp_env env; \ + env.exp = (const unsigned char *)EXP; \ + env.got = (const unsigned char *)GOT; \ + env.size = SIZE; \ + GREATEST_ASSERT_EQUAL_Tm(MSG, env.exp, env.got, \ + &greatest_type_info_memory, &env); \ + } while (0) \ + +/* Fail if EXP is not equal to GOT, according to a comparison + * callback in TYPE_INFO. If they are not equal, optionally use a + * print callback in TYPE_INFO to print them. */ +#define GREATEST_ASSERT_EQUAL_Tm(MSG, EXP, GOT, TYPE_INFO, UDATA) \ + do { \ + greatest_type_info *type_info = (TYPE_INFO); \ + greatest_info.assertions++; \ + if (!greatest_do_assert_equal_t(EXP, GOT, \ + type_info, UDATA)) { \ + if (type_info == NULL || type_info->equal == NULL) { \ + GREATEST_FAILm("type_info->equal callback missing!"); \ + } else { \ + GREATEST_FAILm(MSG); \ + } \ + } \ + } while (0) \ + +/* Pass. */ +#define GREATEST_PASSm(MSG) \ + do { \ + greatest_info.msg = MSG; \ + return GREATEST_TEST_RES_PASS; \ + } while (0) + +/* Fail. */ +#define GREATEST_FAILm(MSG) \ + do { \ + greatest_info.fail_file = __FILE__; \ + greatest_info.fail_line = __LINE__; \ + greatest_info.msg = MSG; \ + if (GREATEST_ABORT_ON_FAIL()) { abort(); } \ + return GREATEST_TEST_RES_FAIL; \ + } while (0) + +/* Optional GREATEST_FAILm variant that longjmps. */ +#if GREATEST_USE_LONGJMP +#define GREATEST_FAIL_WITH_LONGJMP() GREATEST_FAIL_WITH_LONGJMPm(NULL) +#define GREATEST_FAIL_WITH_LONGJMPm(MSG) \ + do { \ + greatest_info.fail_file = __FILE__; \ + greatest_info.fail_line = __LINE__; \ + greatest_info.msg = MSG; \ + longjmp(greatest_info.jump_dest, GREATEST_TEST_RES_FAIL); \ + } while (0) +#endif + +/* Skip the current test. */ +#define GREATEST_SKIPm(MSG) \ + do { \ + greatest_info.msg = MSG; \ + return GREATEST_TEST_RES_SKIP; \ + } while (0) + +/* Check the result of a subfunction using ASSERT, etc. */ +#define GREATEST_CHECK_CALL(RES) \ + do { \ + enum greatest_test_res greatest_RES = RES; \ + if (greatest_RES != GREATEST_TEST_RES_PASS) { \ + return greatest_RES; \ + } \ + } while (0) \ + +#if GREATEST_USE_TIME +#define GREATEST_SET_TIME(NAME) \ + NAME = clock(); \ + if (NAME == (clock_t) -1) { \ + GREATEST_FPRINTF(GREATEST_STDOUT, \ + "clock error: %s\n", #NAME); \ + exit(EXIT_FAILURE); \ + } + +#define GREATEST_CLOCK_DIFF(C1, C2) \ + GREATEST_FPRINTF(GREATEST_STDOUT, " (%lu ticks, %.3f sec)", \ + (long unsigned int) (C2) - (long unsigned int)(C1), \ + (double)((C2) - (C1)) / (1.0 * (double)CLOCKS_PER_SEC)) +#else +#define GREATEST_SET_TIME(UNUSED) +#define GREATEST_CLOCK_DIFF(UNUSED1, UNUSED2) +#endif + +#if GREATEST_USE_LONGJMP +#define GREATEST_SAVE_CONTEXT() \ + /* setjmp returns 0 (GREATEST_TEST_RES_PASS) on first call * \ + * so the test runs, then RES_FAIL from FAIL_WITH_LONGJMP. */ \ + ((enum greatest_test_res)(setjmp(greatest_info.jump_dest))) +#else +#define GREATEST_SAVE_CONTEXT() \ + /*a no-op, since setjmp/longjmp aren't being used */ \ + GREATEST_TEST_RES_PASS +#endif + +/* Run every suite / test function run within BODY in pseudo-random + * order, seeded by SEED. (The top 3 bits of the seed are ignored.) + * + * This should be called like: + * GREATEST_SHUFFLE_TESTS(seed, { + * GREATEST_RUN_TEST(some_test); + * GREATEST_RUN_TEST(some_other_test); + * GREATEST_RUN_TEST(yet_another_test); + * }); + * + * Note that the body of the second argument will be evaluated + * multiple times. */ +#define GREATEST_SHUFFLE_SUITES(SD, BODY) GREATEST_SHUFFLE(0, SD, BODY) +#define GREATEST_SHUFFLE_TESTS(SD, BODY) GREATEST_SHUFFLE(1, SD, BODY) +#define GREATEST_SHUFFLE(ID, SD, BODY) \ + do { \ + struct greatest_prng *prng = &greatest_info.prng[ID]; \ + greatest_prng_init_first_pass(ID); \ + do { \ + prng->count = 0; \ + if (prng->initialized) { greatest_prng_step(ID); } \ + BODY; \ + if (!prng->initialized) { \ + if (!greatest_prng_init_second_pass(ID, SD)) { break; } \ + } else if (prng->count_run == prng->count_ceil) { \ + break; \ + } \ + } while (!GREATEST_FAILURE_ABORT()); \ + prng->count_run = prng->random_order = prng->initialized = 0; \ + } while(0) + +/* Include several function definitions in the main test file. */ +#define GREATEST_MAIN_DEFS() \ + \ +/* Is FILTER a subset of NAME? */ \ +static int greatest_name_match(const char *name, const char *filter, \ + int res_if_none) { \ + size_t offset = 0; \ + size_t filter_len = filter ? strlen(filter) : 0; \ + if (filter_len == 0) { return res_if_none; } /* no filter */ \ + if (greatest_info.exact_name_match && strlen(name) != filter_len) { \ + return 0; /* ignore substring matches */ \ + } \ + while (name[offset] != '\0') { \ + if (name[offset] == filter[0]) { \ + if (0 == strncmp(&name[offset], filter, filter_len)) { \ + return 1; \ + } \ + } \ + offset++; \ + } \ + \ + return 0; \ +} \ + \ +static void greatest_buffer_test_name(const char *name) { \ + struct greatest_run_info *g = &greatest_info; \ + size_t len = strlen(name), size = sizeof(g->name_buf); \ + memset(g->name_buf, 0x00, size); \ + (void)strncat(g->name_buf, name, size - 1); \ + if (g->name_suffix && (len + 1 < size)) { \ + g->name_buf[len] = '_'; \ + strncat(&g->name_buf[len+1], g->name_suffix, size-(len+2)); \ + } \ +} \ + \ +/* Before running a test, check the name filtering and \ + * test shuffling state, if applicable, and then call setup hooks. */ \ +int greatest_test_pre(const char *name) { \ + struct greatest_run_info *g = &greatest_info; \ + int match; \ + greatest_buffer_test_name(name); \ + match = greatest_name_match(g->name_buf, g->test_filter, 1) && \ + !greatest_name_match(g->name_buf, g->test_exclude, 0); \ + if (GREATEST_LIST_ONLY()) { /* just listing test names */ \ + if (match) { \ + GREATEST_FPRINTF(GREATEST_STDOUT, " %s\n", g->name_buf); \ + } \ + goto clear; \ + } \ + if (match && (!GREATEST_FIRST_FAIL() || g->suite.failed == 0)) { \ + struct greatest_prng *p = &g->prng[1]; \ + if (p->random_order) { \ + p->count++; \ + if (!p->initialized || ((p->count - 1) != p->state)) { \ + goto clear; /* don't run this test yet */ \ + } \ + } \ + if (g->running_test) { \ + fprintf(stderr, "Error: Test run inside another test.\n"); \ + return 0; \ + } \ + GREATEST_SET_TIME(g->suite.pre_test); \ + if (g->setup) { g->setup(g->setup_udata); } \ + p->count_run++; \ + g->running_test = 1; \ + return 1; /* test should be run */ \ + } else { \ + goto clear; /* skipped */ \ + } \ +clear: \ + g->name_suffix = NULL; \ + return 0; \ +} \ + \ +static void greatest_do_pass(void) { \ + struct greatest_run_info *g = &greatest_info; \ + if (GREATEST_IS_VERBOSE()) { \ + GREATEST_FPRINTF(GREATEST_STDOUT, "PASS %s: %s", \ + g->name_buf, g->msg ? g->msg : ""); \ + } else { \ + GREATEST_FPRINTF(GREATEST_STDOUT, "."); \ + } \ + g->suite.passed++; \ +} \ + \ +static void greatest_do_fail(void) { \ + struct greatest_run_info *g = &greatest_info; \ + if (GREATEST_IS_VERBOSE()) { \ + GREATEST_FPRINTF(GREATEST_STDOUT, \ + "FAIL %s: %s (%s:%u)", g->name_buf, \ + g->msg ? g->msg : "", g->fail_file, g->fail_line); \ + } else { \ + GREATEST_FPRINTF(GREATEST_STDOUT, "F"); \ + g->col++; /* add linebreak if in line of '.'s */ \ + if (g->col != 0) { \ + GREATEST_FPRINTF(GREATEST_STDOUT, "\n"); \ + g->col = 0; \ + } \ + GREATEST_FPRINTF(GREATEST_STDOUT, "FAIL %s: %s (%s:%u)\n", \ + g->name_buf, g->msg ? g->msg : "", \ + g->fail_file, g->fail_line); \ + } \ + g->suite.failed++; \ +} \ + \ +static void greatest_do_skip(void) { \ + struct greatest_run_info *g = &greatest_info; \ + if (GREATEST_IS_VERBOSE()) { \ + GREATEST_FPRINTF(GREATEST_STDOUT, "SKIP %s: %s", \ + g->name_buf, g->msg ? g->msg : ""); \ + } else { \ + GREATEST_FPRINTF(GREATEST_STDOUT, "s"); \ + } \ + g->suite.skipped++; \ +} \ + \ +void greatest_test_post(int res) { \ + GREATEST_SET_TIME(greatest_info.suite.post_test); \ + if (greatest_info.teardown) { \ + void *udata = greatest_info.teardown_udata; \ + greatest_info.teardown(udata); \ + } \ + \ + greatest_info.running_test = 0; \ + if (res <= GREATEST_TEST_RES_FAIL) { \ + greatest_do_fail(); \ + } else if (res >= GREATEST_TEST_RES_SKIP) { \ + greatest_do_skip(); \ + } else if (res == GREATEST_TEST_RES_PASS) { \ + greatest_do_pass(); \ + } \ + greatest_info.name_suffix = NULL; \ + greatest_info.suite.tests_run++; \ + greatest_info.col++; \ + if (GREATEST_IS_VERBOSE()) { \ + GREATEST_CLOCK_DIFF(greatest_info.suite.pre_test, \ + greatest_info.suite.post_test); \ + GREATEST_FPRINTF(GREATEST_STDOUT, "\n"); \ + } else if (greatest_info.col % greatest_info.width == 0) { \ + GREATEST_FPRINTF(GREATEST_STDOUT, "\n"); \ + greatest_info.col = 0; \ + } \ + fflush(GREATEST_STDOUT); \ +} \ + \ +static void report_suite(void) { \ + if (greatest_info.suite.tests_run > 0) { \ + GREATEST_FPRINTF(GREATEST_STDOUT, \ + "\n%u test%s - %u passed, %u failed, %u skipped", \ + greatest_info.suite.tests_run, \ + greatest_info.suite.tests_run == 1 ? "" : "s", \ + greatest_info.suite.passed, \ + greatest_info.suite.failed, \ + greatest_info.suite.skipped); \ + GREATEST_CLOCK_DIFF(greatest_info.suite.pre_suite, \ + greatest_info.suite.post_suite); \ + GREATEST_FPRINTF(GREATEST_STDOUT, "\n"); \ + } \ +} \ + \ +static void update_counts_and_reset_suite(void) { \ + greatest_info.setup = NULL; \ + greatest_info.setup_udata = NULL; \ + greatest_info.teardown = NULL; \ + greatest_info.teardown_udata = NULL; \ + greatest_info.passed += greatest_info.suite.passed; \ + greatest_info.failed += greatest_info.suite.failed; \ + greatest_info.skipped += greatest_info.suite.skipped; \ + greatest_info.tests_run += greatest_info.suite.tests_run; \ + memset(&greatest_info.suite, 0, sizeof(greatest_info.suite)); \ + greatest_info.col = 0; \ +} \ + \ +static int greatest_suite_pre(const char *suite_name) { \ + struct greatest_prng *p = &greatest_info.prng[0]; \ + if (!greatest_name_match(suite_name, greatest_info.suite_filter, 1) \ + || (GREATEST_FAILURE_ABORT())) { return 0; } \ + if (p->random_order) { \ + p->count++; \ + if (!p->initialized || ((p->count - 1) != p->state)) { \ + return 0; /* don't run this suite yet */ \ + } \ + } \ + p->count_run++; \ + update_counts_and_reset_suite(); \ + GREATEST_FPRINTF(GREATEST_STDOUT, "\n* Suite %s:\n", suite_name); \ + GREATEST_SET_TIME(greatest_info.suite.pre_suite); \ + return 1; \ +} \ + \ +static void greatest_suite_post(void) { \ + GREATEST_SET_TIME(greatest_info.suite.post_suite); \ + report_suite(); \ +} \ + \ +static void greatest_run_suite(greatest_suite_cb *suite_cb, \ + const char *suite_name) { \ + if (greatest_suite_pre(suite_name)) { \ + suite_cb(); \ + greatest_suite_post(); \ + } \ +} \ + \ +int greatest_do_assert_equal_t(const void *expd, const void *got, \ + greatest_type_info *type_info, void *udata) { \ + int eq = 0; \ + if (type_info == NULL || type_info->equal == NULL) { return 0; } \ + eq = type_info->equal(expd, got, udata); \ + if (!eq) { \ + if (type_info->print != NULL) { \ + GREATEST_FPRINTF(GREATEST_STDOUT, "\nExpected: "); \ + (void)type_info->print(expd, udata); \ + GREATEST_FPRINTF(GREATEST_STDOUT, "\n Got: "); \ + (void)type_info->print(got, udata); \ + GREATEST_FPRINTF(GREATEST_STDOUT, "\n"); \ + } \ + } \ + return eq; \ +} \ + \ +static void greatest_usage(const char *name) { \ + GREATEST_FPRINTF(GREATEST_STDOUT, \ + "Usage: %s [-hlfavex] [-s SUITE] [-t TEST] [-x EXCLUDE]\n" \ + " -h, --help print this Help\n" \ + " -l List suites and tests, then exit (dry run)\n" \ + " -f Stop runner after first failure\n" \ + " -a Abort on first failure (implies -f)\n" \ + " -v Verbose output\n" \ + " -s SUITE only run suites containing substring SUITE\n" \ + " -t TEST only run tests containing substring TEST\n" \ + " -e only run exact name match for -s or -t\n" \ + " -x EXCLUDE exclude tests containing substring EXCLUDE\n", \ + name); \ +} \ + \ +static void greatest_parse_options(int argc, char **argv) { \ + int i = 0; \ + for (i = 1; i < argc; i++) { \ + if (argv[i][0] == '-') { \ + char f = argv[i][1]; \ + if ((f == 's' || f == 't' || f == 'x') && argc <= i + 1) { \ + greatest_usage(argv[0]); exit(EXIT_FAILURE); \ + } \ + switch (f) { \ + case 's': /* suite name filter */ \ + greatest_set_suite_filter(argv[i + 1]); i++; break; \ + case 't': /* test name filter */ \ + greatest_set_test_filter(argv[i + 1]); i++; break; \ + case 'x': /* test name exclusion */ \ + greatest_set_test_exclude(argv[i + 1]); i++; break; \ + case 'e': /* exact name match */ \ + greatest_set_exact_name_match(); break; \ + case 'f': /* first fail flag */ \ + greatest_stop_at_first_fail(); break; \ + case 'a': /* abort() on fail flag */ \ + greatest_abort_on_fail(); break; \ + case 'l': /* list only (dry run) */ \ + greatest_list_only(); break; \ + case 'v': /* first fail flag */ \ + greatest_info.verbosity++; break; \ + case 'h': /* help */ \ + greatest_usage(argv[0]); exit(EXIT_SUCCESS); \ + default: \ + case '-': \ + if (0 == strncmp("--help", argv[i], 6)) { \ + greatest_usage(argv[0]); exit(EXIT_SUCCESS); \ + } else if (0 == strcmp("--", argv[i])) { \ + return; /* ignore following arguments */ \ + } \ + GREATEST_FPRINTF(GREATEST_STDOUT, \ + "Unknown argument '%s'\n", argv[i]); \ + greatest_usage(argv[0]); \ + exit(EXIT_FAILURE); \ + } \ + } \ + } \ +} \ + \ +int greatest_all_passed(void) { return (greatest_info.failed == 0); } \ + \ +void greatest_set_test_filter(const char *filter) { \ + greatest_info.test_filter = filter; \ +} \ + \ +void greatest_set_test_exclude(const char *filter) { \ + greatest_info.test_exclude = filter; \ +} \ + \ +void greatest_set_suite_filter(const char *filter) { \ + greatest_info.suite_filter = filter; \ +} \ + \ +void greatest_set_exact_name_match(void) { \ + greatest_info.exact_name_match = 1; \ +} \ + \ +void greatest_stop_at_first_fail(void) { \ + greatest_set_flag(GREATEST_FLAG_FIRST_FAIL); \ +} \ + \ +void greatest_abort_on_fail(void) { \ + greatest_set_flag(GREATEST_FLAG_ABORT_ON_FAIL); \ +} \ + \ +void greatest_list_only(void) { \ + greatest_set_flag(GREATEST_FLAG_LIST_ONLY); \ +} \ + \ +void greatest_get_report(struct greatest_report_t *report) { \ + if (report) { \ + report->passed = greatest_info.passed; \ + report->failed = greatest_info.failed; \ + report->skipped = greatest_info.skipped; \ + report->assertions = greatest_info.assertions; \ + } \ +} \ + \ +unsigned int greatest_get_verbosity(void) { \ + return greatest_info.verbosity; \ +} \ + \ +void greatest_set_verbosity(unsigned int verbosity) { \ + greatest_info.verbosity = (unsigned char)verbosity; \ +} \ + \ +void greatest_set_flag(greatest_flag_t flag) { \ + greatest_info.flags = (unsigned char)(greatest_info.flags | flag); \ +} \ + \ +void greatest_set_test_suffix(const char *suffix) { \ + greatest_info.name_suffix = suffix; \ +} \ + \ +void GREATEST_SET_SETUP_CB(greatest_setup_cb *cb, void *udata) { \ + greatest_info.setup = cb; \ + greatest_info.setup_udata = udata; \ +} \ + \ +void GREATEST_SET_TEARDOWN_CB(greatest_teardown_cb *cb, void *udata) { \ + greatest_info.teardown = cb; \ + greatest_info.teardown_udata = udata; \ +} \ + \ +static int greatest_string_equal_cb(const void *expd, const void *got, \ + void *udata) { \ + size_t *size = (size_t *)udata; \ + return (size != NULL \ + ? (0 == strncmp((const char *)expd, (const char *)got, *size)) \ + : (0 == strcmp((const char *)expd, (const char *)got))); \ +} \ + \ +static int greatest_string_printf_cb(const void *t, void *udata) { \ + (void)udata; /* note: does not check \0 termination. */ \ + return GREATEST_FPRINTF(GREATEST_STDOUT, "%s", (const char *)t); \ +} \ + \ +greatest_type_info greatest_type_info_string = { \ + greatest_string_equal_cb, greatest_string_printf_cb, \ +}; \ + \ +static int greatest_memory_equal_cb(const void *expd, const void *got, \ + void *udata) { \ + greatest_memory_cmp_env *env = (greatest_memory_cmp_env *)udata; \ + return (0 == memcmp(expd, got, env->size)); \ +} \ + \ +/* Hexdump raw memory, with differences highlighted */ \ +static int greatest_memory_printf_cb(const void *t, void *udata) { \ + greatest_memory_cmp_env *env = (greatest_memory_cmp_env *)udata; \ + const unsigned char *buf = (const unsigned char *)t; \ + unsigned char diff_mark = ' '; \ + FILE *out = GREATEST_STDOUT; \ + size_t i, line_i, line_len = 0; \ + int len = 0; /* format hexdump with differences highlighted */ \ + for (i = 0; i < env->size; i+= line_len) { \ + diff_mark = ' '; \ + line_len = env->size - i; \ + if (line_len > 16) { line_len = 16; } \ + for (line_i = i; line_i < i + line_len; line_i++) { \ + if (env->exp[line_i] != env->got[line_i]) diff_mark = 'X'; \ + } \ + len += GREATEST_FPRINTF(out, "\n%04x %c ", \ + (unsigned int)i, diff_mark); \ + for (line_i = i; line_i < i + line_len; line_i++) { \ + int m = env->exp[line_i] == env->got[line_i]; /* match? */ \ + len += GREATEST_FPRINTF(out, "%02x%c", \ + buf[line_i], m ? ' ' : '<'); \ + } \ + for (line_i = 0; line_i < 16 - line_len; line_i++) { \ + len += GREATEST_FPRINTF(out, " "); \ + } \ + GREATEST_FPRINTF(out, " "); \ + for (line_i = i; line_i < i + line_len; line_i++) { \ + unsigned char c = buf[line_i]; \ + len += GREATEST_FPRINTF(out, "%c", isprint(c) ? c : '.'); \ + } \ + } \ + len += GREATEST_FPRINTF(out, "\n"); \ + return len; \ +} \ + \ +void greatest_prng_init_first_pass(int id) { \ + greatest_info.prng[id].random_order = 1; \ + greatest_info.prng[id].count_run = 0; \ +} \ + \ +int greatest_prng_init_second_pass(int id, unsigned long seed) { \ + struct greatest_prng *p = &greatest_info.prng[id]; \ + if (p->count == 0) { return 0; } \ + p->count_ceil = p->count; \ + for (p->m = 1; p->m < p->count; p->m <<= 1) {} \ + p->state = seed & 0x1fffffff; /* only use lower 29 bits */ \ + p->a = 4LU * p->state; /* to avoid overflow when */ \ + p->a = (p->a ? p->a : 4) | 1; /* multiplied by 4 */ \ + p->c = 2147483647; /* and so p->c ((2 ** 31) - 1) is */ \ + p->initialized = 1; /* always relatively prime to p->a. */ \ + fprintf(stderr, "init_second_pass: a %lu, c %lu, state %lu\n", \ + p->a, p->c, p->state); \ + return 1; \ +} \ + \ +/* Step the pseudorandom number generator until its state reaches \ + * another test ID between 0 and the test count. \ + * This use a linear congruential pseudorandom number generator, \ + * with the power-of-two ceiling of the test count as the modulus, the \ + * masked seed as the multiplier, and a prime as the increment. For \ + * each generated value < the test count, run the corresponding test. \ + * This will visit all IDs 0 <= X < mod once before repeating, \ + * with a starting position chosen based on the initial seed. \ + * For details, see: Knuth, The Art of Computer Programming \ + * Volume. 2, section 3.2.1. */ \ +void greatest_prng_step(int id) { \ + struct greatest_prng *p = &greatest_info.prng[id]; \ + do { \ + p->state = ((p->a * p->state) + p->c) & (p->m - 1); \ + } while (p->state >= p->count_ceil); \ +} \ + \ +void GREATEST_INIT(void) { \ + /* Suppress unused function warning if features aren't used */ \ + (void)greatest_run_suite; \ + (void)greatest_parse_options; \ + (void)greatest_prng_step; \ + (void)greatest_prng_init_first_pass; \ + (void)greatest_prng_init_second_pass; \ + (void)greatest_set_test_suffix; \ + \ + memset(&greatest_info, 0, sizeof(greatest_info)); \ + greatest_info.width = GREATEST_DEFAULT_WIDTH; \ + GREATEST_SET_TIME(greatest_info.begin); \ +} \ + \ +/* Report passes, failures, skipped tests, the number of \ + * assertions, and the overall run time. */ \ +void GREATEST_PRINT_REPORT(void) { \ + if (!GREATEST_LIST_ONLY()) { \ + update_counts_and_reset_suite(); \ + GREATEST_SET_TIME(greatest_info.end); \ + GREATEST_FPRINTF(GREATEST_STDOUT, \ + "\nTotal: %u test%s", \ + greatest_info.tests_run, \ + greatest_info.tests_run == 1 ? "" : "s"); \ + GREATEST_CLOCK_DIFF(greatest_info.begin, \ + greatest_info.end); \ + GREATEST_FPRINTF(GREATEST_STDOUT, ", %u assertion%s\n", \ + greatest_info.assertions, \ + greatest_info.assertions == 1 ? "" : "s"); \ + GREATEST_FPRINTF(GREATEST_STDOUT, \ + "Pass: %u, fail: %u, skip: %u.\n", \ + greatest_info.passed, \ + greatest_info.failed, greatest_info.skipped); \ + } \ +} \ + \ +greatest_type_info greatest_type_info_memory = { \ + greatest_memory_equal_cb, greatest_memory_printf_cb, \ +}; \ + \ +greatest_run_info greatest_info + +/* Handle command-line arguments, etc. */ +#define GREATEST_MAIN_BEGIN() \ + do { \ + GREATEST_INIT(); \ + greatest_parse_options(argc, argv); \ + } while (0) + +/* Report results, exit with exit status based on results. */ +#define GREATEST_MAIN_END() \ + do { \ + GREATEST_PRINT_REPORT(); \ + return (greatest_all_passed() ? EXIT_SUCCESS : EXIT_FAILURE); \ + } while (0) + +/* Make abbreviations without the GREATEST_ prefix for the + * most commonly used symbols. */ +#if GREATEST_USE_ABBREVS +#define TEST GREATEST_TEST +#define SUITE GREATEST_SUITE +#define SUITE_EXTERN GREATEST_SUITE_EXTERN +#define RUN_TEST GREATEST_RUN_TEST +#define RUN_TEST1 GREATEST_RUN_TEST1 +#define RUN_SUITE GREATEST_RUN_SUITE +#define IGNORE_TEST GREATEST_IGNORE_TEST +#define ASSERT GREATEST_ASSERT +#define ASSERTm GREATEST_ASSERTm +#define ASSERT_FALSE GREATEST_ASSERT_FALSE +#define ASSERT_EQ GREATEST_ASSERT_EQ +#define ASSERT_NEQ GREATEST_ASSERT_NEQ +#define ASSERT_GT GREATEST_ASSERT_GT +#define ASSERT_GTE GREATEST_ASSERT_GTE +#define ASSERT_LT GREATEST_ASSERT_LT +#define ASSERT_LTE GREATEST_ASSERT_LTE +#define ASSERT_EQ_FMT GREATEST_ASSERT_EQ_FMT +#define ASSERT_IN_RANGE GREATEST_ASSERT_IN_RANGE +#define ASSERT_EQUAL_T GREATEST_ASSERT_EQUAL_T +#define ASSERT_STR_EQ GREATEST_ASSERT_STR_EQ +#define ASSERT_STRN_EQ GREATEST_ASSERT_STRN_EQ +#define ASSERT_MEM_EQ GREATEST_ASSERT_MEM_EQ +#define ASSERT_ENUM_EQ GREATEST_ASSERT_ENUM_EQ +#define ASSERT_FALSEm GREATEST_ASSERT_FALSEm +#define ASSERT_EQm GREATEST_ASSERT_EQm +#define ASSERT_NEQm GREATEST_ASSERT_NEQm +#define ASSERT_GTm GREATEST_ASSERT_GTm +#define ASSERT_GTEm GREATEST_ASSERT_GTEm +#define ASSERT_LTm GREATEST_ASSERT_LTm +#define ASSERT_LTEm GREATEST_ASSERT_LTEm +#define ASSERT_EQ_FMTm GREATEST_ASSERT_EQ_FMTm +#define ASSERT_IN_RANGEm GREATEST_ASSERT_IN_RANGEm +#define ASSERT_EQUAL_Tm GREATEST_ASSERT_EQUAL_Tm +#define ASSERT_STR_EQm GREATEST_ASSERT_STR_EQm +#define ASSERT_STRN_EQm GREATEST_ASSERT_STRN_EQm +#define ASSERT_MEM_EQm GREATEST_ASSERT_MEM_EQm +#define ASSERT_ENUM_EQm GREATEST_ASSERT_ENUM_EQm +#define PASS GREATEST_PASS +#define FAIL GREATEST_FAIL +#define SKIP GREATEST_SKIP +#define PASSm GREATEST_PASSm +#define FAILm GREATEST_FAILm +#define SKIPm GREATEST_SKIPm +#define SET_SETUP GREATEST_SET_SETUP_CB +#define SET_TEARDOWN GREATEST_SET_TEARDOWN_CB +#define CHECK_CALL GREATEST_CHECK_CALL +#define SHUFFLE_TESTS GREATEST_SHUFFLE_TESTS +#define SHUFFLE_SUITES GREATEST_SHUFFLE_SUITES + +#ifdef GREATEST_VA_ARGS +#define RUN_TESTp GREATEST_RUN_TESTp +#endif + +#if GREATEST_USE_LONGJMP +#define ASSERT_OR_LONGJMP GREATEST_ASSERT_OR_LONGJMP +#define ASSERT_OR_LONGJMPm GREATEST_ASSERT_OR_LONGJMPm +#define FAIL_WITH_LONGJMP GREATEST_FAIL_WITH_LONGJMP +#define FAIL_WITH_LONGJMPm GREATEST_FAIL_WITH_LONGJMPm +#endif + +#endif /* USE_ABBREVS */ + +#if defined(__cplusplus) && !defined(GREATEST_NO_EXTERN_CPLUSPLUS) +} +#endif + +#endif diff --git a/bakelite/tests/generator/struct-ctiny.bakelite b/bakelite/tests/generator/struct-ctiny.bakelite new file mode 100644 index 0000000..3a17145 --- /dev/null +++ b/bakelite/tests/generator/struct-ctiny.bakelite @@ -0,0 +1,70 @@ +enum Direction: uint8 { + Up = 0 + Down = 1 + Left = 2 + Right = 3 +} + +enum Speed: uint8 { + Stopped = 0 + Slow = 80 + Fast = 255 +} + +struct EnumStruct { + direction: Direction + speed: Speed +} + +struct TestStruct { + int1: int8 + int2: int32 + uint1: uint8 + uint2: uint16 + float1: float32 + b1: bool + b2: bool + b3: bool + data: bytes[4] + str: string[5] +} + +struct Ack { + code: uint8 +} + +struct SubA { + b1: bool + b2: bool +} + +struct SubB { + num: uint8 +} + +struct NestedStruct { + a: SubA + b: SubB + num: int8 +} + +struct SubC { + a: SubA +} + +struct DeeplyNestedStruct { + c: SubC +} + +struct ArrayStruct { + a: Direction[3] + b: Ack[2] + c: string[4][3] +} + +# Simple variable-length types without nesting +struct VariableLength { + a: bytes[] + b: string[] + c: uint8[] +}