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

Codegen

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

24.02.2021 08:04:01 | Автор: admin

Предисловие

Я при работе с микроконтроллерами часто сталкивался с бинарными протоколами. Особенно, когда есть несколько контроллеров. Или же используется 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, было бы интересно прочитать в комментариях. Или какая-то успешная практика внедрения похожих систем для описания бинарных протоколов.

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

Подробнее..

Перевод Go Как подключить внешнюю библиотеку и исключить ненужные зависимости

04.10.2020 04:14:12 | Автор: admin


TL;DR: Кодогенератор.


Представьте, что вы разрабатываете некоторую библиотеку, которая поддерживает различные драйвера для хранения данных: Postgresql, Scylla, JSON и какие-либо другие.


У каждого из этих драйверов есть внешние зависимости от других библиотек. Postgresql database/sql, github.com/lib/pq. Scylla github.com/scylladb/gocql или стандартный github.com/gocql/gocql. JSON github.com/mailru/easyjson или github.com/json-iterator/go, или encoding/json.


Обычный пользователь подключает вашу библиотеку и получает неявное включение всех этих внешних библиотек.


В большинстве случаев это нормально, но не идеально: иногда пользователю нужен только Postgres драйвер, но вместе с ним он неявно получает огромное количество кода из внешних библиотек, которые ему никогда не будут нужны.


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


Кодогенерация


Разбейте файлы на части


Разбейте файлы вашей библиотеки на логические части, которые реализуют каждую отдельную конкретную зависимость. Пример: sql.go, cql.go, json.go.


Это позволит вам сгенерировать для пользователя только ту часть вашей библиотеки, которую он запросит.


Упакуйте файлы


Упакуйте файлы через pkger или любое другое решение для хранения их в бинарном файле.


Для включения файлов через pkger в исходном коде просто нужно их использовать


    files := make([]string, 0)    if *cql {        files = append(files, pkger.Include("/cql.go"))    }    if *json {        files = append(files, pkger.Include("/json.go"))    }    if *sql {        files = append(files, pkger.Include("/sql.go"))    }

Далее запустите


pkger -o <путь к исходнику кодогенератора>

И после этого вы получите статический исходник с упакованными файлами, который будут включены в бинарный файл.


Удобно добавить генерацию в go generate


//go:generate pkger -o <путь к исходнику кодогенератора>

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


Напишите кодогенератор


Кодогенератор это обычная программа на Go, которая принимает аргументы для генерации и записывает исходные файлы для выбранных типов в проект пользователя.


    for i := range files {        _, filename := filepath.Split(files[i])        in, err := pkger.Open(files[i])        if err != nil {            panic(err)        }        src := []byte(fmt.Sprintf(genStr, strings.Join(os.Args[1:], " ")))        reader := bufio.NewReader(in)        for {            line, err := reader.ReadBytes('\n')            if strings.HasPrefix(string(line), "package ") {                line = []byte(fmt.Sprintf("package %s\n", *pkgName))            }            src = append(src, line...)            if err == io.EOF {                break            }            if err != nil {                panic(err)            }        }        if err := ioutil.WriteFile(filename, src, 0644); err != nil {            panic(err)        }        in.Close()    }

Разместите исходный код вашего кодогенератора в подпапке проекта вида cmd/<имя вашего проекта>. Это позволит пользователю получить бинарный файл кодогенератора с тем же названием, что и название вашей библиотеки.


Использование


Пользователь устанавливает кодогенератор


go get github.com/<ваш профиль>/<ваша библиотека>/<путь к кодогенератору>

И затем запускает кодогенератор в своём проекте


<имя бинарного файла вашего кодогенератора> -sql

Когда нужно обновить вашу библиотеку пользователь обновляет кодогенератор


go get -u github.com/<ваш профиль>/<ваша библиотека>/<путь к кодогенератору>

И затем запускает генерацию заново.


Пример данного подхода к использованию зависимостей я и creker реализовали в библиотеке nan: с библиотекой можно работать как в обычном режиме, через import, так и в варианте кодогенерации.

Подробнее..
Категории: Go , Codegen

Категории

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

  • Имя: Макс
    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