Компилируем TypeScript в натив: хардкор и мясо
Выжимка статьи: Компилируем TypeScript в натив: хардкор и мясо
Крупный проект с миллионами строк кода на C++ столкнулся с серьезными проблемами: лицензионными ограничениями на использование новых версий Qt для десктопного UI и низкой скоростью разработки пользовательских интерфейсов. В ответ на эти вызовы возникла идея перенести лучшие практики и автоматизированные инструменты из мира веб-разработки, такие как TypeScript, в сферу нативных приложений, исключая при этом браузерные прослойки. Основная цель — обеспечить возможность бесшовного вызова C++-кода из TypeScript и обратно, компилируя TS в нативный исполняемый файл.
Проект TSNative был разработан для решения этой задачи, предлагая новый UI-фреймворк с TypeScript-обвязкой и компилятор TypeScript для сборки всего кода в единый бандл. Выбран подход AOT-компиляции (Ahead-Of-Time), который преобразует весь код в машинный заранее, до запуска программы, что обеспечивает высокую производительность и полный контроль над приложением, в отличие от интерпретации или JIT-компиляции. TSNative функционирует как фронтенд компилятора, принимая TypeScript-код и преобразуя его в промежуточное представление LLVM IR, которое затем докомпилируется стандартным бэкендом LLVM в нативный бинарный файл.
Архитектура TSNative включает три ключевых компонента:
- TSNative STD: Реализация JavaScript-рантайма на C++, совместимая с TypeScript-типами, включающая базовые типы, сборку мусора и цикл обработки событий.
- Компилятор: Использует готовый компонент Microsoft для парсинга TypeScript-кода и построения AST, после чего генерирует LLVM IR. Компилятор сам является TypeScript-приложением, работающим под Node.js.
TSNative Declarator: Отвечает за сопоставление вызовов из TypeScript с C++-реализациями, генерируя .d.ts файлы на основе C++-кода, например:
class TS_DECLARE Number : public Object
{
public:
TS_METHOD TS_SIGNATURE("parseInt(s: string, radix?: number): number")
static Number* parseInt(String* str, Union* radix) noexcept;
};
Макросы вроде TS_SIGNATURE позволяют декларатору автоматически формировать или переносить сигнатуры.
Механизм связывания между языками осуществляется путем извлечения символов (в том числе "mangled" имен) из скомпилированных C++-библиотек с помощью утилиты nm и их последующей вставки в LLVM IR при генерации. Точка входа приложения находится на стороне C++, которая инициализирует рантайм TSNative, а скомпилированный TypeScript-код компилируется в статическую библиотеку (tsmain), вызываемую из C++-кода.
Проект, начатый в 2018 году, успешно реализовал полноценный AOT-компилятор для TypeScript, обеспечивая двустороннюю совместимость и компиляцию в нативный бинарник без использования V8 или других промежуточных рантаймов. Это позволило значительно ускорить разработку UI и бизнес-логики. Однако переиспользование существующего веб-кода оказалось затруднительным из-за частого использования JavaScript-вставок и типа any, что указывает на потенциальную перспективность компиляции напрямую JavaScript. TSNative — это рабочий прототип, подтверждающий жизнеспособность технического подхода.
Оригинал статьи: Компилируем TypeScript в натив: хардкор и мясо