Source code for ifgen.struct.header
"""
A module implementing a struct header-file interface generator.
"""
# built-in
from contextlib import ExitStack
from typing import Any
# third-party
from runtimepy.ui.controls import Default
from vcorelib.io.file_writer import (
CommentStyle,
IndentedFileWriter,
LineWithComment,
)
# internal
from ifgen.generation.interface import GenerateTask
from ifgen.struct.methods import struct_methods
from ifgen.struct.methods.fields import bit_fields
from ifgen.struct.python import python_struct_header
from ifgen.struct.stream import struct_stream_methods
FieldConfig = dict[str, int | str]
[docs]
def enforce_expected_size(
size: int, data: dict[str, Any], assert_msg: str
) -> None:
"""Enforce an expected-size field."""
# If expected size is set, verify it.
if "expected_size" in data:
assert data["expected_size"] == size, (
assert_msg,
data["expected_size"],
size,
)
[docs]
def struct_line(
name: str,
value: FieldConfig,
volatile: bool,
const: bool,
packed: bool,
array_length: int = None,
default: Default | str = None,
) -> LineWithComment:
"""Build a string for a struct-field line."""
prefix = "volatile " if volatile else ""
prefix += "const " if const else ""
line = str(value["type"])
if not packed and array_length is not None:
line = f"<{prefix}{value['type']}, {array_length}>"
prefix = "std::array"
line += f" {name}"
if packed and array_length is not None:
line += f"[{name}_length]"
if default is not None:
if default is True or default is False:
default = str(default).lower()
line += f" = {default}"
elif const:
line += " = {}"
return f"{prefix}{line};", value.get("description") # type: ignore
[docs]
def struct_fields(task: GenerateTask, writer: IndentedFileWriter) -> None:
"""Generate struct fields."""
writer.c_comment("Fields.")
with writer.trailing_comment_lines(style=CommentStyle.C_DOXYGEN) as lines:
# Fields.
for field in task.instance["fields"]:
enforce_expected_size(
task.env.size(field["type"]) * field.get("array_length", 1),
field,
f"{task.name}.{field['name']}",
)
standard_array = task.instance["packed"] or bool(
task.instance.get("instances")
)
if "array_length" in field and standard_array:
lines.append(
(
(
f"static constexpr std::size_t "
f"{field['name']}_length = "
f"{field['array_length']};"
),
None,
)
)
with ExitStack() as stack:
is_union = field.get("alternates")
all_fields = [field]
if is_union:
writer.write("union")
stack.enter_context(writer.scope(suffix=";"))
all_fields.extend(field["alternates"])
for possible_union in all_fields:
if "type" not in possible_union:
possible_union["type"] = field["type"]
# Handle enum defaults.
default = field.get("default")
if task.env.is_enum(possible_union["type"]):
enum = task.env.get_enum(possible_union["type"])
if enum.default and default is None:
default = f"{possible_union['type']}_default"
if "array_length" in field and default is not None:
default = (
"{"
+ ", ".join(
[default for _ in range(field["array_length"])]
)
+ "}"
)
line, comment = struct_line(
possible_union["name"],
possible_union,
possible_union["volatile"],
possible_union["const"],
standard_array,
array_length=possible_union.get("array_length"),
default=default,
)
if is_union:
writer.write(
line + ("" if not comment else f" /* {comment} */")
)
else:
lines.append((line, comment))
lines.append(("", None))
[docs]
def struct_instance(
task: GenerateTask, writer: IndentedFileWriter, instance: dict[str, Any]
) -> None:
"""Generate struct instances."""
writer.empty()
if instance.get("description"):
with writer.javadoc():
writer.write(instance["description"])
type_base = f"{task.name} *"
writer.write(
(
"static "
+ ("volatile " if instance["volatile"] else "")
+ f"{type_base}const "
f"{instance['name']} = "
f"reinterpret_cast<{type_base}>({instance['address']});"
)
)
[docs]
def cpp_struct_header(task: GenerateTask, writer: IndentedFileWriter) -> None:
"""Create the contents of a C++ struct header file."""
attributes = ["gnu::packed"] if task.instance["packed"] else None
attribute_str = f"[[{', '.join(attributes)}]]" if attributes else ""
writer.write(f"struct {attribute_str} {task.name}")
with writer.scope(suffix=";"):
writer.c_comment("Constant attributes.")
with writer.trailing_comment_lines(
style=CommentStyle.C_DOXYGEN
) as lines:
if task.instance["identifier"]:
lines.append(
(
"static constexpr struct_id_t "
f"id = {task.protocol().id};",
f"{task.name}'s identifier.",
)
)
# trace=True
size = task.env.size(task.name)
enforce_expected_size(size, task.instance, task.name)
lines.append(
(
f"static constexpr std::size_t size = {size};",
f"{task.name}'s size in bytes.",
)
)
writer.empty()
struct_fields(task, writer)
# Methods.
writer.c_comment("Methods.")
if task.instance["methods"]:
struct_methods(task, writer)
bit_fields(task, writer)
# Add size assertion.
writer.empty()
writer.write(f"static_assert(sizeof({task.name}) == {task.name}::size);")
writer.write(f"static_assert(ifgen_struct<{task.name}>);")
if task.instance["stream"]:
writer.empty()
struct_stream_methods(task, writer)
# Instances.
instances = task.instance.get("instances", [])
for instance in instances:
struct_instance(task, writer, instance)
if len(instances) > 1:
for idx, instance in enumerate(instances):
writer.empty()
writer.write("template <uint8_t instance>")
writer.write(
f"inline volatile {task.name} *{task.name}_instance(void)"
)
with writer.indented():
writer.write(f"requires(instance == {idx})")
with writer.scope():
writer.write(f"return {instance['name']};")
[docs]
def struct_header(task: GenerateTask, writer: IndentedFileWriter) -> None:
"""Create the contents of a struct header file."""
if task.is_cpp:
cpp_struct_header(task, writer)
elif task.is_python:
python_struct_header(task, writer)