# SPDX-License-Identifier: BSD-2-Clause
""" This module provides functions for the generation of interfaces. """
# Copyright (C) 2020 embedded brains GmbH (http://www.embedded-brains.de)
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from contextlib import contextmanager
import os
from typing import Any, Callable, Dict, Iterator, List, Union
from rtemsspec.content import CContent, CInclude, enabled_by_to_exp, \
ExpressionMapper, get_value_double_colon, get_value_doxygen_function, \
get_value_hash
from rtemsspec.items import Item, ItemCache, ItemGetValueContext, ItemMapper
ItemMap = Dict[str, Item]
Lines = Union[str, List[str]]
GetLines = Callable[["Node", Item, Any], Lines]
def _get_ingroups(item: Item) -> ItemMap:
ingroups = {} # type: ItemMap
for link in item.links_to_parents():
if link.role == "interface-ingroup":
ingroups[link.item.uid] = link.item
return ingroups
def _get_group_identifiers(groups: ItemMap) -> List[str]:
return [item["identifier"] for item in groups.values()]
def _forward_declaration(item: Item) -> str:
target = next(item.parents("interface-target"))
return f"{target['interface-type']} {target['name']}"
def _get_value_forward_declaration(ctx: ItemGetValueContext) -> Any:
return _forward_declaration(ctx.item)
class _InterfaceMapper(ItemMapper):
def __init__(self, node: "Node"):
super().__init__(node.item)
self._node = node
self._code_or_doc = "doc"
self.add_get_value("interface/forward-declaration:code:/name",
_get_value_forward_declaration)
self.add_get_value("interface/forward-declaration:doc:/name",
_get_value_forward_declaration)
self.add_get_value("interface/function:doc:/name",
get_value_doxygen_function)
self.add_get_value("interface/enumerator:doc:/name",
get_value_double_colon)
self.add_get_value("interface/typedef:doc:/name",
get_value_double_colon)
self.add_get_value("interface/define:doc:/name", get_value_hash)
self.add_get_value("interface/enum:doc:/name", get_value_hash)
self.add_get_value("interface/macro:doc:/name",
get_value_doxygen_function)
self.add_get_value("interface/variable:doc:/name", get_value_hash)
for opt in ["feature-enable", "feature", "initializer", "integer"]:
name = f"interface/appl-config-option/{opt}:doc:/name"
self.add_get_value(name, get_value_hash)
self.add_get_value("interface/unspecified-function:doc:/name",
get_value_doxygen_function)
@contextmanager
def code(self) -> Iterator[None]:
""" Enables code mapping. """
code_or_doc = self._code_or_doc
self._code_or_doc = "code"
yield
self._code_or_doc = code_or_doc
def get_value(self, ctx: ItemGetValueContext) -> Any:
if self._code_or_doc == "code" and ctx.item["type"] == "interface":
node = self._node
header_file = node.header_file
if ctx.item["interface-type"] == "enumerator":
for link in ctx.item.links_to_children():
if link.role == "interface-enumerator":
header_file.add_includes(link.item)
else:
header_file.add_includes(ctx.item)
header_file.add_potential_edge(node, ctx.item)
return super().get_value(
ItemGetValueContext(ctx.item, f"{self._code_or_doc}:{ctx.path}",
ctx.value, ctx.key, ctx.index))
def enabled_by_to_defined(self, enabled_by: str) -> str:
"""
Maps an item-level enabled-by attribute value to the corresponding
defined expression.
"""
return self._node.header_file.enabled_by_defined[enabled_by]
class _InterfaceExpressionMapper(ExpressionMapper):
def __init__(self, mapper: _InterfaceMapper):
super().__init__()
self._mapper = mapper
def map_symbol(self, symbol: str) -> str:
with self._mapper.code():
return self._mapper.substitute(symbol)
class _ItemLevelExpressionMapper(ExpressionMapper):
def __init__(self, mapper: _InterfaceMapper):
super().__init__()
self._mapper = mapper
def map_symbol(self, symbol: str) -> str:
with self._mapper.code():
return self._mapper.substitute(
self._mapper.enabled_by_to_defined(symbol))
class _HeaderExpressionMapper(ExpressionMapper):
def __init__(self, item: Item, enabled_by_defined: Dict[str, str]):
super().__init__()
self._mapper = ItemMapper(item)
self._enabled_by_defined = enabled_by_defined
def map_symbol(self, symbol: str) -> str:
return self._mapper.substitute(self._enabled_by_defined[symbol])
def _add_definition(node: "Node", item: Item, prefix: str,
value: Dict[str, Any], get_lines: GetLines) -> CContent:
content = CContent()
default = value["default"]
variants = value["variants"]
if variants:
ifelse = "#if "
with node.mapper.prefix(os.path.join(prefix, "variants")):
for variant in variants:
enabled_by = enabled_by_to_exp(
variant["enabled-by"],
_InterfaceExpressionMapper(node.mapper))
content.append(f"{ifelse}{enabled_by}")
with content.indent():
content.append(get_lines(node, item,
variant["definition"]))
ifelse = "#elif "
if default is not None:
content.append("#else")
with node.mapper.prefix(os.path.join(prefix, "default")):
with content.indent():
content.append(get_lines(node, item, default))
content.append("#endif")
else:
with node.mapper.prefix(os.path.join(prefix, "default")):
content.append(get_lines(node, item, default))
return content
class Node:
""" Nodes of a header file. """
def __init__(self, header_file: "_HeaderFile", item: Item,
ingroups: ItemMap):
self.header_file = header_file
self.item = item
self.ingroups = ingroups
self.in_edges = {} # type: ItemMap
self.out_edges = {} # type: ItemMap
self.content = CContent()
self.mapper = _InterfaceMapper(self)
def __lt__(self, other: "Node") -> bool:
return self.item.uid < other.item.uid
@contextmanager
def _enum_struct_or_union(self) -> Iterator[None]:
self.content.add(self._get_description(self.item, self.ingroups))
name = self.item["name"]
typename = self.item["interface-type"]
kind = self.item["definition-kind"]
if kind == f"{typename}-only":
self.content.append(f"{typename} {name} {{")
elif kind == "typedef-only":
self.content.append(f"typedef {typename} {{")
else:
self.content.append(f"typedef {typename} {name} {{")
self.content.push_indent()
yield
self.content.pop_indent()
if kind == f"{typename}-only":
self.content.append("};")
else:
self.content.append(f"}} {name};")
def _generate(self) -> None:
_NODE_GENERATORS[self.item["interface-type"]](self)
def generate(self) -> None:
""" Generates a node to generate the node content. """
enabled_by = self.item["enabled-by"]
if not isinstance(enabled_by, bool) or not enabled_by:
mapper = _ItemLevelExpressionMapper(self.mapper)
self.content.add(f"#if {enabled_by_to_exp(enabled_by, mapper)}")
with self.content.indent():
self._generate()
self.content.add("#endif")
else:
self._generate()
def generate_compound(self) -> None:
""" Generates a compound (struct or union). """
with self._enum_struct_or_union():
for index, definition in enumerate(self.item["definition"]):
self.content.add(
_add_definition(self, self.item, f"definition[{index}]",
definition, Node._get_compound_definition))
def generate_enum(self) -> None:
""" Generates an enum. """
with self._enum_struct_or_union():
enumerators = [] # type: List[CContent]
for link in self.item.links_to_parents():
if link.role != "interface-enumerator":
continue
enumerator = self._get_description(link.item, {})
enumerator.append(
_add_definition(self, link.item, "definition",
link.item["definition"],
Node._get_enumerator_definition))
enumerators.append(enumerator)
for enumerator in enumerators[0:-1]:
enumerator.lines[-1] += ","
enumerator.append("")
self.content.append(enumerator)
try:
self.content.append(enumerators[-1])
except IndexError:
pass
def generate_define(self) -> None:
""" Generates a define. """
self._add_generic_definition(Node._get_define_definition)
def generate_forward_declaration(self) -> None:
""" Generates a forward declaration. """
self.content.append([
"", "/* Forward declaration */",
_forward_declaration(self.item) + ";"
])
def generate_function(self) -> None:
""" Generates a function. """
self._add_generic_definition(Node._get_function_definition)
def generate_group(self) -> None:
""" Generates a group. """
self.header_file.add_ingroup(self.item)
for ingroup in self.ingroups.values():
self.header_file.add_potential_edge(self, ingroup)
self.content.add_group(self.item["identifier"], self.item["name"],
_get_group_identifiers(self.ingroups),
self.item["brief"], self.item["description"])
def generate_macro(self) -> None:
""" Generates a macro. """
self._add_generic_definition(Node._get_macro_definition)
def generate_typedef(self) -> None:
""" Generates a typedef. """
self._add_generic_definition(Node._get_typedef_definition)
def generate_variable(self) -> None:
""" Generates a variable. """
self._add_generic_definition(Node._get_variable_definition)
def substitute_code(self, text: str) -> str:
"""
Performs a variable substitution on code using the item mapper of the
node.
"""
if text:
with self.mapper.code():
return self.mapper.substitute(text.strip("\n"))
return text
def substitute_text(self, text: str) -> str:
"""
Performs a variable substitution on a description using the item mapper
of the node.
"""
if text:
return self.mapper.substitute(text.strip("\n"))
return text
def _get_compound_definition(self, item: Item, definition: Any) -> Lines:
content = CContent()
content.add_description_block(
self.substitute_text(definition["brief"]),
self.substitute_text(definition["description"]))
kind = definition["kind"]
if kind == "member":
member = self.substitute_code(definition["definition"]) + ";"
content.append(member.split("\n"))
else:
content.append(f"{kind} {{")
content.gap = False
with content.indent():
for index, compound_member in enumerate(
definition["definition"]):
content.add(
_add_definition(self, item, f"definition[{index}]",
compound_member,
Node._get_compound_definition))
name = definition["name"]
content.append(f"}} {name};")
return content.lines
def _get_enumerator_definition(self, item: Item, definition: Any) -> Lines:
name = item["name"]
if definition:
return f"{name} = {self.substitute_code(definition)}"
return f"{name}"
def _get_define_definition(self, item: Item, definition: Any) -> Lines:
name = item["name"]
value = self.substitute_code(definition)
if value:
return f"#define {name} {value}".split("\n")
return f"#define {name}"
def _get_function_definition(self, item: Item, definition: Any) -> Lines:
content = CContent()
name = item["name"]
ret = self.substitute_code(definition["return"])
params = [
self.substitute_code(param) for param in definition["params"]
]
body = definition["body"]
if body:
with content.function("static inline " + ret, name, params):
content.add(self.substitute_code(body))
else:
content.declare_function(ret, name, params)
return content.lines
def _get_macro_definition(self, item: Item, definition: Any) -> Lines:
name = item["name"]
params = [param["name"] for param in item["params"]]
if params:
param_line = " " + ", ".join(params) + " "
else:
param_line = ""
line = f"#define {name}({param_line})"
if len(line) > 79:
param_block = ", \\\n ".join(params)
line = f"#define {name}( \\\n {param_block} \\\n)"
if not definition:
return line
body_lines = self.substitute_code(definition).split("\n")
if len(body_lines) == 1 and len(line + body_lines[0]) <= 79:
body = " "
else:
body = " \\\n "
body += " \\\n ".join(body_lines)
return line + body
def _get_typedef_definition(self, _item: Item, definition: Any) -> Lines:
return f"typedef {self.substitute_code(definition)};"
def _get_variable_definition(self, _item: Item, definition: Any) -> Lines:
return f"extern {self.substitute_code(definition)};"
def _get_description(self, item: Item, ingroups: ItemMap) -> CContent:
content = CContent()
with content.doxygen_block():
content.add_ingroup(_get_group_identifiers(ingroups))
content.add_brief_description(self.substitute_text(item["brief"]))
content.wrap(self.substitute_text(item["description"]))
content.wrap(self.substitute_text(item["notes"]))
if "params" in item:
content.add_param_description(item["params"],
self.substitute_text)
if "return" in item:
ret = item["return"]
for retval in ret["return-values"]:
content.wrap(self.substitute_text(retval["description"]),
initial_indent=self.substitute_text(
f"@retval {retval['value']} "))
content.wrap(self.substitute_text(ret["return"]),
initial_indent="@return ")
return content
def _add_generic_definition(self, get_lines: GetLines) -> None:
self.content.add(self._get_description(self.item, self.ingroups))
self.content.append(
_add_definition(self, self.item, "definition",
self.item["definition"], get_lines))
_NODE_GENERATORS = {
"enum": Node.generate_enum,
"define": Node.generate_define,
"forward-declaration": Node.generate_forward_declaration,
"function": Node.generate_function,
"group": Node.generate_group,
"macro": Node.generate_macro,
"struct": Node.generate_compound,
"typedef": Node.generate_typedef,
"union": Node.generate_compound,
"variable": Node.generate_variable,
}
class _HeaderFile:
""" A header file. """
def __init__(self, item: Item, enabled_by_defined: Dict[str, str]):
self._item = item
self._content = CContent()
self._content.register_license_and_copyrights_of_item(item)
self._ingroups = {} # type: ItemMap
self._includes = [] # type: List[Item]
self._nodes = {} # type: Dict[str, Node]
self.enabled_by_defined = enabled_by_defined
def add_includes(self, item: Item) -> None:
""" Adds the includes of the item to the header file includes. """
for link in item.links_to_parents():
if link.role == "interface-placement" and link.item[
"interface-type"] == "header-file":
self._includes.append(link.item)
def add_ingroup(self, item: Item) -> None:
""" Adds an ingroup to the header file. """
self._ingroups[item.uid] = item
def _add_child(self, item: Item) -> None:
ingroups = _get_ingroups(item)
if item["interface-type"] != "group":
self._ingroups.update(ingroups)
self._nodes[item.uid] = Node(self, item, ingroups)
self._content.register_license_and_copyrights_of_item(item)
def add_potential_edge(self, node: Node, item: Item) -> None:
"""
Adds a potential edge from a node to another node identified by an
item.
"""
if item.uid in self._nodes and item.uid != node.item.uid:
node.out_edges[item.uid] = item
self._nodes[item.uid].in_edges[node.item.uid] = node.item
def _resolve_ingroups(self, node: Node) -> None:
for ingroup in node.ingroups.values():
self.add_potential_edge(node, ingroup)
def generate_nodes(self) -> None:
""" Generates all nodes of this header file. """
for link in self._item.links_to_children():
if link.role == "interface-placement":
self._add_child(link.item)
for node in self._nodes.values():
self._resolve_ingroups(node)
node.generate()
def _get_nodes_in_dependency_order(self) -> List[Node]:
"""
Gets the nodes of this header file ordered according to node
dependencies and UIDs.
Performs a topological sort using Kahn's algorithm.
"""
nodes_in_dependency_order = [] # type: List[Node]
# Get incoming edge degrees for all nodes
in_degree = {} # type: Dict[str, int]
for node in self._nodes.values():
in_degree[node.item.uid] = len(node.in_edges)
# Create a queue with all nodes with no incoming edges sorted by UID
queue = [] # type: List[Node]
for node in self._nodes.values():
if in_degree[node.item.uid] == 0:
queue.append(node)
queue.sort(reverse=True)
# Topological sort
while queue:
node = queue.pop(0)
nodes_in_dependency_order.insert(0, node)
# Sort by UID
for uid in sorted(node.out_edges):
in_degree[uid] -= 1
if in_degree[uid] == 0:
queue.append(self._nodes[uid])
return nodes_in_dependency_order
def finalize(self) -> None:
""" Finalizes the header file. """
self._content.prepend_spdx_license_identifier()
with self._content.file_block():
self._content.add_ingroup(_get_group_identifiers(self._ingroups))
self._content.add_brief_description(self._item["brief"])
self._content.add_copyrights_and_licenses()
self._content.add_automatically_generated_warning()
with self._content.header_guard(self._item["path"]):
exp_mapper = _HeaderExpressionMapper(self._item,
self.enabled_by_defined)
includes = [
CInclude(item["path"],
enabled_by_to_exp(item["enabled-by"], exp_mapper))
for item in self._includes if item != self._item
]
includes.extend([
CInclude(link.item["path"],
enabled_by_to_exp(link["enabled-by"], exp_mapper))
for link in self._item.links_to_parents()
if link.role == "interface-include"
])
self._content.add_includes(includes)
with self._content.extern_c():
for node in self._get_nodes_in_dependency_order():
self._content.add(node.content)
def write(self, domain_path: str) -> None:
""" Writes the header file. """
self._content.write(
os.path.join(domain_path, self._item["prefix"],
self._item["path"]))
def _generate_header_file(item: Item, domains: Dict[str, str],
enabled_by_defined: Dict[str, str]) -> None:
domain = next(item.parents("interface-placement"))
assert domain["interface-type"] == "domain"
domain_path = domains.get(domain.uid, None)
if domain_path is None:
return
header_file = _HeaderFile(item, enabled_by_defined)
header_file.generate_nodes()
header_file.finalize()
header_file.write(domain_path)
def _visit_header_files(item: Item, domains: Dict[str, str],
enabled_by_defined: Dict[str, str]) -> None:
for child in item.children():
_visit_header_files(child, domains, enabled_by_defined)
if item["type"] == "interface" and item["interface-type"] == "header-file":
_generate_header_file(item, domains, enabled_by_defined)
def _gather_enabled_by_defined(item_level_interfaces: List[str],
item_cache: ItemCache) -> Dict[str, str]:
enabled_by_defined = {} # type: Dict[str, str]
for uid in item_level_interfaces:
for link in item_cache[uid].links_to_children():
if link.role == "interface-placement":
define = f"defined(${{{link.item.uid}:/name}})"
enabled_by_defined[link.item["name"]] = define
return enabled_by_defined
def generate(config: dict, item_cache: ItemCache) -> None:
"""
Generates header files according to the configuration.
:param config: A dictionary with configuration entries.
:param item_cache: The specification item cache containing the interfaces.
"""
enabled_by_defined = _gather_enabled_by_defined(
config["item-level-interfaces"], item_cache)
for item in item_cache.top_level.values():
_visit_header_files(item, config["domains"], enabled_by_defined)