×

Обережно, кодогенерація

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті

Раніше вже писав про збільшення швидкодії та зменшення використання пам’яті після використання кодогенерації і ця історія має продовження, а саме розбір помилок.

Protobuf, перша помилка яка показала себе через пару тижнів після змін в коді

В проекті ми використовуємо офіційну бібліотеку Protobuf, яка підчас серіалізації використовує рефлексію і будує слайс байтів через append.

А потім я дізнався про Protocol Buffers for Go with Gadgets, бібліотеку-fork яка генерує додатковий код щоб прибрати рефексію підчас серіалізації і вже записує в слайс байтів по індексу бо так швидше.

Коли змінював одну бібліотеку на іншу то важливим вважав, що стало працювати швидше і написані раніше тести пройшли успішно.

І все б було гаразд, але в проекті існувала латка, яка через пару тижнів після заміни перезапустила мікросервіс через паніку:

panic: runtime error: index out of range

Латка виглядала приблизно так:

import (
	"github.com/golang/protobuf/proto"
	google "gitlab.com/go-yp/go-warning-codegeneration/models/protos/google/advertisement"
)

func example() {
	var popup = &google.Popup{
		Id:      uuid(),
		Viewed:  true,
		Clicked: false,
	}

	// some deep nested function
	go func() {
		var content, err = proto.Marshal(popup)

		if err != nil {
			// log error

			return
		}

		// store to database
		store(content)
	}()

	// some delay with other actions

	// @temporary hack
	go func() {
		popup.Clicked = true

		var content, err = proto.Marshal(popup)

		if err != nil {
			// log error

			return
		}

		// store to database again
		store(content)
	}()
}

І зі стандартною бібліотекою латка працювала без паніки для мікросервісу який працює постійно:

import (
	"github.com/golang/protobuf/proto"
	google "gitlab.com/go-yp/go-warning-codegeneration/models/protos/google/advertisement"
	"testing"
)

const (
	n = 1000000
)

func TestGoogleProtoMarshal(t *testing.T) {
	for i := 0; i < n; i++ {
		var popup = &google.Popup{
			Id:      uint32(i),
			Viewed:  true,
			Clicked: false,
		}

		// some deep nested function
		go func() {
			_, _ = proto.Marshal(popup)
		}()

		// @temporary hack
		go func() {
			popup.Clicked = true

			_, _ = proto.Marshal(popup)
		}()
	}
}

А от з github.com/gogo/protobuf при аналогічному тесті вже видає паніку.

Якщо розглянути згенерований код:

func (m *Popup) Marshal() (dAtA []byte, err error) {
	size := m.Size()
	dAtA = make([]byte, size)
	n, err := m.MarshalToSizedBuffer(dAtA[:size])
	if err != nil {
		return nil, err
	}
	return dAtA[:n], nil
}

func (m *Popup) MarshalToSizedBuffer(dAtA []byte) (int, error) {
	i := len(dAtA)

	//...

	if m.Clicked {
		i--
		dAtA[i] = 1
		i--
		dAtA[i] = 0x18
	}

	//...

	return len(dAtA) - i, nil
}

то стає зрозуміло, що розрахунок ємності слайсу відбувався за умов m.Clicked = false, а серіалізація за умов m.Clicked = true і таким чином отримав паніку «index out of range».

Звісно латку ми виправили і стало працювати навіть краще.

JSON, помилка у vendor бібліотеці

Бібліотека easyjson теж для серіалізації працює через додатковий код замість використання рефлексії.
Але після внесення в easyjson одної з оптимізацій, час від часу почали отримувати зламаний JSON, ось приклад тесту який покаже помилку.

package tests

import (
	"github.com/stretchr/testify/require"
	"gitlab.com/go-yp/go-warning-codegeneration/models/jsons/easy"
	"testing"
)

const (
	// language=JSON
	popupWithUnicodeContent = `{
        "title": "Some title with symbol \u201Dt",
        "description": "Any description"
    }`

	// language=JSON
	popupContent = `{
        "title": "Some title",
        "description": "Any description"
    }`
)

