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

Аврора ос

Как писать кодогенераторы в Go

03.06.2021 16:16:28 | Автор: admin

Однообразный код писать неинтересно, нудно, но приходится. Испокон веков изворотливые программисты ищут Святой Грааль формализма, позволяющего переложить рутинные задачи на машину, писать только раз и переиспользовать код. Так появились структурное программирование, потом объектно-ориентированное, полиморфизм с параметризованными типами, кодогенерация на основе формальных грамматик, препроцессоры макроязыка и прочее Под катом рассмотрим, как обстоят дела именно в Go.


В Go на сегодня generics нет (хоть третий год и обещают), а выписывать по шаблону GetMax([]MyType) для каждого MyType надоедает.

Параметрический полиморфизм можно реализовать генерацией частных форм обобщённого кода на стадии компиляции (или выполнения) и поддержкой таблиц соответствия на стадии выполнения. В Go поддерживаются таблицы методов для типов и интерфейсов и диспетчеризация этих таблиц просто, зато эффективно реализовано.

Runtime-доступ к диспетчеру предоставлен пакетом reflect, что обеспечивает сильный, но дорогостоящий механизм интроспекции, позволяющий динамически оперировать статически заявленными типами. Вызовы reflect затратны, но, например, в C нет и этого, там на стадии выполнения данные не знают, какого они типа.

Стандартного препроцессора в Go тоже нет. Зато есть директива go:generate и есть доступ к потрохам компилятора, в частности к дереву разбора (Abstract Syntax Tree), в пакетах go/ стандартной библиотеки. Это в совокупности даёт инструментарий богаче, чем препроцессор макросов.

Идиоматическое применение интерфейсов реализовано в stdlib-пакете sort, интроспекция применяется в пакетах encoding и fmt, go:generate в придворном пакете golang.org/x/tools/cmd/stringer.

Манипулирование AST исходного кода не очень распространено, потому что:

  • кодогенерацию трудно верифицировать;

  • дерево разбора кажется сложным, непонятным и пугает.

Как раз на использовании AST в быту мы и остановимся.

Go- и JS-разработчик Открытой мобильной платформы Дима Смотров рассказал, как писать кодогенераторы в Go и оптимизировать работу над микросервисами с помощью создания инструмента для генерации шаблонного кода.Статья составлена на основе выступления Димы на GopherCon Russia 2020.

О продуктах и компонентах на Go

Наша команда разрабатывает мобильную ОС Аврора, SDK и экосистему приложений под неё, доверенную среду исполнения Аврора ТЕЕ, систему по управлению корпоративной мобильной инфраструктурой Аврора Центр, включающую несколько коробочных продуктов и компонентов.

Группа Дмитрия, в частности, работает над продуктом Аврора Маркет, который обеспечивает управление дистрибуцией приложений. Его бэкенд полностью написан на Go.

В Go принято отдавать предпочтение явному программированию (explicit) в противовес неявному (implicit). Это помогает новым разработчикам легче начинать работать над существующими проектами. Но по пути от неявного программирования к явному можно легко заблудиться и забрести в дебри дубляжа кода, а дубляж кода в дальнейшем превратит поддержку проекта в ад.

Чтобы этого избежать, код выносят в отдельные модули. Но как насчёт кода, который пишется специально для каждого микросервиса и не может быть вынесен в модуль? Например, код репозитория для работы с базой данных. Этот код есть в каждом микросервисе, выглядит примерно одинаково, но он разный и не дублируется. Не хочется писать шаблонный код, который потом придётся ещё и поддерживать во всех микросервисах.

Кодогенерация официальный инструмент от авторов Go

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

И хотя в Go принято отдавать предпочтение явному программированию, разработчики предоставили инструменты для метапрограммирования, такие как кодогенерация ($go help generate) и Reflection API. Reflection API используется на этапе выполнения программы, кодогенерация перед этапом компиляции. Reflection API увеличивает время работы программы. Пример: инструмент для кодирования и декодирования JSON из стандартной библиотеки Go использует Reflection API. Взамен ему сообществом были рождены такие альтернативы, как easyjson, который с помощью кодогенерации кодирует и декодирует JSON в 5 раз быстрее.

Так как кодогенерация неявное программирование, она недооценивается сообществом Go, хотя и является официальным инструментом от создателей этого языка программирования. Поэтому в интернете немного информации о написании кодогенераторов на Go. Но всё же на Хабре примеры есть: 1 и 2.

При разработке микросервисов есть много похожего шаблонного кода, который нужно писать в каждом микросервисе. Например, код репозитория по работе с базой данных. Мы создали кодогенераторы для того, чтобы разработчики не тратили время на написание этого шаблонного кода и могли сфокусироваться на решении задач, относящихся к дизайну кода и предметной области бизнеса. Команда использует кодогенераторы и для сокращения времени на создание новых микросервисов. Это позволяет не ограничивать разработчика в принятии архитектурных решений, так как создание нового микросервиса не влияет на трудоёмкость выполнения задачи.

Пример дублирующего кода:

type UserRepository struct{ db *gorm.DB }func NewRepository(db *gorm.DB) UserRepository {    return UserRepository{db: db}}func (r UserRepository) Get(userID uint) (*User, error) {    entity := new(User)    err := r.db.Limit(limit: 1).Where(query: "user_id = ?", userID).Find(entity).Error    return entity, err}func (r UserRepository) Create(entity *User) error {    return r.db.Create(entity).Error}func (r UserRepository) Update(entity *User) error {    return r.db.Model(entity).Update(entity).Error}func (r UserRepository) Delete(entity *User) error {    return r.db.Delete(entity).Error}

Про удачные кодогенераторы

