seL4-microkit/tool/sel4coreplat/sysxml.py

353 lines
13 KiB
Python

#
# Copyright 2021, Breakaway Consulting Pty. Ltd.
#
# SPDX-License-Identifier: BSD-2-Clause
#
from dataclasses import dataclass
from pathlib import Path
# See: https://stackoverflow.com/questions/6949395/is-there-a-way-to-get-a-line-number-from-an-elementtree-element
# Force use of Python elementtree to avoid overloading
import sys
sys.modules['_elementtree'] = None # type: ignore
import xml.etree.ElementTree as ET
from typing import Dict, Iterable, Optional, Set, Tuple
from sel4coreplat.util import str_to_bool, UserError
MIN_PAGE_SIZE = 0x1000 # FIXME: This shouldn't be here
class MissingAttribute(Exception):
def __init__(self, attribute_name: str, element: ET.Element):
super().__init__(f"Missing attribute: {attribute_name}")
self.attribute_name = attribute_name
self.element = element
def checked_lookup(el: ET.Element, attr: str) -> str:
try:
return el.attrib[attr]
except KeyError:
raise MissingAttribute(attr, el)
def _check_attrs(el: ET.Element, valid_keys: Iterable[str]) -> None:
for key in el.attrib:
if key not in valid_keys:
raise ValueError(f"invalid attribute '{key}'")
@dataclass(frozen=True, eq=True)
class PlatformDescription:
page_sizes: Tuple[int, ...]
class LineNumberingParser(ET.XMLParser):
def __init__(self, path: Path):
super().__init__()
self._path = path
def _start(self, *args, **kwargs): # type: ignore
element = super(self.__class__, self)._start(*args, **kwargs)
element._path = self._path
element._start_line_number = self.parser.CurrentLineNumber
element._start_column_number = self.parser.CurrentColumnNumber
element._loc_str = f"{element._path}:{element._start_line_number}.{element._start_column_number}"
return element
@dataclass(frozen=True, eq=True)
class SysMap:
mr: str
vaddr: int
perms: str # FIXME: should make this a better typed thing
cached: bool
element: Optional[ET.Element]
@dataclass(frozen=True, eq=True)
class SysIrq:
irq: int
id_: int
@dataclass(frozen=True, eq=True)
class SysSetVar:
symbol: str
region_paddr: Optional[str] = None
vaddr: Optional[int] = None
@dataclass(frozen=True, eq=True)
class ProtectionDomain:
name: str
priority: int
budget: int
period: int
pp: bool
program_image: Path
maps: Tuple[SysMap, ...]
irqs: Tuple[SysIrq, ...]
setvars: Tuple[SysSetVar, ...]
element: ET.Element
@dataclass(frozen=True, eq=True)
class SysMemoryRegion:
name: str
size: int
page_size: int
page_count: int
phys_addr: Optional[int]
@dataclass(frozen=True, eq=True)
class Channel:
pd_a: str
id_a: int
pd_b: str
id_b: int
element: ET.Element
class SystemDescription:
def __init__(
self,
memory_regions: Iterable[SysMemoryRegion],
protection_domains: Iterable[ProtectionDomain],
channels: Iterable[Channel]
) -> None:
self.memory_regions = tuple(memory_regions)
self.protection_domains = tuple(protection_domains)
self.channels = tuple(channels)
# Note: These could be dict comprehensions, but
# we want to perform duplicate checks as we
# build the data structure
self.pd_by_name: Dict[str, ProtectionDomain] = {}
self.mr_by_name: Dict[str, SysMemoryRegion] = {}
# Ensure there is at least one protection domain
if len(self.protection_domains) == 0:
raise UserError("At least one protection domain must be defined")
if len(self.protection_domains) > 63:
raise UserError(f"Too many protection domains ({len(self.protection_domains)}) defined. Maximum is 63.")
for pd in protection_domains:
if pd.name in self.pd_by_name:
raise UserError(f"Duplicate protection domain name '{pd.name}'.")
self.pd_by_name[pd.name] = pd
for mr in memory_regions:
if mr.name in self.mr_by_name:
raise UserError(f"Duplicate memory region name '{mr.name}'.")
self.mr_by_name[mr.name] = mr
# Ensure all CCs make senses
for cc in self.channels:
for pd_name in (cc.pd_a, cc.pd_b):
if pd_name not in self.pd_by_name:
raise UserError(f"Invalid pd name '{pd_name}'. on element '{cc.element.tag}': {cc.element._loc_str}") # type: ignore
# Ensure no duplicate IRQs
all_irqs = set()
for pd in self.protection_domains:
for sysirq in pd.irqs:
if sysirq.irq in all_irqs:
raise UserError(f"duplicate irq: {sysirq.irq} in protection domain: '{pd.name}' @ {pd.element._loc_str}") # type: ignore
all_irqs.add(sysirq.irq)
# Ensure no duplicate channel identifiers
ch_ids: Dict[str, Set[int]] = {pd_name: set() for pd_name in self.pd_by_name}
for pd in self.protection_domains:
for sysirq in pd.irqs:
if sysirq.id_ in ch_ids[pd.name]:
raise UserError(f"duplicate channel id: {sysirq.id_} in protection domain: '{pd.name}' @ {pd.element._loc_str}") # type: ignore
ch_ids[pd.name].add(sysirq.id_)
for cc in self.channels:
if cc.id_a in ch_ids[cc.pd_a]:
pd = self.pd_by_name[cc.pd_a]
raise UserError(f"duplicate channel id: {cc.id_a} in protection domain: '{pd.name}' @ {pd.element._loc_str}") # type: ignore
if cc.id_b in ch_ids[cc.pd_b]:
pd = self.pd_by_name[cc.pd_b]
raise UserError(f"duplicate channel id: {cc.id_b} in protection domain: '{pd.name}' @ {pd.element._loc_str}") # type: ignore
ch_ids[cc.pd_a].add(cc.id_a)
ch_ids[cc.pd_b].add(cc.id_b)
# Ensure that all maps are correct
for pd in self.protection_domains:
for map in pd.maps:
if map.mr not in self.mr_by_name:
raise UserError(f"Invalid memory region name '{map.mr}' on '{map.element.tag}' @ {map.element._loc_str}") # type: ignore
mr = self.mr_by_name[map.mr]
extra = map.vaddr % mr.page_size
if extra != 0:
raise UserError(f"Invalid vaddr alignment on '{map.element.tag}' @ {map.element._loc_str}") # type: ignore
# Note: Overlapping memory is checked in the build.
# Ensure all memory regions are used at least once. This only generates
# warnings, not errors
check_mrs = set(self.mr_by_name.keys())
for pd in self.protection_domains:
for m in pd.maps:
if m.mr in check_mrs:
check_mrs.remove(m.mr)
for mr_ in check_mrs:
print(f"WARNING: Unused memory region: {mr_}")
def xml2mr(mr_xml: ET.Element, plat_desc: PlatformDescription) -> SysMemoryRegion:
_check_attrs(mr_xml, ("name", "size", "page_size", "phys_addr"))
name = checked_lookup(mr_xml, "name")
size = int(checked_lookup(mr_xml, "size"), base=0)
page_size_str = mr_xml.attrib.get("page_size")
page_size = min(plat_desc.page_sizes) if page_size_str is None else int(page_size_str, base=0)
if page_size not in plat_desc.page_sizes:
raise ValueError(f"page size 0x{page_size:x} not supported")
if size % page_size != 0:
raise ValueError("size is not a multiple of the page size")
paddr_str = mr_xml.attrib.get("phys_addr")
paddr = None if paddr_str is None else int(paddr_str, base=0)
if paddr is not None and paddr % page_size != 0:
raise ValueError("phys_addr is not aligned to the page size")
page_count = size // page_size
return SysMemoryRegion(name, size, page_size, page_count, paddr)
def xml2pd(pd_xml: ET.Element) -> ProtectionDomain:
_check_attrs(pd_xml, ("name", "priority", "pp", "budget", "period"))
program_image: Optional[Path] = None
name = checked_lookup(pd_xml, "name")
priority = int(pd_xml.attrib.get("priority", "0"), base=0)
if priority < 0 or priority > 254:
raise ValueError("priority must be between 0 and 254")
budget = int(pd_xml.attrib.get("budget", "1000"), base=0)
period = int(pd_xml.attrib.get("period", str(budget)), base=0)
if budget > period:
raise ValueError(f"budget ({budget}) must be less than, or equal to, period ({period})")
pp = str_to_bool(pd_xml.attrib.get("pp", "false"))
maps = []
irqs = []
setvars = []
for child in pd_xml:
try:
if child.tag == "program_image":
_check_attrs(child, ("path", ))
if program_image is not None:
raise ValueError("program_image must only be specified once")
program_image = Path(checked_lookup(child, "path"))
elif child.tag == "map":
_check_attrs(child, ("mr", "vaddr", "perms", "cached", "setvar_vaddr"))
mr = checked_lookup(child, "mr")
vaddr = int(checked_lookup(child, "vaddr"), base=0)
perms = child.attrib.get("perms", "rw")
cached = str_to_bool(child.attrib.get("cached", "true"))
maps.append(SysMap(mr, vaddr, perms, cached, child))
setvar_vaddr = child.attrib.get("setvar_vaddr")
if setvar_vaddr:
setvars.append(SysSetVar(setvar_vaddr, vaddr=vaddr))
elif child.tag == "irq":
_check_attrs(child, ("irq", "id"))
irq = int(checked_lookup(child, "irq"), base=0)
id_ = int(checked_lookup(child, "id"), base=0)
irqs.append(SysIrq(irq, id_))
elif child.tag == "setvar":
_check_attrs(child, ("symbol", "region_paddr"))
symbol = checked_lookup(child, "symbol")
region_paddr = checked_lookup(child, "region_paddr")
setvars.append(SysSetVar(symbol, region_paddr=region_paddr))
else:
raise UserError(f"Invalid XML element '{child.tag}': {child._loc_str}") # type: ignore
except ValueError as e:
raise UserError(f"Error: {e} on element '{child.tag}': {child._loc_str}") # type: ignore
if program_image is None:
raise ValueError("program_image must be specified")
return ProtectionDomain(name, priority, budget, period, pp, program_image, tuple(maps), tuple(irqs), tuple(setvars), pd_xml)
def xml2channel(ch_xml: ET.Element) -> Channel:
_check_attrs(ch_xml, ())
ends = []
for child in ch_xml:
try:
if child.tag == "end":
_check_attrs(ch_xml, ("pd", "id"))
pd = checked_lookup(child, "pd")
id_ = int(checked_lookup(child, "id"))
if id_ >= 64:
raise ValueError("id must be < 64")
if id_ < 0:
raise ValueError("id must be >= 0")
ends.append((pd, id_))
else:
raise UserError(f"Invalid XML element '{child.tag}': {child._loc_str}") # type: ignore
except ValueError as e:
raise UserError(f"Error: {e} on element '{child.tag}': {child._loc_str}") # type: ignore
if len(ends) != 2:
raise ValueError("exactly two end elements must be specified")
return Channel(ends[0][0], ends[0][1], ends[1][0], ends[1][1], ch_xml)
def _check_no_text(el: ET.Element) -> None:
if not (el.text is None or el.text.strip() == ""):
raise UserError(f"Error: unexpected text found in element '{el.tag}' @ {el._loc_str}") # type: ignore
if not (el.tail is None or el.tail.strip() == ""):
raise UserError(f"Error: unexpected text found after element '{el.tag}' @ {el._loc_str}") # type: ignore
for child in el:
_check_no_text(child)
def xml2system(filename: Path, plat_desc: PlatformDescription) -> SystemDescription:
try:
tree = ET.parse(filename, parser=LineNumberingParser(filename))
except ET.ParseError as e:
line, column = e.position
raise UserError(f"XML parse error: {filename}:{line}.{column}")
root = tree.getroot()
memory_regions = []
protection_domains = []
channels = []
# Ensure there is no non-whitespace text
_check_no_text(root)
for child in root:
try:
if child.tag == "memory_region":
memory_regions.append(xml2mr(child, plat_desc))
elif child.tag == "protection_domain":
protection_domains.append(xml2pd(child))
elif child.tag == "channel":
channels.append(xml2channel(child))
else:
raise UserError(f"Invalid XML element '{child.tag}': {child._loc_str}") # type: ignore
except ValueError as e:
raise UserError(f"Error: {e} on element '{child.tag}': {child._loc_str}") # type: ignore
except MissingAttribute as e:
raise UserError(f"Error: Missing required attribute '{e.attribute_name}' on element '{e.element.tag}': {e.element._loc_str}") # type: ignore
return SystemDescription(
memory_regions=memory_regions,
protection_domains=protection_domains,
channels=channels,
)