func TestEasyjsonUnmarshalJSON(t *testing.T) {
	content := make([]byte, 0, 1024)

	content = append(content[:0], popupWithUnicodeContent...)

	var popup easy.Popup

	unmarshalErr := popup.UnmarshalJSON(content)

	require.NoError(t, unmarshalErr)

	var expected = easy.Popup{
		Title:       "Some title with symbol \u201Dt",
		Description: "Any description",
	}

	require.Equal(t, expected, popup)

	content = append(content[:0], popupContent...)

	/**
	Failed:
	expected: easy.Popup{Title:"Some title with symbol ”t", Description:"Any description"}
	actual  : easy.Popup{Title:"Some title with symbol ”t", Description:" }y description"}
	*/
	require.Equal(t, expected, popup)
}

В easyjson цю помилку вже виправили.

Висновки

Звісно, хочеться використовувати оптимізовані бібліотеки, але стандартні краще протестовані та мають менше помилок.

Приклади доступні в репозиторії.

👍ПодобаєтьсяСподобалось0
До обраногоВ обраному3
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

В gotime нещодавно (я тільки минулого тижна до того випуску добрався) розмовляли з контрбютором в стандартний json. рекомендую — принамні вказується чому стандартні ліби відрізняються від «швидко і майже так само» альтернатив.

Знайшов Go Time {"encoding":"json"}, дякую, послухаю, і англійський дуже чіткий це теж добре

Отличный материал и опыт, спасибо большое !

Звісно, хочеться використовувати оптимізовані бібліотеки, але стандартні краще протестовані та мають менше помилок.

ваш кэп

Якщо ти заміниш в тексті теми «латка» на «милиці» або «костиль» то сенс буде той же.

В 90х ідея кодогенерації була модною, продавали усякі тулзи.
Згодом зрозуміли, що нагенерувать можна, але відладити нагенерований код — фіг.
І повернулись до писання ручками.
Тепер нове покоління наступає на ті ж граблі.
en.wikipedia.org/wiki/Rational_Rhapsody

Згодом зрозуміли, що нагенерувать можна, але відладити нагенерований код — фіг.
І повернулись до писання ручками.

Поняли что лучше чтобы он каждый раз перегенерировался при компиляции / во время исполнения и сделали вменяемое метапрограммирование, и пишут такие вещи как в дотти или в расте.

Для вещей аля нагенерить заглушек для протобуфа какоето особо мудренное метапрограммирование не нужно особо.

У випадку Golang згенерований код зберігається в проекті як і звичайний код і відлагодити можна

А от писати руками серіалізацію для 20+ JSON моделей в проекті дуже дорого і помилитись ще простіше

Хоча в Rust робиться через атрибут derive, що простіше ніж запускати генерацію через CLI

что-то не похоже, что с этой штукой можно сделать любую кодогенерацию и заменить полностью рефлексию на compile time работу с AST как например тут:
docs.microsoft.com/...​erator?view=roslyn-dotnet

в F#, апи попроще, но те не менее задачи как в сабже решали с покон веков без рефлексии через Type providers и с куда более удобным тулингом
raw.githubusercontent.com/...​ocs/files/sqlprovider.gif

и заменить полностью рефлексию

На «заменить полностью рефлексию» по части динамических вещей есть staged programming которые позволяют сделать деривацию и прочие вещи в рантайме но с типами.

Да, эти штуки(о чем ты пишешь) действительно не могут заменять рефлексию так как имеют ограничения в отличии от более мощных Source Generators в C#.
В целом те апи которые позволяют в рантайме что-то сгенерить динамически или изменить поведение типа еще и не очень удобны — имеют свои накладные расходы, специфичный апи поверх типичных языковых конструкций или AST и нередко усложняют отладку такого кода.

действительно не могут заменять рефлексию

 В подавлющем большинстве прикладных случаев могут, особенно если сделано что то по типу вот этого: dotty.epfl.ch/...​aprogramming/staging.html.

более мощных Source Generators в C#.

не смешите мои тапочки, сишарповый кодоген немощен и с корявейшим API.

усложняют отладку такого кода.

если вам надо дебажить, значит что то вы делаете не так.

если вам надо дебажить, значит что то вы делаете не так.

если вам не надо дебажить — значит, у вас нет бизнес логики

Нет, у меня хорошие логи и нету состояния, а бизнес логика — есть.

В подавлющем большинстве прикладных случаев могут, особенно если сделано что то по типу вот этого: dotty.epfl.ch/...​aprogramming/staging.html.