Из примеров написанных и удачно используемых в нашей команде кодогенераторов хотим подробнее рассмотреть генератор репозитория по работе с базой данных. Нам нравится переносить опыт из одного языка программирования в другой. Так, наша команда попыталась перенести идею генерации репозиториев по работе с базой данных из Java Spring (https://spring.io/).

В Java Spring разработчик описывает интерфейс репозитория, исходя из сигнатуры метода автоматически генерируется реализация в зависимости от того, какой бэкенд для базы данных используется: MySQL, PostgreSQL или MongoDB. Например, для метода интерфейса с сигнатурой FindTop10WhereNameStartsWith (prefix string) автоматически генерируется реализация метода репозитория, которая вернёт до 10 записей из базы данных, имя которых начинается с переданного в аргументе префикса.

О нюансах и траблах внедрения кодогенератора

Существует парадигма Monolith First, когда пишут первую версию как монолит, а потом распиливают на микросервисы. На заре новой версии проекта, когда все команды должны были разбить монолит на микросервисы, мы решили написать свой генератор, который:

  • позволит вводить в систему новые микросервисы с меньшими усилиями, чем при его создании вручную (копируя предыдущий и удаляя лишнее);

  • сократит время на код-ревью за счёт общего шаблона для генерируемых микросервисов;

  • сократит время на будущие обновления одинакового кода микросервисов (main, инфрастуктура, etc).

Для разработки микросервисов командами было принято решение использовать go-kit. За основу мы взяли один из популярных существующих кодогенераторов для go-kit и стали его дорабатывать под наши требования для микросервисов. Он был написан с использованием не очень удобной библиотеки, которая использовала промежуточные абстракции для генерации кода Go. Код получался громоздким и трудным для восприятия и поддержки. В будущих версиях мы отказались от такого подхода и начали генерировать код Go с помощью шаблонов Go. Это позволило нам писать тот же самый код без каких-либо промежуточных абстракций. За пару недель нашей командой был написан прототип. А ещё через месяц был написан кодогенератор для go-kit, который буквально умел делать всё.

Разработчик описывает интерфейс go-kit-сервиса, а кодогенератор генерирует сразу всё, что для сервиса нужно:

  • CRUD-эндпоинты и REST-, gRPC- и NATS-транспорты;

  • репозиторий для работы с базой данных с возможностью расширять интерфейс репозитория;

  • main для всех go-kit-сервисов.

После того как кодогенератор был закончен, началось его внедрение. Мы сразу же столкнулись с проблемами. Разработчики компании неохотно принимали кодогенератор. Он генерировал слишком много кода, который нужно было ревьюить и перерабатывать. Сначала команды помогали исправлять генерируемый код, но подошло время уже разрабатывать сами микросервисы, а не кодогенератор для генерации сервисов. В итоге одни команды продолжали использовать кодогенератор, обходя его баги, а другие начали писать микросервисы без кодогенератора. Мы получили сегментацию сервисов. Когда инфраструктура менялась, изменения давались нам очень тяжело.

Позже наша команда работала над проектом, где нужно было больше заниматься адаптацией существующих Open-Source-продуктов под наши требования, чем разработкой новых микросервисов. Мы больше не могли заниматься кодогенератором, так как не использовали его активно в работе. А впоследствии мы тоже начали копировать сервисы вместе со всеми их недостатками.

Когда наша команда вернулась к разработке своего основного продукта, мы провели ретроспективу кодогенератора и поняли, в чём была основная проблема. Кодогенераторы, которые генерируют ВСЁ, сложно внедрять и поддерживать.

  • Кодогенератор генерировал слишком много кода.

  • Весь код нужно было ревьювить и перерабатывать.

  • Только часть команд решила пользоваться кодогенератором.

  • Получили сегментацию микросервисов.

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

Как же всё-таки генерировать Go-код

Можно просто использовать шаблоны. Можно написать шаблон и начинить его параметрами, на это вполне способны продвинутые редакторы текста. Можно использовать неинтерактивные редакторы sed или awk, порог входа круче, зато лучше поддаётся автоматизации и встраивается в производственный конвейер. Можно использовать специфические инструменты рефакторинга Go из пакета golang.org/x/tools/cmd, а именно gorename или eg. А можно воспользоваться пакетом text/template из стандартной библиотеки решение достаточно гибкое, человекочитаемое (в отличие от sed), удобно интегрируется в pipeline и позволяет оставаться в среде одного языка.

И всё же для конвейерной обработки этого маловато: требует существенного вмешательства оператора.

Можно пойти по проторённому пути: gRPC, Protobuf, Swagger. Недостатки подхода:

  • привязывает к gRPC, Protobuf;

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

Чтобы остаться в родных пенатах воспользуемся средствами из стандартной библиотеки пакетами go/:

  • go/ast декларирует типы дерева разбора;

  • go/parser разбирает исходный код в эти типы;

  • go/printer выливает AST в файл исходного кода;

  • go/token обеспечивает привязку дерева разбора к файлу исходного кода.

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

Можно вытащить из AST параметры, вмонтировать в шаблон и всё. Недостаток разрозненные обработки неудобно собирать в конвейер.

Поэтому выбран такой алгоритм кодогенерации:

  1. Разбираем AST исходного файла.

  2. Создаём пустое AST для генерируемого файла.

  3. Генерируем код из шаблонов Go (template/text).

  4. Разбираем AST сгенерированного кода.

  5. Копируем узлы AST из сгенерированного кода в AST генерируемого файла.

  6. Печатаем и сохраняем AST генерируемого файла в файл.

Чтобы было понятней и не пугала загадочная аббревиатура AST дерево разбора Hello World:

package mainimport "fmt"func main() {    fmt.Println("Hello, World!")}

...выглядит вот так:

...или вот так, напечатанное специализированным принтером ast.Print():

ast.Print
0  *ast.File {1  .  Package: 2:12  .  Name: *ast.Ident {3  .  .  NamePos: 2:94  .  .  Name: "main"5  .  }6  .  Decls: []ast.Decl (len = 2) {7  .  .  0: *ast.GenDecl {8  .  .  .  TokPos: 4:19  .  .  .  Tok: import10  .  .  .  Lparen: -11  .  .  .  Specs: []ast.Spec (len = 1) {12  .  .  .  .  0: *ast.ImportSpec {13  .  .  .  .  .  Path: *ast.BasicLit {14  .  .  .  .  .  .  ValuePos: 4:815  .  .  .  .  .  .  Kind: STRING16  .  .  .  .  .  .  Value: "\"fmt\""17  .  .  .  .  .  }18  .  .  .  .  .  EndPos: -19  .  .  .  .  }20  .  .  .  }21  .  .  .  Rparen: -22  .  .  }23  .  .  1: *ast.FuncDecl {24  .  .  .  Name: *ast.Ident {25  .  .  .  .  NamePos: 6:626  .  .  .  .  Name: "main"27  .  .  .  .  Obj: *ast.Object {28  .  .  .  .  .  Kind: func29  .  .  .  .  .  Name: "main"30  .  .  .  .  .  Decl: *(obj @ 23)31  .  .  .  .  }32  .  .  .  }33  .  .  .  Type: *ast.FuncType {34  .  .  .  .  Func: 6:135  .  .  .  .  Params: *ast.FieldList {36  .  .  .  .  .  Opening: 6:1037  .  .  .  .  .  Closing: 6:1138  .  .  .  .  }39  .  .  .  }40  .  .  .  Body: *ast.BlockStmt {41  .  .  .  .  Lbrace: 6:1342  .  .  .  .  List: []ast.Stmt (len = 1) {43  .  .  .  .  .  0: *ast.ExprStmt {44  .  .  .  .  .  .  X: *ast.CallExpr {45  .  .  .  .  .  .  .  Fun: *ast.SelectorExpr {46  .  .  .  .  .  .  .  .  X: *ast.Ident {47  .  .  .  .  .  .  .  .  .  NamePos: 7:248  .  .  .  .  .  .  .  .  .  Name: "fmt"49  .  .  .  .  .  .  .  .  }50  .  .  .  .  .  .  .  .  Sel: *ast.Ident {51  .  .  .  .  .  .  .  .  .  NamePos: 7:652  .  .  .  .  .  .  .  .  .  Name: "Println"53  .  .  .  .  .  .  .  .  }54  .  .  .  .  .  .  .  }55  .  .  .  .  .  .  .  Lparen: 7:1356  .  .  .  .  .  .  .  Args: []ast.Expr (len = 1) {57  .  .  .  .  .  .  .  .  0: *ast.BasicLit {58  .  .  .  .  .  .  .  .  .  ValuePos: 7:1459  .  .  .  .  .  .  .  .  .  Kind: STRING60  .  .  .  .  .  .  .  .  .  Value: "\"Hello, World!\""61  .  .  .  .  .  .  .  .  }62  .  .  .  .  .  .  .  }63  .  .  .  .  .  .  .  Ellipsis: -64  .  .  .  .  .  .  .  Rparen: 7:2965  .  .  .  .  .  .  }66  .  .  .  .  .  }67  .  .  .  .  }68  .  .  .  .  Rbrace: 8:169  .  .  .  }70  .  .  }71  .  }72  .  Scope: *ast.Scope {73  .  .  Objects: map[string]*ast.Object (len = 1) {74  .  .  .  "main": *(obj @ 27)75  .  .  }76  .  }77  .  Imports: []*ast.ImportSpec (len = 1) {78  .  .  0: *(obj @ 12)79  .  }80  .  Unresolved: []*ast.Ident (len = 1) {81  .  .  0: *(obj @ 46)82  .  }83  }

Хватит трепаться, покажите код

В целом задача сводится к тому, чтобы разработчик микросервиса мог описать свои данные обычным образом в стандартном синтаксисе Go, только добавить в комментариях директивы процессинга:

//repogen:entitytype User struct {    ID              uint `gorm:"primary_key"`    Email           string    PasswordHash    string}

...запустить go generate и получить вот такой файл с готовой обвязкой для работы с DB, в котором прописаны методы именно для его типа данных User:

User
type UserRepository struct{db *gorm.DB}func NewRepository(db *gorm.DB) UserRepository {    return UserRepository{db: db}}func (r UserRepository) Get(userID uint) (*User, error) {    entity := new(User)    err := r.db.Limit(limit: 1).Where(query: "user_id = ?", userID).Find(entity).Error    return entity, err}func (r UserRepository) Create(entity *User) error {    return r.db.Create(entity).Error}func (r UserRepository) Update(entity *User) error {    return r.db.Model(entity).Update(entity).Error}func (r UserRepository) Delete(entity *User) error {    return r.db.Delete(entity).Error}

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

Кода потребовалось не очень много, поэтому он представлен одним листингом, чтобы не терялась общая картина. Пояснения даны в комментариях, в стиле literate programming.

Вот модель, для которой нам нужно сгенерировать методы работы с DB. В комментариях видны директивы:

  • go:generate repogen для команды go generate на запуск процессора repogen;

  • repogen:entity помечает цель для процессора repogen;

  • и тег поля структуры gorm:"primary_key" для процессора gorm помечает первичный ключ в таблице DB.

package gophercon2020//go:generate repogen//repogen:entitytype User struct {    ID              uint `gorm:"primary_key"`    Email           string    PasswordHash    string}

Вот код, собственно, процессора repogen:

Процессор repogen
package mainimport (    "bytes"    "go/ast"    "go/parser"    "go/printer"    "go/token"    "golang.org/x/tools/go/ast/inspector"    "log"    "os"    "text/template")//Шаблон, на основе которого будем генерировать//.EntityName, .PrimaryType  параметры,//в которые будут установлены данные, добытые из AST-моделиvar repositoryTemplate = template.Must(template.New("").Parse(`package mainimport (    "github.com/jinzhu/gorm")type {{ .EntityName }}Repository struct {    db *gorm.DB}func New{{ .EntityName }}Repository(db *gorm.DB) {{ .EntityName }}Repository {    return {{ .EntityName }}Repository{ db: db}}func (r {{ .EntityName }}Repository) Get({{ .PrimaryName }} {{ .PrimaryType}}) (*{{ .EntityName }}, error) {    entity := new({{ .EntityName }})    err := r.db.Limit(1).Where("{{ .PrimarySQLName }} = ?", {{ .PrimaryName }}).Find(entity).Error()    return entity, err}func (r {{ .EntityName }}Repository) Create(entity *{{ .EntityName }}) error {    return r.db.Create(entity).Error}func (r {{ .EntityName }}Repository) Update(entity *{{ .EntityName }}) error {    return r.db.Model(entity).Update.Error}func (r {{ .EntityName }}Repository) Update(entity *{{ .EntityName }}) error {    return r.db.Model(entity).Update.Error}func (r {{ .EntityName }}Repository) Delete(entity *{{ .EntityName }}) error {    return r.db.Delete.Error}`))//Агрегатор данных для установки параметров в шаблонеtype repositoryGenerator struct{    typeSpec    *ast.TypeSpec    structType  *ast.StructType}//Просто helper-функция для печати замысловатого ast.Expr в обычный stringfunc expr2string(expr ast.Expr) string {    var buf bytes.Buffer    err := printer.Fprint(&buf, token.NewFileSet(), expr)    if err !- nil {        log.Fatalf("error print expression to string: #{err}")    return buf.String()}//Helper для извлечения поля структуры,//которое станет первичным ключом в таблице DB//Поиск поля ведётся по тегам//Ищем то, что мы пометили gorm:"primary_key"func (r repositoryGenerator) primaryField() (*ast.Field, error) {    for _, field := range r.structType.Fields.List {        if !strings.Contains(field.Tag.Value, "primary")            continue        }        return field, nil    }    return nil, fmt.Errorf("has no primary field")}//Собственно, генератор//оформлен методом структуры repositoryGenerator,//так что параметры передавать не нужно://они уже аккумулированы в ресивере метода r repositoryGenerator//Передаём ссылку на ast.File,//в котором и окажутся плоды трудовfunc (r repositoryGenerator) Generate(outFile *ast.File) error {    //Находим первичный ключ    primary, err := r.primaryField()    if err != nil {        return err    }    //Аллокация и установка параметров для template    params := struct {        EntityName      string        PrimaryName     string        PrimarySQLName  string        PrimaryType     string    }{        //Параметры извлекаем из ресивера метода        EntityName      r.typeSpec.Name.Name,        PrimaryName     primary.Names[0].Name,        PrimarySQLName  primary.Names[0].Name,        PrimaryType     expr2string(primary.Type),    }    //Аллокация буфера,    //куда будем заливать выполненный шаблон    var buf bytes.Buffer    //Процессинг шаблона с подготовленными параметрами    //в подготовленный буфер    err = repositoryTemplate.Execute(&buf, params)    if err != nil {        return fmt.Errorf("execute template: %v", err)    }    //Теперь сделаем парсинг обработанного шаблона,    //который уже стал валидным кодом Go,    //в дерево разбора,    //получаем AST этого кода    templateAst, err := parser.ParseFile(        token.NewFileSet(),        //Источник для парсинга лежит не в файле,        "",        //а в буфере        buf.Bytes(),        //mode парсинга, нас интересуют в основном комментарии        parser.ParseComments,    )    if err != nil {        return fmt.Errorf("parse template: %v", err)    }    //Добавляем декларации из полученного дерева    //в результирующий outFile *ast.File,    //переданный нам аргументом    for _, decl := range templateAst.Decls {        outFile.Decls = append(outFile.Decls, decl)    }    return nil}func main() {    //Цель генерации передаётся переменной окружения    path := os.Getenv("GOFILE")    if path == "" {        log.Fatal("GOFILE must be set")    }    //Разбираем целевой файл в AST    astInFile, err := parser.ParseFile(        token.NewFileSet(),        path,        src: nil,        //Нас интересуют комментарии        parser.ParseComments,    )    if err != nil {        log.Fatalf("parse file: %v", err)    }    //Для выбора интересных нам деклараций    //используем Inspector из golang.org/x/tools/go/ast/inspector    i := inspector.New([]*ast.File{astInFile})    //Подготовим фильтр для этого инспектора    iFilter := []ast.Node{        //Нас интересуют декларации        &ast.GenDecl{},    }    //Выделяем список заданий генерации    var genTasks []repositoryGenerator    //Запускаем инспектор с подготовленным фильтром    //и литералом фильтрующей функции    i.Nodes(iFilter, func(node ast.Node, push bool) (proceed bool){        genDecl := node.(*ast.GenDecl)        //Код без комментариев не нужен,        if genDecl.Doc == nil {            return false        }        //интересуют спецификации типов,        typeSpec, ok := genDecl.Specs[0].(*ast.TypeSpec)        if !ok {            return false        }        //а конкретно структуры        structType, ok := typeSpec.Type.(*ast.StructType)        if !ok {            return false        }        //Из оставшегося        for _, comment := range genDecl.Doc.List {            switch comment.Text {            //выделяем структуры, помеченные комментарием repogen:entity,            case "//repogen:entity":                //и добавляем в список заданий генерации                genTasks = append(genTasks, repositoryGenerator{                    typeSpec: typeSpec,                    structType: structType,                })            }        }        return false    })    //Аллокация результирующего дерева разбора    astOutFile := &ast.File{        Name: astInFile.Name,    }    //Запускаем список заданий генерации    for _, task := range genTask {        //Для каждого задания вызываем написанный нами генератор        //как метод этого задания        //Сгенерированные декларации помещаются в результирующее дерево разбора        err = task.Generate(astOutFile)        if err != nil {            log.Fatalf("generate: %v", err)        }    }    //Подготовим файл конечного результата всей работы,    //назовем его созвучно файлу модели, добавим только суффикс _gen    outFile, err := os.Create(strings.TrimSuffix(path, ".go") + "_gen.go")    if err != nil {        log.Fatalf("create file: %v", err)    }    //Не забываем прибраться    defer outFile.Close()    //Печатаем результирующий AST в результирующий файл исходного кода    //Печатаем не следует понимать буквально,    //дерево разбора нельзя просто переписать в файл исходного кода,    //это совершенно разные форматы    //Мы здесь воспользуемся специализированным принтером из пакета ast/printer    err = printer.Fprint(outFile, token.NewFileSet(), astOutFile)    if err != nil {        log.Fatalf("print file: %v", err)    }}

Подводя итоги

Работа с деревом разбора в Go не требует сверхъестественных способностей. Язык предоставляет для этого вполне годный инструментарий. Кода получилось не слишком много, и он достаточно читаем и, надеемся, понятен. Высокой эффективности здесь добиваться нет нужды, потому что всё происходит ещё до стадии компиляции и на стадии выполнения издержек не добавляет (в отличие от reflect). Важнее валидность генерации и манипуляций с AST. Кодогенерация сэкономила нам достаточно времени и сил в написании и поддержке большого массива кода, состоящего из повторяющихся паттернов (микросервисов). В целом кодогенераторы оправдали затраты на своё изготовление. Выбранный pipeline показал себя работоспособным и прижился в производственном процессе. Из стороннего опыта можем рекомендовать к использованию:

  • dst (у которого лучше разрешение импортируемых пакетов и привязка комментариев к узлам AST, чем у go/ast из stdlib).

  • kit (хороший toolkit для быстрой разработки в архитектуре микросервисов. Предлагает внятные, рациональные абстракции, методики и инструменты).

  • jennifer (полноценный кодогенератор. Но его функциональность достигнута ценой применения промежуточных абстракций, которые хлопотно обслуживать. Генерация из шаблонов text/template на деле оказалась удобней, хоть и менее универсальной, чем манипулирование непосредственно AST с использованием промежуточных абстракций. Писать, читать и править шаблоны проще).

Набор узконаправленных генераторов оказался сподручней одного универсального и в применении, и в поддержке. Здесь допустима аналогия микросервисы vs монолит. Делитесь опытом и мнениями в комментариях.

Подробнее..

Запускаем Rust-приложение на мобильной ОС Аврора

02.03.2021 10:09:04 | Автор: admin

Всем привет! Меня зовут Шамиль, я ведущий инженер-разработчик в КРОК. Помимо всего прочего мы в компании занимаемся ещё и разработкой мобильных приложений для операционной системы Аврора, есть даже центр компетенций по ней.

Для промышленной разработки мы, конечно же, пока используем связку C++ и QML, но однажды подсев на "ржавую" иглу Rust, я не мог не попробовать применить свой любимый язык программирования для написания мобильных приложений. В этой статье я опишу эксперимент по написанию простейшего приложения на Rust, предназначенного для запуска на мобильном устройстве под управлением вышеупомянутой ОС. Сразу оговорюсь, что легких путей я не искал эксперименты проводил на сертифицированной версии Авроры, которая добавила огонька в этот процесс. Но, как говорится, только защищённая ОС, только хардкор.

Пара выходных у меня ушла только на то, чтобы запустить минимальное консольное приложение (речь о нём пойдёт в первой части), ещё пара дней на эксперименты с графическим интерфейсом, выбор оптимального подхода и запуск приложения с GUI (этому посвящена вторая часть повествования). В итоге получился минимальный скелет мобильного приложения, готового к сборке и запуску, на который при желании уже можно наращивать мясо.

Готовим окружение

Итак, работа будет вестись из-под Ubuntu Linux с уже установленным Rust. В качестве подопытного планшета выступает Aquarius NS220 с сертифицированной ОС Аврора последней (на момент написания статьи) версии 3.2.2 с включённым режимом разработчика, который обеспечивает связь по SSH, а также привилегированный доступ с правами суперпользователя.

Первым делом добавим средства кросскомпилятора для архитектуры ARM, так как на целевом планшете стоит именно такой процессор.

sudo apt install -y g++-arm-linux-gnueabihfrustup target add armv7-unknown-linux-gnueabihf

В сертифицированной версии ОС Аврора не разрешается запускать неподписанные приложения. Подписывать надо проприетарной утилитой из состава Aurora Certified SDK под названием ompcert-cli, которая поддерживает на входе только пакет в формате RPM. Поэтому сразу установим замечательную утилиту cargo-rpm, которая возьмёт на себя всю рутинную работу по упаковке приложения в RPM-пакет:

cargo install cargo-rpm

Саму процедуру подписывания RPM-пакета я описывать не буду, она неплохо документирована в справочных материалах ОС Аврора.

Aurora SDK можно скачать с сайта производителя.

Часть 1. Hello. World

TL;DR Исходники проекта можно найти в репозитории на Гитхабе.

Создаем минимальный проект

Создаём пустое приложение на Rust:

cargo new aurora-rust-helloworld

Пытаемся сгенерировать .spec файл для RPM-пакета:

cargo rpm init

Получаем ошибки, что не хватает некоторых полей в Cargo.toml, добавляем их:

Cargo.toml:

[package]name = "aurora-rust-helloworld"version = "0.1.0"authors = ["Shamil Yakupov <syakupov@croc.ru>"]edition = "2018"description = "Rust example for Aurora OS"license = "MIT"

Закидываем в папку .cargo конфигурационный файл с указанием правильного линкера для компоновки исполняемого файла под архитектуру ARM:

.cargo/config.toml:

[target.armv7-unknown-linux-gnueabihf]linker = "arm-linux-gnueabihf-gcc"

Собираем RPM-пакет:

cargo rpm initcargo rpm build -v --target=armv7-unknown-linux-gnueabihf

Всё собралось, забираем RPM из папки target/armv7-unknown-linux-gnueabihf/release/rpmbuild/RPMS/armv7hl, подписываем его, копируем на планшет и пытаемся установить:

$ devel-suPassword:# pkcon install-local ./aurora-rust-helloworld-0.1.0-1.armv7hl.rpm

Получаем ошибку:

Fatal error: nothing provides libc.so.6(GLIBC_2.32) needed by aurora-rust-helloworld-0.1.0-1.armv7hl

Смотрим версию glibc на устройстве, и понимаем, что она явно ниже той, что нам требуется:

$ ldd --versionldd (GNU libc) 2.28

Что ж, тогда попробуем забрать нужные библиотеки с планшета, закинуть их в директорию lib и слинковать с ними. Для верности будем пользоваться линкером, входящим в состав Aurora SDK, который закинем в директорию bin. Для начала посмотрим, какие именно библиотеки нам нужны. Меняем содержимое .cargo/config.toml:

[target.armv7-unknown-linux-gnueabihf]rustflags = ["-C", "link-args=-L lib"]linker = "bin/armv7hl-meego-linux-gnueabi-ld"

Пробуем собрать:

cargo build --release --target=armv7-unknown-linux-gnueabihf

Получаем ошибки:

aurora-rust-helloworld/bin/armv7hl-meego-linux-gnueabi-ld: cannot find -lgcc_saurora-rust-helloworld/bin/armv7hl-meego-linux-gnueabi-ld: cannot find -lutilaurora-rust-helloworld/bin/armv7hl-meego-linux-gnueabi-ld: cannot find -lrtaurora-rust-helloworld/bin/armv7hl-meego-linux-gnueabi-ld: cannot find -lpthreadaurora-rust-helloworld/bin/armv7hl-meego-linux-gnueabi-ld: cannot find -lmaurora-rust-helloworld/bin/armv7hl-meego-linux-gnueabi-ld: cannot find -ldlaurora-rust-helloworld/bin/armv7hl-meego-linux-gnueabi-ld: cannot find -lcaurora-rust-helloworld/bin/armv7hl-meego-linux-gnueabi-ld: cannot find -lutil

Копируем недостающие библиотеки с планшета:

mkdir -p libscp nemo@192.168.2.15:/usr/lib/libgcc_s.so ./libscp nemo@192.168.2.15:/usr/lib/libutil.so ./libscp nemo@192.168.2.15:/usr/lib/librt.so ./libscp nemo@192.168.2.15:/usr/lib/libpthread.so ./libscp nemo@192.168.2.15:/usr/lib/libm.so ./libscp nemo@192.168.2.15:/usr/lib/libdl.so ./libscp nemo@192.168.2.15:/usr/lib/libc.so ./libscp nemo@192.168.2.15:/usr/lib/libutil.so ./lib

Снова пытаемся собрать, получаем новую порцию ошибок:

aurora-rust-helloworld/bin/armv7hl-meego-linux-gnueabi-ld: skipping incompatible /lib/libc.so.6 when searching for /lib/libc.so.6aurora-rust-helloworld/bin/armv7hl-meego-linux-gnueabi-ld: cannot find /lib/libc.so.6aurora-rust-helloworld/bin/armv7hl-meego-linux-gnueabi-ld: skipping incompatible /usr/lib/libc_nonshared.a when searching for /usr/lib/libc_nonshared.aaurora-rust-helloworld/bin/armv7hl-meego-linux-gnueabi-ld: cannot find /usr/lib/libc_nonshared.aaurora-rust-helloworld/bin/armv7hl-meego-linux-gnueabi-ld: cannot find /lib/ld-linux-armhf.so.3

Копируем недостающее:

scp nemo@192.168.2.15:/lib/libc.so.6 ./libscp nemo@192.168.2.15:/usr/lib/libc_nonshared.a ./libscp nemo@192.168.2.15:/lib/ld-linux-armhf.so.3 ./lib

Ещё надо подредактировать файл libc.so (который является фактически скриптом линкера), чтобы дать понять линкеру, где надо искать библиотеки:

lib/libc.so:

/* GNU ld script   Use the shared library, but some functions are only in   the static library, so try that secondarily.  */OUTPUT_FORMAT(elf32-littlearm)GROUP ( libc.so.6 libc_nonshared.a  AS_NEEDED ( ld-linux-armhf.so.3 ) )

Запускаем сборку RPM-пакета, копируем, пытаемся установить.

Здесь позволю себе небольшое лирическое отступление. Перед установкой RPM-пакета на сертифицированной версии ОС Аврора запускается RPM-валидатор утилита, которая проверяет, насколько собранный пакет удовлетворяет требованиям системы. И до тех пор, пока пакет не пройдёт валидацию, установить приложение не получится. Безопасность превыше всего.

Итак, мы видим, что валидатор выдал несколько ошибок:

вот таких
Desktop file============ERROR [/usr/share/applications/aurora-rust-helloworld.desktop] File is missing - cannot validate .desktop filePaths=====WARNING [/usr/share/aurora-rust-helloworld] Directory not foundERROR [/usr/share/applications/aurora-rust-helloworld.desktop] File not foundWARNING [/usr/share/icons/hicolor/86x86/apps/aurora-rust-helloworld.png] File not foundWARNING [/usr/share/icons/hicolor/108x108/apps/aurora-rust-helloworld.png] File not foundWARNING [/usr/share/icons/hicolor/128x128/apps/aurora-rust-helloworld.png] File not foundWARNING [/usr/share/icons/hicolor/172x172/apps/aurora-rust-helloworld.png] File not foundERROR [/usr/share/icons/hicolor/[0-9x]{5,9}/apps/aurora-rust-helloworld.png] No icons found! RPM must contain at least one icon, see: https://community.omprussia.ru/doc/software_development/guidelines/rpm_requirementsLibraries=========ERROR [/usr/bin/aurora-rust-helloworld] Cannot link to shared library: libutil.so.1Symbols=======ERROR [/usr/bin/aurora-rust-helloworld] Binary does not link to 9__libc_start_main@GLIBC_2.4.Requires========ERROR [libutil.so.1] Cannot require shared library: 'libutil.so.1'

Что ж, будем бороться с каждой ошибкой по списку.

Добавляем недостающие файлы

Добавим иконки и ярлык (файл с расширением desktop) в директорию .rpm.

.rpm/aurora-rust-helloworld.desktop:

[Desktop Entry]Type=ApplicationX-Nemo-Application-Type=silica-qt5Icon=aurora-rust-helloworldExec=aurora-rust-helloworldName=Rust Hello-World

Для того, чтобы копировать нужные файлы на этапе сборки RPM-пакета, сделаем простенький Makefile:

Makefile
.PHONY: all clean install prepare release rpmall:@cargo build --target=armv7-unknown-linux-gnueabihfclean:@rm -rvf targetinstall:@scp ./target/armv7-unknown-linux-gnueabihf/release/aurora-rust-helloworld nemo@192.168.2.15:/home/nemo/@scp ./target/armv7-unknown-linux-gnueabihf/release/rpmbuild/RPMS/armv7hl/*.rpm nemo@192.168.2.15:/home/nemo/prepare:@rustup target add armv7-unknown-linux-gnueabihf@cargo install cargo-rpmrelease:@cargo build --release --target=armv7-unknown-linux-gnueabihfrpm:@mkdir -p ./target/armv7-unknown-linux-gnueabihf/release/rpmbuild/SOURCES@cp -vf .rpm/aurora-rust-helloworld.desktop ./target/armv7-unknown-linux-gnueabihf/release/rpmbuild/SOURCES@cp -rvf .rpm/icons ./target/armv7-unknown-linux-gnueabihf/release/rpmbuild/SOURCES@cargo rpm build -v --target=armv7-unknown-linux-gnueabihf

Обновим aurora-rust-helloworld.spec:

.rpm/aurora-rust-helloworld.spec
%define __spec_install_post %{nil}%define __os_install_post %{_dbpath}/brp-compress%define debug_package %{nil}Name: aurora-rust-helloworldSummary: Rust example for Aurora OSVersion: @@VERSION@@Release: @@RELEASE@@%{?dist}License: MITGroup: Applications/SystemSource0: %{name}-%{version}.tar.gzSource1: %{name}.desktopSource2: iconsBuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root%description%{summary}%prep%setup -q%installrm -rf %{buildroot}mkdir -p %{buildroot}cp -a * %{buildroot}mkdir -p %{buildroot}%{_datadir}/applicationscp -a %{SOURCE1} %{buildroot}%{_datadir}/applicationsmkdir -p %{buildroot}%{_datadir}/icons/hicolor/86x86/appsmkdir -p %{buildroot}%{_datadir}/icons/hicolor/108x108/appsmkdir -p %{buildroot}%{_datadir}/icons/hicolor/128x128/appsmkdir -p %{buildroot}%{_datadir}/icons/hicolor/172x172/appscp -a %{SOURCE2}/86x86/%{name}.png %{buildroot}%{_datadir}/icons/hicolor/86x86/appscp -a %{SOURCE2}/108x108/%{name}.png %{buildroot}%{_datadir}/icons/hicolor/108x108/appscp -a %{SOURCE2}/128x128/%{name}.png %{buildroot}%{_datadir}/icons/hicolor/128x128/appscp -a %{SOURCE2}/172x172/%{name}.png %{buildroot}%{_datadir}/icons/hicolor/172x172/apps%cleanrm -rf %{buildroot}%files%defattr(-,root,root,-)%{_bindir}/*%{_datadir}/applications/%{name}.desktop%{_datadir}/icons/hicolor/*/apps/%{name}.png

Для сборки пакета теперь достаточно выполнить:

make rpm

Убираем зависимость от libutil.so

Я не нашёл способа, как убедить cargo при вызове команды линкера не передавать ключ -lutil. Вместо этого я решил создать скрипт-заглушку для линкера с какой-нибудь незначащей командой.

lib/libutil.so:

/* GNU ld script   Dummy script to avoid dependency on libutil.so */ASSERT(1, "Unreachable")

Да, не самый хороший способ. Если кто-нибудь знает, как сделать лучше, делитесь в комментариях.

Добавляем символ __libc_start_main

Перепробовав несколько способов, остановился на том, чтобы добавить при линковке стандартный объектный файл crt1.o. Копируем его с планшета:

scp nemo@192.168.2.15:/usr/lib/crt1.o ./lib

И добавляем в команды линкера:

.cargo/config.toml:

[target.armv7-unknown-linux-gnueabihf]rustflags = ["-C", "link-args=-L lib lib/crt1.o"]linker = "bin/armv7hl-meego-linux-gnueabi-ld"

Однако при попытке сборки получаем ошибки:

undefined reference to `__libc_csu_fini'undefined reference to `__libc_csu_init'

Добавим заглушки этих функций в main.rs:

src/main.rs:

#[no_mangle]pub extern "C" fn __libc_csu_init() {}#[no_mangle]pub extern "C" fn __libc_csu_fini() {}fn main() {    println!("Hello, world!");}

Ещё один быстрый и грязный хак, зато теперь RPM-пакет проходит валидацию и устанавливается!

Момент истины близок, запускаем на планшете и получаем очередную ошибку:

$ aurora-rust-helloworld-bash: /usr/bin/aurora-rust-helloworld: /usr/lib/ld.so.1: bad ELF interpreter: No such file or directory

Смотрим зависимости:

$ ldd /usr/bin/aurora-rust-helloworldlinux-vdso.so.1 (0xbeff4000)libgcc_s.so.1 => /lib/libgcc_s.so.1 (0xa707f000)librt.so.1 => /lib/librt.so.1 (0xa7069000)libpthread.so.0 => /lib/libpthread.so.0 (0xa7042000)libm.so.6 => /lib/libm.so.6 (0xa6fc6000)libdl.so.2 => /lib/libdl.so.2 (0xa6fb3000)libc.so.6 => /lib/libc.so.6 (0xa6e95000)/usr/lib/ld.so.1 => /lib/ld-linux-armhf.so.3 (0xa70e7000)

И видим динамическую линковку с библиотекой ld-linux-armhf.so.3. Если решать в лоб, то нужно создать символическую ссылку /usr/lib/ld.so.1 /lib/ld-linux-armhf.so.3 (и это даже будет неплохо работать). Но, к сожалению, такое решение не подходит. Дело в том, что строгий RPM-валидатор не пропустит ни пред(пост)-установочные скрипты в .spec-файле, ни деплой в директорию /usr/lib. Вообще список того, что можно, приведён здесь.

Долгое и разнообразное гугление подсказало, что у линкера GCC есть нужный нам ключ (dynamic-linker), который позволяет сослаться непосредственно на нужную зависимость. Правим config.toml:

.cargo/config.toml:

[target.armv7-unknown-linux-gnueabihf]rustflags = ["-C", "link-args=-L lib lib/crt1.o --dynamic-linker /lib/ld-linux-armhf.so.3"]linker = "bin/armv7hl-meego-linux-gnueabi-ld"

Собираем RPM-пакет, подписываем, копируем на планшет, устанавливаем и с замиранием сердца запускаем:

$ aurora-rust-helloworldHello, world!

Часть 2. Запускаем приложение с GUI

TL;DR Исходники проекта можно найти в репозитории.

В Авроре всё очень сильно завязано на Qt/QML, поэтому сначала я думал использовать крейт qmetaobject. Однако в комплекте с ОС идёт библиотека Qt версии 5.6.3, а qmetaobject, судя по описанию, требует минимум Qt 5.8. И действительно, попытка сборки крейта приводит к ошибкам.

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

Для начала копируем проект, созданный в предыдущей части, и переименовываем его в aurora-rust-gui.

Приступаем

Чтобы не утомлять читателя, сразу скажу, что для сборки понадобится скопировать с планшета ещё множество разных библиотек:

вот таких
scp nemo@192.168.2.15:/usr/lib/libstdc++.so ./libscp nemo@192.168.2.15:/usr/lib/libQt5Core.so.5 ./lib/libQt5Core.soscp nemo@192.168.2.15:/usr/lib/libQt5Gui.so.5 ./lib/libQt5Gui.soscp nemo@192.168.2.15:/usr/lib/libQt5Qml.so.5 ./lib/libQt5Qml.soscp nemo@192.168.2.15:/usr/lib/libQt5Quick.so.5 ./lib/libQt5Quick.soscp nemo@192.168.2.15:/usr/lib/libGLESv2.so.2 ./libscp nemo@192.168.2.15:/usr/lib/libpng16.so.16 ./libscp nemo@192.168.2.15:/usr/lib/libz.so.1 ./libscp nemo@192.168.2.15:/usr/lib/libicui18n.so.63 ./libscp nemo@192.168.2.15:/usr/lib/libicuuc.so.63 ./libscp nemo@192.168.2.15:/usr/lib/libpcre16.so.0 ./libscp nemo@192.168.2.15:/usr/lib/libglib-2.0.so.0 ./libscp nemo@192.168.2.15:/usr/lib/libsystemd.so.0 ./libscp nemo@192.168.2.15:/usr/lib/libQt5Network.so.5 ./libscp nemo@192.168.2.15:/lib/libresolv.so.2 ./libscp nemo@192.168.2.15:/usr/lib/libhybris-common.so.1 ./libscp nemo@192.168.2.15:/usr/lib/libicudata.so.63 ./libscp nemo@192.168.2.15:/usr/lib/libpcre.so.1 ./libscp nemo@192.168.2.15:/usr/lib/libselinux.so.1 ./libscp nemo@192.168.2.15:/usr/lib/liblzma.so.5 ./libscp nemo@192.168.2.15:/usr/lib/libgcrypt.so.11 ./libscp nemo@192.168.2.15:/usr/lib/libgpg-error.so.0 ./libscp nemo@192.168.2.15:/usr/lib/libcap.so.2 ./libscp nemo@192.168.2.15:/usr/lib/libsailfishapp.so.1 ./lib/libsailfishapp.soscp nemo@192.168.2.15:/usr/lib/libmdeclarativecache5.so.0 ./libscp nemo@192.168.2.15:/usr/lib/libmlite5.so.0 ./libscp nemo@192.168.2.15:/usr/lib/libdconf.so.1 ./libscp nemo@192.168.2.15:/usr/lib/libgobject-2.0.so.0 ./libscp nemo@192.168.2.15:/usr/lib/libQt5DBus.so.5 ./libscp nemo@192.168.2.15:/usr/lib/libdconf.so.1 ./libscp nemo@192.168.2.15:/usr/lib/libffi.so.6 ./libscp nemo@192.168.2.15:/usr/lib/libdbus-1.so.3 ./libscp nemo@192.168.2.15:/usr/lib/libgio-2.0.so.0 ./libscp nemo@192.168.2.15:/usr/lib/libgmodule-2.0.so.0 ./lib

А еще копируем заголовочные файлы, которые идут в составе Aurora SDK:

  • AuroraOS/mersdk/targets/AuroraOS-3.2.2.21-cert-armv7hl/usr/include/qt5 include/qt5

  • AuroraOS/mersdk/targets/AuroraOS-3.2.2.21-cert-armv7hl/usr/include/sailfishapp include/sailfishapp

  • AuroraOS/mersdk/targets/AuroraOS-3.2.2.21-cert-armv7hl/usr/include/GLES3 include/GLES3

  • AuroraOS/mersdk/targets/AuroraOS-3.2.2.21-cert-armv7hl/usr/include/KHR include/KHR

Для сборки проекта напишем скрипт build.rs и укажем его в Cargo.toml.

build.rs:

fn main() {    let include_path = "include";    let qt_include_path = "include/qt5";    let sailfish_include_path = "include/sailfishapp";    let library_path = "lib";    let mut config = cpp_build::Config::new();    config        .include(include_path)        .include(qt_include_path)        .include(sailfish_include_path)        .opt_level(2)        .flag("-std=gnu++1y")        .flag("-mfloat-abi=hard")        .flag("-mfpu=neon")        .flag("-mthumb")        .build("src/main.rs");    println!("cargo:rustc-link-search={}", library_path);    println!("cargo:rustc-link-lib=sailfishapp");    println!("cargo:rustc-link-lib=Qt5Gui");    println!("cargo:rustc-link-lib=Qt5Core");    println!("cargo:rustc-link-lib=Qt5Quick");    println!("cargo:rustc-link-lib=Qt5Qml");}

Cargo.toml:

[package]# ...build = "build.rs"[dependencies]cpp = "0.5.6"[build-dependencies]cpp_build = "0.5.6"#...

Теперь возьмёмся за само приложение. За создание инстанса приложения у нас будет отвечать структура SailfishApp по аналогии с приложением для Авроры, написанном на C++.

src/main.rs:

#[macro_use]extern crate cpp;mod qbytearray;mod qstring;mod qurl;mod sailfishapp;use sailfishapp::SailfishApp;#[no_mangle]pub extern "C" fn __libc_csu_init() {}#[no_mangle]pub extern "C" fn __libc_csu_fini() {}fn main() {    let mut app = SailfishApp::new();    app.set_source("main.qml".into());    app.show();    app.exec();}

SailfishApp это по сути обвязка (биндинги) к соответствующему классу на C++. Берём за образец структуру QmlEngine из крейта qmetaobject.

src/sailfishapp.rs
use crate::qstring::QString;cpp! {{    #include <sailfishapp.h>    #include <QtCore/QDebug>    #include <QtGui/QGuiApplication>    #include <QtQuick/QQuickView>    #include <QtQml/QQmlEngine>    #include <memory>    struct SailfishAppHolder {        std::unique_ptr<QGuiApplication> app;        std::unique_ptr<QQuickView> view;        SailfishAppHolder() {            qDebug() << "SailfishAppHolder::SailfishAppHolder()";            int argc = 1;            char *argv[] = { "aurora-rust-gui" };            app.reset(SailfishApp::application(argc, argv));            view.reset(SailfishApp::createView());            view->engine()->addImportPath("/usr/share/aurora-rust-gui/qml");        }    };}}cpp_class!(    pub unsafe struct SailfishApp as "SailfishAppHolder");impl SailfishApp {    /// Creates a new SailfishApp.    pub fn new() -> Self {        cpp!(unsafe [] -> SailfishApp as "SailfishAppHolder" {            qDebug() << "SailfishApp::new()";            return SailfishAppHolder();        })    }    /// Sets the main QML (see QQuickView::setSource for details).    pub fn set_source(&mut self, url: QString) {        cpp!(unsafe [self as "SailfishAppHolder *", url as "QString"] {            const auto full_url = QString("/usr/share/aurora-rust-gui/qml/%1").arg(url);            qDebug() << "SailfishApp::set_source()" << full_url;            self->view->setSource(full_url);        });    }    /// Shows the main view.    pub fn show(&self) {        cpp!(unsafe [self as "SailfishAppHolder *"] {            qDebug() << "SailfishApp::show()";            self->view->showFullScreen();        })    }    /// Launches the application.    pub fn exec(&self) {        cpp!(unsafe [self as "SailfishAppHolder *"] {            qDebug() << "SailfishApp::exec()";            self->app->exec();        })    }}

Биндинги для используемых классов QByteArray, QString, QUrl копируем из того же qmetaobject и расфасовываемым по отдельным файлам. Здесь приводить их не буду, если что, исходники можно посмотреть в репозитории на GitHub.

Немного скорректируем заголовочный файл sailfishapp.h, чтобы он искал заголовочные файлы Qt в правильных местах:

include/sailfishapp/sailfishapp.h:

// ...#ifdef QT_QML_DEBUG#include <QtQuick>#endif#include <QtCore/QtGlobal>  // Было `#include <QtGlobal>`#include <QtCore/QUrl>      // Было `#include <QUrl>`class QGuiApplication;class QQuickView;class QString;// ...

Осталось только добавить файлы QML и положить их в дистрибутив RPM.

все здесь

qml/main.qml:

import QtQuick 2.6import Sailfish.Silica 1.0ApplicationWindow {    cover: Qt.resolvedUrl("cover.qml")    initialPage: Page {        allowedOrientations: Orientation.LandscapeMask        Label {            anchors.centerIn: parent            text: "Hello, Aurora!"        }    }}

qml/cover.qml:

import QtQuick 2.6import Sailfish.Silica 1.0CoverBackground {    Rectangle {        id: background        anchors.fill: parent        color: "blue"        Label {            id: label            anchors.centerIn: parent            text: "Rust GUI"            color: "white"        }    }    CoverActionList {        id: coverAction        CoverAction {            iconSource: "image://theme/icon-cover-cancel"            onTriggered: Qt.quit()        }    }}

.rpm/aurora-rust-gui.spec:

# ...Source3: qml# ...%install# ...mkdir -p %{buildroot}%{_datadir}/%{name}cp -ra %{SOURCE3} %{buildroot}%{_datadir}/%{name}/qml%cleanrm -rf %{buildroot}%files# ...%{_datadir}/%{name}/qml

Makefile:

# ...rpm:# ...@cp -rvf qml ./target/armv7-unknown-linux-gnueabihf/release/rpmbuild/SOURCES# ...

Собираем:

make cleanmake releasemake rpm

Подписываем, копируем, устанавливаем, запускаем из командной строки и вуаля:

$ devel-suPassword:# pkcon install-local ./aurora-rust-gui-0.1.0-1.armv7hl.rpmInstalling filesTesting changesFinishedInstalling filesStartingResolving dependenciesInstalling packagesDownloading packagesInstalling packagesFinishedDownloaded  aurora-rust-gui-0.1.0-1.armv7hl (PK_TMP_DIR)         Rust GUI example for Aurora OSInstalled   aurora-rust-gui-0.1.0-1.armv7hl (PK_TMP_DIR)            Rust GUI example for Aurora OS# exit$ aurora-rust-gui[D] __cpp_closure_14219197022164792912_impl:33 - SailfishApp::new()[D] SailfishAppHolder::SailfishAppHolder:15 - SailfishAppHolder::SailfishAppHolder()[D] unknown:0 - Using Wayland-EGLlibrary "libpq_cust_base.so" not found[D] __cpp_closure_16802020016530731597:42 - SailfishApp::set_source() "/usr/share/aurora-rust-gui/qml/main.qml"[W] unknown:0 - Could not find any zN.M subdirs![W] unknown:0 - Theme dir "/usr/share/themes/sailfish-default/meegotouch/z1.0/" does not exist[W] unknown:0 - Theme dir "/usr/share/themes/sailfish-default/meegotouch/" does not exist[D] onCompleted:432 - Warning: specifying an object instance for initialPage is sub-optimal - prefer to use a Component[D] __cpp_closure_12585295123509486988:50 - SailfishApp::show()[D] __cpp_closure_15029454612933909268:59 - SailfishApp::exec()

Вот так выглядит наше приложение с разных ракурсов:

 Рабочий стол с ярлыком Рабочий стол с ярлыком Главное окно приложенияГлавное окно приложения Панель задач Панель задач

Последние штрихи

Приложение отлично стартует из командной строки при подключении по SSH, однако никак не реагирует при попытке запуска с помощью ярлыка. Путём некоторых экспериментов удалось установить, что для этого надо экспортировать символ main (RPM-валидатор выдавал предупреждение на этот счёт, но некритичное).

Серия проб и ошибок показала, что надо добавить ещё один ключ линкера: -export-dynamic.

.cargo/config.toml:

[target.armv7-unknown-linux-gnueabihf]rustflags = ["-C", "link-args=-L lib lib/crt1.o -rpath lib --dynamic-linker /lib/ld-linux-armhf.so.3 -export-dynamic"]linker = "bin/armv7hl-meego-linux-gnueabi-ld"

После этого всё работает так, как и ожидается.

Заключение

Понятно, что до того, как использовать Rust в проде, ещё надо решить немало вопросов. Как минимум, я предвижу сложности с дополнительными зависимостями при подключении новых крейтов, извечные танцы с бубном вокруг сегфолтов при FFI-вызовах, увязывание систем владения Qt и Rust. Некоторые интересные подробности можно почерпнуть из статьи от автора qmetaobject-rs. Наверняка, время от времени будут всплывать и другие проблемы.

Плюс к этому, для того, чтобы использовать Qt-овские классы, к каждому из них необходимо писать биндинги.

Однако всё это стоит того, чтобы иметь возможность писать на Rust, с его мощной системой типов, средствами обеспечения безопасности при работе с памятью, приятной реализацией многопоточности, самым лучшим подходом к обработке ошибок и всеми теми вещами за которые мы его так любим.

Буду рад вопросам и замечаниям в комментариях. И ставьте лайк, подписывайтесь на канал :-)

Подробнее..

Категории

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

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