Source Generators in Action
Презентація доповіді
Під час доповіді ми в режимі реального часу створимо source generator, розберемося з деякими проблемами, які ми потенційно можемо зустріти під час розробки, та навчимося вирішувати їх.
Транскрипція доповіді
Доброго дня. Всім привіт. Мене звати Володимир Ланцов. Сьогодні я виступаю з темою "Source Generators in Action". Власне, трошки про мене. Я Middle.net-розробник у компанії ELEX. Мене, в принципі, вже представили. Як сказали, в основному працюю з вебом та Azure. Але Source Generators – це одна з тих тем, які мені дуже подобаються на рівні Advanced – розбиратися, писати, генерувати код. Ось, власне, за QR-кодом доступне посилання на ресурс, через який ви можете зі мною зв'язатися. Також є юзернейми з Telegram. Також можете зв'язатися, якщо вам буде потрібно. Власне, що таке Source Generator? Це особливість компілятора, що додає можливість створювати та додавати новий код до вже існуючого у процесі компіляції. Навіщо вам може бути потрібен Source Generator? Перше, що приходить на думку – це спростити річну роботу при розширенні коду.
Наприклад, у вас є декілька сотень чи тисяч файлів, які ви хочете одноразово, не на постійній основі розширити. Наприклад, згенерувати до кожного файлу якийсь mapper або extension клас. Відповідно, ви можете написати Source Generator. Він при білді виконається і згенерує для вас багато необхідних файлів. В такому випадку ви після цього можете видалити Source Generator і більше його не використовувати. Це приклад одноразового використання чисто для генерації якихось файлів автоматично. Я такий спосіб використовував у себе на проєкті, коли було необхідно згенерувати mapper. Тільки я використав вже готову бібліотеку Mapster. Вона згенерувала всі необхідні мені mapper Я їх потім вручну трішки підправив, де було необхідно.
Другий приклад використання — це багаторазовий. Це коли ви додаєте Source Generator і він залишається у вашому проєкті умовно назавжди. Цей спосіб може бути використаний для заміни повільної рефлексії на Compile Timecode. Наприклад, в паттернах Mediator або в Command паттерні. Є велика кількість рефлексій для зручності. І, наприклад, бібліотека Mediator, дуже відома, вона використовує якраз такий Source Generator, щоб пришвидшити виконання, замість рефлексій. Процес виконання Source Generator наступний. Компіляція запускається, доходить до кроку, в якому виконується Source Generator. І Source Generator має повний доступ до вашого існуючого коду. Він цей код аналізує і на основі нього створює новий код, який пізніше додається до компіляції. І компіляція, власне, продовжує своє виконання.
Передумови для використання Source Generator наступні. Ви маєте мати .NET 5 SDK або вище. .NET Standard 2.0 Target Framework, але це правило розповсюджується лише на проєкт з Source Generator. Тобто ваш основний проєкт, до якого Source Generator підключається, він може бути .NET 5, 6 і вище. Ви маєте встановити також два Nugget-пакети. І в цих пакетах розміщений атрибут Generator, яким ви маєте відмітити кожен Source-генератор, який ви створили. Інакше він не буде працювати. Існує два види Source-генераторів. Один новий, інший застарілий.
Застарілий, ми його сьогодні розглядати в рамках презентації не будемо. А новий, якраз на основі нового, будуть всі приклади. Як ми можемо побачити відмінність, раніше у нас етап аналізу коду і етап генерації коду були розділені на два окремих методи. А зараз у нас ці... Кроки об'єднали. Це було зроблено в першу чергу для продуктивності, тому що Source-генератори інколи виконуються досить повільно, особливо коли треба аналізувати дуже багато файлів. І продуктивність грає дуже важливу роль.
Тепер перейдемо до практики. Приклад створення Source-генератора. Ми створимо новий проєкт. Даємо йому назву. Це має бути бібліотека класів. Обов'язково задаємо .NET Standard 2.0. Можна і 2.1. Це також буде працювати. Після чого ми встановлюємо необхідні Nugget-пакети. Ви можете побачити, що я вибрав версію 4.3. Це тому, що в Rider використовується саме такий розтин компілятор при білді Source-генераторів.
Далі ми переходимо в CS-проект і додаємо референс на наш Source-генератор. Обов'язковими елементами є Output-item-type, що дорівнює Analyzer, і Reference-output-assembly, що дорівнює False. Далі ми створюємо клас нашого генератора і реалізуємо інтерфейс. Наступним кроком, який ми пам'ятаємо, це Attribute-generator. Ви також можете побачити, що він має дві перегрузки конструктора. Перша перегрузка, як написано в описі, Provides C-Sharp Sources. Це означає, що за замовчуванням у нас підтримується створення саме C-Sharp коду за допомогою Source-генератора. У нас є можливість змінити цю поведінку, додати підтримку для F-Sharp, для Visual Basic, використовуючи другу перегрузку конструктора. І передати туди список всіх мов, які нас цікавлять.
Базовий процес генерації коду виглядає наступним чином. Ми робимо спочатку фільтр по існуючому коду. SyntaxNode — це об'єкт, що представляє собою одну з частин нашого файлу. Це може бути namespace або scoped namespace, або клас, або властивість. Тобто все, що завгодно, що у нас у файлах з кодом розміщено. Далі ми трансформуємо отримані дані. Трансформація працює по принципу метода Select в Link. Тобто те, що ми трансформували, ми отримуємо надалі як об'єкт-айтем. Ось. В цьому місці. Далі метод AddSource. Він додає код до компіляції. Тут ми бачимо два параметри. HintName — це ім'я файлу. Якщо ми вкажемо там myclass, то воно створить файл myclass.cs. Воно автоматично підставить розширення. Але якщо у нас декілька мов, то нам треба сказати розширення самостійно. E-source — це контент файлу, який ми додаємо.
Кодогенерація на основі існуючих типів. Ми тільки що розглянули, що у нас є предікат. Ми використовуємо його для фільтру. Фільтр може виглядати наступним чином. У нас є helper. І ось приклад того, як це може виглядати. Ми фільтруємо, що це рекорд або клас, що у нього є модифікатор public і відсутній модифікатор static. Також фільтруємо, що це верхнерівневий клас. Тобто він знаходиться безпосередньо в namespace. Іншими словами, ми не підтримуємо клас в класі. І ми перевіряємо, що namespace має в собі статус. Тобто в папці models знаходиться клас.
Це приклад фільтру, predicate, який ми можемо написати. Далі ми отримуємо declaredSymbol. DeclaredSymbol – це те, як описується в SourceGenerator існуючий тип. Кожен тип, кожна нода, умовно. Вона описана інтерфейсом iSymbol. Там є різні різновиди, такі як iTypeSymbol, iPropertySymbol, iNamespaceSymbol і так далі. Але враховуючи, що ми predicate вже відфільтрували, що це тільки клас або рекорд, відповідно, ми знаємо, що там завжди буде ім'я типу, яке ми отримуємо. І надалі ми передаємо новий контент для генерації в процесі компіляції. Існує також можливість генерації на основі зовнішніх файлів.
Наприклад, у нас в проєкті є DataJSON, в якому масив об'єктів, з яких ми хочемо створити класи. Приклад такого кейсу може бути, наприклад, для бібліотеки TelegramBot я парсив Bot API. Там було сотні методів і типів, і з них треба було згенерувати .NET-класи і .NET-методи. Відповідно, таким чином можна прочитати файл, отримати з нього контент на методі getText. Далі ми цей контент отримуємо як параметр в делегації, і ми можемо децентралізувати наші дані. Приклад того, як виглядають дані, можна побачити тут. Це може бути ось такий JSON, просто з даними, які ми хочемо згенерувати. Ось. І відповідно до цієї діагностики, яка буде існувати. Дуже важливо не забути додати цей файл, щоб його бачив source-генератор. Тому що за замовчуванням компілятор не бачить файли, які у нас є в проєкті. Для цього нам треба перейти в properties і вибрати build action additional files. В такому випадку у нас додається ось такий запис, такий тег additional files. І він якраз робить цей файл доступним для source-генератора. Якщо ми це не вкажемо, то він просто не буде знайдений.
Далі. Шаблон вихідних файлів. Генерувати через інтерполяцію чи string builder буває незручно. Наші класи, які ми перейдемо до компіляції. Цей код складно читати. Тож я рекомендую використовувати шаблонізатори. Мастеч в даному випадку виступає як простий шаблонізатор. Він підтримує передачу змінних. Підтримує прості логічні операції, такі як if. Підтримує цикли. Є більш просунутий шаблонізатор, який вже виступає як повноцінна скриптова мова. Називається Scriban або Scriban. Тут можна вже вказувати прям конструкції коду, також передавати змінні, робити вже блоки більш складні, такі як if-else. Можна використовувати switch case, цикли, можна створювати свої власні функції навіть, і викликати їх. Також в цій мові є багато вбудованих функцій. Наприклад, string split або array for each, або ще якісь.
Далі, встановлення залежностей. Як ми знаємо, System.txt.json не входить в .NET Standard 2.0, який є цільовим фреймворком для Source генератора. Тому що ця бібліотека з'явилась пізніше. Відповідно, нам треба підключити пакет nugget. І взагалі розглянемо процес підключення. Ми додаємо пакет до проекту, і здавалося б, воно має зараз запрацювати. Але, як ми бачимо, у нас виліз порушення, яке каже "could not load file or assembly." Це тому, що він не може знайти файл System.txt.json.dll. Цей файл просто недоступний на етапі компіляції. Відповідно, поки що не придумали нічого краще, ніж вставити костиль в цей спроч файл, який буде самостійно копіювати ці .dll файли, і Source генератор буде їх бачити. Виглядає це наступним чином: якщо ми зробимо rebuild, то наш ворнінг зникне.
Розглянемо налагодження або ж дебаг. Спочатку почнемо з прикладів ручного дебагу, коли ми запускаємо дебагер власноруч. У Visual Studio за замовчуванням це робиться дуже просто. Ми використовуємо debugger-launch, який запустить дебагер і приєднає його до процесу. Все. Ми змогли увійти в дебаг. Тепер розглянемо, як це працює в Rider. В Rider за замовчуванням, на жаль, ручний дебаг не працює. Нам пропонує запустити Visual Studio. Тому є два варіанти вирішення проблеми.
Варіант перший – це приєднати дебагер власноруч і заблокувати Source генератор, що ми зараз і робимо. Там буде дуже багато дебагу. Запускаємо білд, у нас блокується виконання source-генератора, потім ми робимо attach to process, шукаємо файл csc.dll і приєднуємо до нього .NET Core Debugger. Це перший варіант, як це можна зробити.
Варіант №2 – це ми беремо наш source-генератор і йдемо в налаштування, в Rider, розділ build execution deployment, debugger. Опускаємося вниз і викликаємо set rider. Відповідно, воно перепрописує JIT на інший, той, що поставляється з Rider. Після цього наш incremental генератор буде працювати з Debugger Lounge. Ми робимо rebuild. Воно пропонує вибрати наш запущений debugger. І ми приєднуємося тільки що в Debugger. Відповідно, ще існує другий варіант – це debug через автоматичний debug. По суті, це відрізняється тим, що в райдері це було додано нещодавно, а в студії вже досить давно існує така практика. Ми можемо створити Lounge профіль, який буде запускати Source Generator, і нам тоді не потрібно взагалі писати Debugger Lounge.
Повертаємось до Rider. Ми прибираємо це і вибираємо Debug Generator. Після чого ми можемо поставити точку зупинки і запустити повноцінний дебаг, ніби ми запускаємо програму. Як це працює? У нас є Lounge Settings – це новий тип, відносно новий, з'явився, здається, в 2019 році. Debug Roasting Component. І ми вказуємо також шлях до нашого консольного проекту, тобто до проекту, в якому наш генератор приєднаний. Це дозволяє використовувати автоматичний дебаг як в Rider, так і в студії, без необхідності використання інших класів та методів.
Далі. Згенеровані вихідні файли. Коли ми вже написали Source Generator, ми можемо захотіти подивитися, що саме було згенеровано. Для цього ми можемо використовувати наступне меню. В Rider це папка Source Generators, а в студії це розділ Analyzers. Там ми бачимо всі файли, що були згенеровані. У нас є також можливість зробити так, щоб всі файли, які ми згенерували, були в межах нашого проекту, тобто фізично розміщені в структурі проекту. Для цього нам потрібно зайти в CS Project і додати ось ці записи: Emit Compiler Generated Files і Output Path до цих файлів. Якщо ми зробимо Rebuild, то у нас буде створена папка з цими файлами. Вони у мене зараз падають, тому що конфлікт, але воно додає їх. Це все щодо моєї доповіді. За QR-кодом доступне посилання на список ресурсів, які я використав, а також на сам код проекту. Дуже хочу подякувати колегам з моєї компанії, які допомогли мені підготуватись. Дякую.