quotations в fsharp/scala одна хрень даже называются одинаково, они же expression trees в с# уже успело покрытся слоем пыли, хотя как в Scala я правда не знаю , если добавили недавно я тебя поздравляю — можешь что-то новое узнать. Рефлексию оно само собой заменить не может — так как апи порезано и позволяет генерить только что-то узкоспециализированное типа методов делегатов или трейтов.

не смешите мои тапочки, сишарповый кодоген немощен и с корявейшим API.

Расскажи детальней, что с ним не так.

апи порезано и позволяет генерить только что-то узкоспециализированное типа методов делегатов или трейтов.

Квазиквоты позволяют генерить все что угодно и типизируемо — любой терм (вот эта типизируемая порезка API это не кривизна) до запуска, и вовремя выполнения приложения с шикарнейшим синтаксисом — хоть интерпретатор языка в скалу пиши.

Расскажи детальней, что с ним не так.

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

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

— кодогенератор в c# не юзает рефлексию и тоже дает compile type sefety с подсветкой;

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

в кодогенераторе тоже работает из коробки.

Сделать кодоген кодогенов

Звучит как нафиг никому ненужная херня.

Вот открываем твою ссылку и там же пишут вторым абзацем про ограничения, что я говорил выше:
Providing an interpreter for the full language is quite difficult, and it is even more difficult to make that interpreter run efficiently. So we currently impose the following restrictions on the use of splices.

A top-level splice must appear in an inline method (turning that method into a macro)

The splice must call a previously compiled method passing quoted arguments, constant arguments or inline arguments.

Splices inside splices (but no intervening quotes) are not allowed.

В C# подобных ограничений нет — генерируй любой валидный С# код(если хочешь рекурсивно), нельзя только код уже скомпиленный менять.

github.com/...​ures/source-generators.md
github.com/...​ce-generators.cookbook.md

Что там скала может противоставить, неотлаживаемые методы в байткоде сгенеренные из квотейшинов — спасибо, не надо задаром.

что-то не похоже, что с этой штукой можно сделать любую кодогенерацию и заменить полностью рефлексию на compile time работу с AST как например тут:
docs.microsoft.com/...​erator?view=roslyn-dotnet

В Rust это делается с помощью процедурных макросов, пример: github.com/...​aphql-rust/graphql-client

#[derive(GraphQLQuery)]
#[graphql(
    schema_path = "schema.json",
    query_path = "examples/puppy_smiles.graphql",
    response_derives = "Debug"
)]
struct PuppySmiles;

как дела у процедурных макросов с поддержкой IDE? например как делать отладку сгенеренного кода или intelli sense после внесения изменений?

Rust Language Service предоставляет информацию о коде, сгенерированном макросом. Интеграцию с RLS поддерживает с десяток разных IDE (включая vim, vscode, intelij).

Для отладки кодогенераторов макроса есть стандартная тулза, которая выдает исходный код программы с развернутыми макросами (github.com/dtolnay/cargo-expand).

Rust Language Service предоставляет информацию о коде, сгенерированном макросом. Интеграцию с RLS поддерживает с десяток разных IDE (включая vim, vscode, intelij).

ты так и не ответил на вопрос — появляться ли автокомплит в коде раста описанном через derive и если появляется, что для этого надо сделать?

Для отладки кодогенераторов макроса есть стандартная тулза, которая выдает исходный код программы с развернутыми макросами (github.com/dtolnay/cargo-expand).
Once installed, the following command prints out the result of macro expansion and #[derive] expansion applied to the current crate.

очень удобно. Серьезно? Для этого в с# коде лет 15 назад в watch вызывали tostring().
я думал ты мне предлагаешь как в современных языках — можно просто скомпилить и и через навигацию по символам ставить брейкпоинты.

It is expected instead that source generators would work on an ’opt-in’ approach to IDE enablement.

By default, a generator implementing only ISourceGenerator would see no IDE integration and only be correct at build time. Based on conversations with 1st party customers, there are several cases where this would be enough.

However, for scenarios such as code first gRPC, and in particular Razor and Blazor, the IDE will need to be able to generate code on-they-fly as those file types are edited and reflect the changes back to other files in the IDE in near real-time.

The proposal is to have a set of advanced callbacks that can be optionally implemented, that would allow the IDE to query the generator to decide what needs to be run in the case of any particular edit.

когда раст дизайн дойдет до этого, дашь знать. возможно появиться смысл заценить ваш кодоген. пока он мало чем отличается от того, на чем делали кодоген в С# до того, как ты не забросил его много лет назад — expression trees

ты так и не ответил на вопрос — появляться ли автокомплит в коде раста описанном через derive

Да

и если появляется, что для этого надо сделать?

Использовать IDE, которая поддерживает RLS

очень удобно. Серьезно?
я думал ты мне предлагаешь как в современных языках — можно просто скомпилить и и через навигацию по символам ставить брейкпоинты.

Нужно понимать, что фича Roslyn, на которую ты ссылаешься, и процедурные макросы в Rust — это немного разные вещи, и ожидать идентичного экспириенса работы с ними неразумно.

В Roslyn «Explicitly additive only. Generators can add new source code to a compilation but may not modify existing user code.»

В Rust «Returned syntax either replaces or adds the syntax depending on the kind of procedural macro.»

Другими словами, Roslyn дает возможность в фоне сгенерить несколько C# файлов.

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

Если стоит простая задача нагенерить *.rs файлов, которые должны быть скомпилированы вместе с остальным проектом, то это в Rust это тоже возможно через build.rs расширение, которые также поддерживаются RLS, и возможности отладки сгенерированного кода будут аналогичны возможностям отладки обычного кода.

когда раст дизайн дойдет до этого, дашь знать. возможно появиться смысл заценить ваш кодоген. пока он мало чем отличаться от того на чем делали кодоген в С#, до того как ты не забросил его много лет назад — expression trees

Судя по тому, что ты цитируешь, в этом плане экосистема Rust далеко впереди Roslyn.

Да

Это хорошо, стало интересно.

Заглянул сюда:

github.com/...​github/examples/github.rs

Если подсветка пашет из коробки — зачем делать апи в котором надо аннотировать код на десериализации и при подготовке запроса?

Работа с http клиентом, аннотированние типов явное — не то что бы я ожидал от удобного graphql клиента — можно человеческих названий методов, type safety на вызове метода с полным перечнем аргументов и возвращаемого значения типизированного из сгенеренного кода, как это сделать, если через build-scripts то этого я тоже повидал, но это не кодоген о котором я говорю выше — почему, потому что нет интеграции с IDE (нет возможности работать с AST, нет возможности контролировать когда в IDE произойдет изменение в шаблоне обновить автокомплит. (подсветка у меня в расте кстати с derive тоже не заработала с этим marketplace.visualstudio.com/...​s?itemName=rust-lang.rust). На гитхабе нашел сразу проблему с поддержкой этого — стало лень разбираться. Не похоже что бы оно работало из коробки.

Нужно понимать, что фича Roslyn, на которую ты ссылаешься, и процедурные макросы в Rust — это немного разные вещи, и ожидать идентичного экспириенса работы с ними неразумно.

Поэтому я тебе и говорю, что такой кодоген как ты предлагаешь не удобен в отличии от SourceGenerator с удобной поддержкой тулинга, которого в расте нет — согласись ссылка на third party библиотеку, которая форматирует в строку код через вызов cli это не выглядит как надежное и удобное промышленное решение, больше отсылка к js экземпляров двадцатилетней давности с отладкой через консоль.

Это дает гораздо большие возможности, и сложнее в использовании.

Судя по тому, что ты цитируешь, в этом плане экосистема Rust далеко впереди Roslyn.

Из недостающих возможностей roslyn ты упомянул только возможность менять уже написанный код — для таких задач есть более похожий апи как я тебе уже сказал на макросы — expression tree там можно легко менять при необходимости код. Только эти возможности весьма сомнительны, из реальных use cases приходит в голову только всякое AOP по типу логирования и другой ерунды что и без кодогена можно сделать. Но если у тебя есть более подходящие примеры практически полезные — я бы охотно выслушал.

Если подсветка пашет из коробки — зачем делать апи в котором надо аннотировать код на десериализации и при подготовке запроса?

Я не понимаю о чем идет речь, можно конкретный код, который смущает?

Работа с http клиентом, аннотированние типов явное — не то что бы я ожидал от удобного graphql клиента — можно человеческих названий методов, type safety на вызове метода с полным перечнем аргументов и возвращаемого значения типизированного из сгенеренного кода, как это сделать

Как ты считаешь, то, что ты описал, это характеристика самого Rust или характеристика какой-то конкретной библиотеки graphql, которая написана на Rust?

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

Есть такое дело, в данный момент Rust это не язык для неосиляторов. Learning curve достаточно крутая, и настройка IDE это не самый сложный аспект языка.

Поэтому я тебе и говорю, что такой кодоген как ты предлагаешь не удобен в отличии от SourceGenerator с удобной поддержкой тулинга, которого в расте нет

Ты начал с того, что «что-то не похоже, что с этой штукой можно сделать любую кодогенерацию и заменить полностью рефлексию на compile time работу с AST». Ты согласен, что в плане функциональности Rust как минимум имеет feature parity с Roslyn и дело только в удобстве использовании?

согласись ссылка на third party библиотеку, которая форматирует в строку код через вызов cli это не выглядит как надежное и удобное промышленное решение, больше отсылка к js экземпляров двадцатилетней давности с отладкой через консоль.

Не соглашусь. Для того, чтоб делать такое заявление, нужно иметь достаточно опыта разработки и отладки процедурных макросов, а их у меня нет. Мой небольшой опыт разработки декларативных макросов в Rust не привел меня к выводу, что в экосистеме не хватает каких-то инструментов, и что разработка макросов «ненадежна».

Из недостающих возможностей roslyn ты упомянул только возможность менять уже написанный код — для таких задач есть более похожий апи как я тебе уже сказал на макросы — expression tree там можно легко менять при необходимости код. Только эти возможности весьма сомнительны, из реальных use cases приходит в голову только всякое AOP по типу логирования и другой ерунды что и без кодогена можно сделать.

Как ты считаешь, твое мнение о макросах отличалось бы от текущего, если бы C# имел поддержку макросов с первого дня и вокруг этой фичи было бы запилено сотни сторонних библиотек?

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

lib.rs/...​/procedural-macro-helpers

Ты начал с того, что «что-то не похоже, что с этой штукой можно сделать любую кодогенерацию и заменить полностью рефлексию на compile time работу с AST». Ты согласен, что в плане функциональности Rust как минимум имеет feature parity с Roslyn и дело только в удобстве использовании?

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

Как ты считаешь, твое мнение о макросах отличалось бы от текущего, если бы C# имел поддержку макросов с первого дня и вокруг этой фичи было бы запилено сотни сторонних библиотек?

Не совсем уловил суть вопроса — на всяких неудобных кодогенах делают много хороших библиотек, но это не меняет тот факт что какие-то из них менее удобны, чем другие. Понимаешь?

lib.rs/...​/proceduralmacro-helpers

Речь шла не про весь кодоген, а про то что ты позиционируешь как продвинутое преимущество — возможность менять уже написанный код. То что ты скинул сплошные парсеры и поддержка квотейшинов в основном — где там модификация существующего кода макросами и под какие задачи?

1. Хакер ломает репу и внедряет бекдор в либу
2. Вы билдите либу и пихаете в прод
3. Hacker owned

Так что в этом кейсе еще дешево отделались

Переглядав зміни в коді перед тим як оновлювати бібліотеку, попри це пропустив що оптимізація спрацьовувала раз при певних умовах і далі завжди використовувалась ігноруючи умови

А если недоглядел бэкдор — то будешь виноватым мб даже уголовно

Цей бекдор ще мають додати в репозиторій, що також складно коли є спільнота

Вспоминаем историю с openssl.

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

Вообще я не представляю, как можно просто так внедрить бекдор, пулл реквест должен еще и ревью пройти.

Тю. Взламывается и пихается. Например www.anti-malware.ru/...​-06-29-1447/26671?page=16
Более профессиональный метод — внедрить агентов в комьюнити. Например habr.com/ru/post/219105

То, что вы не представляете как это сделать, говорит о том, что INFUSEmedia скорее всего не отправляет вас на тематические семинары по кибербезопасности, что не делает вам чести.

Чим це відрізняється від використання залежності напряму, в будь-якій іншій мові? Якщо залежність скомпроментована, то і твій код скомпроментований, чи ні?

Именно. По этому надо иметь форки сорцов в своей репе с защитой.

Підписатись на коментарі