Русский
Русский
English
Статистика
Реклама

Опыт написания IDL для embedded

Предисловие

Я при работе с микроконтроллерами часто сталкивался с бинарными протоколами. Особенно, когда есть несколько контроллеров. Или же используется bluetooth low energy и необходимо написать код для обработки бинарных данных в характеристике. Помимо кода всегда требуется понятная документация.

Всегда возникает вопрос - а можно ли описать как-то протокол и сгенерировать на все платформы код и документацию? В этом может помочь IDL.

1. Что такое IDL

Определение IDL довольно простое и уже представлено на wikipedia

IDL, илиязык описания интерфейсов(англ.Interface Description LanguageилиInterface Definition Language)язык спецификацийдля описанияинтерфейсов, синтаксически похожий на описание классов в языкеC++.

Самое главное в IDL - он должен хорошо описывать интерфейс взаимодействия, API, протокол. Он должен быть достаточно понятен, чтобы служить другим инженерам документацией.

Бонус также является - генерация документации, структур, кода.

2. Мотивация

В процессе работы я попробовал разные кодогенераторы и IDL. Среди тех, что попробовал были - QFace (http://personeltest.ru/aways/github.com/Pelagicore/qface), swagger (Это не IDL, а API development tool). Также существует коммерческое решение проблемы: https://www.protlr.com/.

Swagger больше подходит к REST API. Поэтому сразу был отметён. Однако его можно использовать если применяется cbor (бинарный аналог json с кучей крутых фич).

В QFace давно не было коммитов, хотелось некоторых "наворотов" для применения в embedded, возникли сложности при написании шаблона. Он не ищет символы сам, не умеет считать поля enum-ов.

Бесплатные решения было найти сложно, чтобы можно было комфортно использовать при разработке бинарных протоколов.

Поэтому я отказался от генераторов кода и IDL в пользу написания некоторых "автоматизаций" в коде, позволяющих проще писать адаптер протокола. Но протокол с коллегами продолжили описывать при помощи QFace. Решил в свободное время попробовать сделать что-то более или менее годное.

2.1 Обзор QFace

IDL, которая являлась источником вдохновения, имеет простой синтаксис:

module <module> <version>import <module> <version>interface <Identifier> {    <type> <identifier>    <type> <operation>(<parameter>*)    signal <signal>(<parameter>*)}struct <Identifier> {    <type> <identifier>;}enum <Identifier> {    <name> = <value>,}flag <Identifier> {    <name> = <value>,}

Для генерации используется jinja2. Пример:

{% for module in system.modules %}    {%- for interface in module.interfaces -%}    INTERFACE, {{module}}.{{interface}}    {% endfor -%}    {%- for struct in module.structs -%}    STRUCT , {{module}}.{{struct}}    {% endfor -%}    {%- for enum in module.enums -%}    ENUM   , {{module}}.{{enum}}    {% endfor -%}{% endfor %}

Концепция интересная. Можно было просто "подпилить" для комфорта "напильником", что конечно и сделал мой коллега. Но мне показалось интересным взять библиотеку sly и просто написать IDL с нужными фичами.

3. Обзор sly

Почему именно sly - библиотека очень проста для описания грамматики.

Сначала надо написать лексер. Он токенизирует код чтобы далее было проще обрабатывать парсером. Код из документации:

class CalcLexer(Lexer):    # Set of token names.   This is always required    tokens = { ID, NUMBER, PLUS, MINUS, TIMES,               DIVIDE, ASSIGN, LPAREN, RPAREN }    # String containing ignored characters between tokens    ignore = ' \t'    # Regular expression rules for tokens    ID      = r'[a-zA-Z_][a-zA-Z0-9_]*'    NUMBER  = r'\d+'    PLUS    = r'\+'    MINUS   = r'-'    TIMES   = r'\*'    DIVIDE  = r'/'    ASSIGN  = r'='    LPAREN  = r'\('    RPAREN  = r'\)'

Нужно наследовать класс Lexer, в переменную tokens - добавить свои использованные токены. Само определение токенов делается в теле класса - достаточно просто описать регулярное выражение, соответсвующее токену.

Парсер - делает работу по преобразованию набора токенов по определенным правилам. С помощью его и осуществляется основная работа. В случае компиляторов - преобразование в байт-код/объектный файл итд. Для интерпретаторов - можно сразу выполнять вычисления. При реализации кодогенератора - можно преобразовать в дерево классов.

Также парсер задается очень простым способом (пример из документации):

class CalcParser(Parser):    # Get the token list from the lexer (required)    tokens = CalcLexer.tokens    # Grammar rules and actions    @_('expr PLUS term')    def expr(self, p):        return p.expr + p.term    @_('expr MINUS term')    def expr(self, p):        return p.expr - p.term    @_('term')    def expr(self, p):        return p.term    @_('term TIMES factor')    def term(self, p):        return p.term * p.factor    @_('term DIVIDE factor')    def term(self, p):        return p.term / p.factor    @_('factor')    def term(self, p):        return p.factor    @_('NUMBER')    def factor(self, p):        return p.NUMBER    @_('LPAREN expr RPAREN')    def factor(self, p):        return p.expr

Каждый метод класса отвечает за парсинг конкретной конструкции. В декораторе @_ указывается правило, которое обрабатывается. Имя метода sly распознает как название правила.

В этом примере сразу происходят вычисления.

Подробнее можно прочитать в официальной документации: https://sly.readthedocs.io/en/latest/sly.html

4. Процесс создания

В самом начале программа получает yml файл с настройками. Затем при помощи sly преобразовывает код в древо классов. Далее выполняются вычисления и поиски объектов. После вычисления - передается в jinja2 шаблон и дерево символов.

Читать как был определен список токенов для лексера может быть скучно, поэтому перейдем сразу к парсеру.

Вначале определили, что модуль состоит из списка термов:

    @_('term term')    def term(self, p):        t0 = p.term0        t1 = p.term1        t0.extend(t1)        return t0

Затем определим, что терм состоит из определений структуры, энумератора или интерфейса разделенные символом ";"(SEPARATOR):

   @_('enum_def SEPARATOR')    def term(self, p):        return [p.enum_def]    @_('statement SEPARATOR')    def term(self, p):        return [p.statement]    @_('interface SEPARATOR')    def term(self, p):        return [p.interface]    @_('struct SEPARATOR')    def term(self, p):        return [p.struct]

Здесь терм сразу паковался в массив для удобства. Чтобы список термов (term term правило) работал уже сразу с листами и собрал в один лист.

Ниже представлен набор правил для описания структуры:

    @_('STRUCT NAME LBRACE struct_items RBRACE')    def struct(self, p):        return Struct(p.NAME, p.struct_items, lineno=p.lineno)    @_('decorator_item STRUCT NAME LBRACE struct_items RBRACE')    def struct(self, p):        return Struct(p.NAME, p.struct_items, lineno=p.lineno, tags=p.decorator_item)    @_('struct_items struct_items')    def struct_items(self, p):        si0 = p.struct_items0        si0.extend(p.struct_items1)        return si0    @_('type_def NAME SEPARATOR')    def struct_items(self, p):        return [StructField(p.type_def, p.NAME, lineno=p.lineno)]    @_('type_def NAME COLON NUMBER SEPARATOR')    def struct_items(self, p):        return [StructField(p.type_def, p.NAME, bitsize=p.NUMBER, lineno=p.lineno)]    @_('decorator_item type_def NAME SEPARATOR')    def struct_items(self, p):        return [StructField(p.type_def, p.NAME, lineno=p.lineno, tags=p.decorator_item)]    @_('decorator_item type_def NAME COLON NUMBER SEPARATOR')    def struct_items(self, p):        return [StructField(p.type_def, p.NAME, bitsize=p.NUMBER, lineno=p.lineno, tags=p.decorator_item)]

Если описать простым языком правила - структура (struct) содержит поля структур (struct_items). А поля структур могут определяться как:

  • тип (type_def), имя (NAME), разделитель (SEPARATOR)

  • тип (type_def), имя, двоеточие (COLON), число (NUMBER - для битфилда, означает количество бит), разделитель

  • список декораторов (decorator_item), тип, имя, разделитель

  • список декораторов, тип, имя, двоеточие (COLON), число (NUMBER - для битфилда), разделитель

Новшество относительно QFace (однако есть в protlr) - была введена возможность описывать специальные условные ссылки на структуры. Было решено назвать эту фичу - alias.

    @_('DECORATOR ALIAS NAME COLON expr struct SEPARATOR')    def term(self, p):        return [Alias(p.NAME, p.expr, p.struct), p.struct]

Это было сделано чтобы поддерживалась следующая конструкция:

enum Opcode {    Start =  0x00,    Stop = 0x01};@alias Payload: Opcode.Startstruct StartPayload {...};@alias Payload: Opcode.Stopstruct StopPayload {...};struct Message {    Opcode opcode: 8;    Payload<opcode> payload;};

Данная конструкция обозначает, что если opcode = Opcode.Start (0x00) - payload будет соответствовать структуре StartPayload. Если opcode = Opcode.Stop (0x01) - payload будет иметь структуру StopPayload. То есть создаем ссылку структуры с определенными условиями.

Следующее что было сделано - отказался от объявления модуля. Показалось это избыточным так как - имя файла уже содержит имя модуля, а версию писать бессмысленно так как есть git. Хороший протокол имеет прямую и обратную совместимость и в версии нуждаться не должен. Был выкинут тип flag так как есть enum, и добавил возможность описания битфилдов. Убрал возможность определения сигналов так как пока что низкоуровневого примера, демонстрирующего пользу, не было.

Была добавлена возможность python-подобных импортов. Чтобы можно было импортировать из другого модуля только конкретный символ. Это полезно для генерации документации.

Для вычислений был создан класс - Solvable. Его наследует каждый объект, которому есть что посчитать. Например, для SymbolType (тип поля класса или интерфейса). В данном классе этот метод ищет по ссылке тип, чтобы добавить его в поле reference. Чтобы в jinja можно было сразу на месте обратиться к полям enum или структуры. Класс Solvable должен искать во вложенных символах вычислимые и вызывать solve. Т.е. вычисления происходят рекурсивно.

Пример реализации метода solve для структуры:

    def solve(self, scopes: list):        scopes = scopes + [self]        for i in self.items:            if isinstance(i, Solvable):                i.solve(scopes=scopes)

Как видно, в методе solve есть аргумент - scopes. Этот аргумент отвечает за видимость символов. Пример использования:

struct SomeStruct {i32someNumber;@setter: someNumber;void setInteger(i32 integer);};

Как видно из примера - это позволяет производить поиск символа someNumber в области видимости структуры, вместо явного указания SomeStruct.someNumber.

Заключение

По сравнению с QFace мне удалось - упростить написание шаблона за счет поиска типов, вычисления перечислений. Также полезно иметь импорт символов и возможность условно ссылаться на разные структуры.

В папке examples/uart - находится пример генерации заголовков, кода и html документации. Пример иллюстрирует типичный uart протокол с применением новых фич. Подразумевается, что функции типа put_u32 итд - определит сам пользователь исходя из порядка байт и архитектуры MCU.

Ознакомиться подробнее с реализацией можно по ссылке: https://gitlab.com/volodyaleo/volk-idl

P.S.

Это моя первая статья на Хабр. Буду рад получить отзывы - интересна ли данная тематика или нет. Если у кого-то есть хорошие примеры кодо+доко-генераторов бинарных протоколов для Embedded, было бы интересно прочитать в комментариях. Или какая-то успешная практика внедрения похожих систем для описания бинарных протоколов.

В данном проекте я не обращал особого внимания на скорость работы. Некоторые вещи делал чтобы "быстрее решить задачу". Было важнее получить рабочий код, который можно уже пробовать применять к разным проектам.

Источник: habr.com
К списку статей
Опубликовано: 24.02.2021 08:04:01
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Python

Api

Программирование микроконтроллеров

Разработка для интернета вещей

Python3

Embedded

Codegen

Codegeneration

Idl

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru