Архитектура

Как это работает «под капотом» (высокоуровневое объяснение).

Основная архитектура Plugify разработана для обеспечения бесшовного взаимодействия между языковыми модулями, каждый из которых реализован как отдельная библиотека C++. В результате ядро не взаимодействует напрямую с плагинами; вместо этого все взаимодействия происходят через эти языковые модули. Аналогично, плагины не взаимодействуют напрямую с ядром, а полагаются на языковые модули для коммуникации. Этот модульный дизайн распространяется даже на сам язык C++, требуя выделенного языкового модуля для обработки плагинов на основе C++. Основная цель проекта Plugify — создать универсальную среду, обеспечивающую быстрое и эффективное межъязыковое взаимодействие.

Основные компоненты Plugify

Plugify состоит из нескольких ключевых компонентов, каждый из которых выполняет свою роль в системе:

  • Контекст Plugify (Plugify Context): Класс, отвечающий за инициализацию и завершение работы системы Plugify. Он предоставляет методы для логирования, доступа к компонентам системы и получения конфигурационной информации.
  • Провайдер Plugify (Plugify Provider): Предоставляет основную функциональность языковым модулям, включая поддержку логирования, доступ к базовой директории и механизмы для обнаружения и нахождения плагинов и модулей.
  • Менеджер плагинов (Plugin Manager): Контролирует загрузку и управление языковыми модулями и плагинами, обеспечивая правильный порядок инициализации.
  • Менеджер пакетов (Package Manager): Управляет как локальными, так и удаленными пакетами, выполняя такие задачи, как загрузка, удаление и обновление компонентов.

Система пакетов

Пакеты в Plugify содержат файлы-манифесты с расширениями, такими как .pplugin для плагинов и .pmodule для языковых модулей. Эти файлы-манифесты в формате JSON предоставляют важные метаданные, включая экспортируемые методы, спецификации языка и уникальные точки входа для каждого модуля. Имя пакета определяется из имени файла-манифеста, в то время как отдельный параметр friendly name используется для отображения. Эта избыточность гарантирует, что даже если манифест содержит ошибки, правильное имя пакета все равно может быть получено для отчета об ошибках. Более подробное обсуждение структуры манифеста будет представлено в отдельной статье.

Реализация языкового модуля

API Plugify облегчает разработку языковых модулей через интерфейс ILanguageModule. Этот интерфейс экспортируется библиотекой языкового модуля и используется ядром Plugify для взаимодействия. Для поддержания совместимости и предотвращения расхождений в символах, языковые модули должны быть скомпилированы с той же версией C++ и тем же компилятором, что и ядро. Различия в реализации STL и декорировании имен между версиями компиляторов обуславливают это требование.

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

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

На некоторых платформах, таких как Windows, имя основной библиотеки ядра имеет значение для импорта функций. В некоторых реализациях Plugify компонуется как объектная библиотека для выполнения этого требования. Однако также доступны опции динамической и статической компоновки, настраиваемые через систему сборки CMake.

Handles

Загрузка плагинов и языковых модулей

Менеджер плагинов загружает языковые модули в том порядке, в котором они указаны в определенных папках. Однако, поскольку языковые модули не зависят друг от друга, их точная последовательность инициализации не имеет значения. Каждый языковой модуль уникально идентифицируется параметром language name в своем файле-манифесте, и одновременно может быть загружен только один модуль для каждого языка. Этот дизайн позволяет легко заменять модули, предотвращая конфликты, вызванные дублирующимися языковыми модулями.

Перед загрузкой плагинов система сортирует их на основе зависимостей, используя поиск в глубину (DFS). Это гарантирует, что плагины загружаются только после того, как их зависимости станут доступны, что позволяет точно контролировать порядок выполнения функций жизненного цикла (onStart, onUpdate, onEnd). Однако циклические зависимости могут приводить к проблемам, что требует тщательного управления зависимостями. Зависимости объявляются в файлах-манифестах плагинов, опционально указывая требуемые версии. После загрузки плагинов индивидуальная выгрузка не поддерживается, чтобы предотвратить сбои, связанные с зависимостями. Вместо этого все плагины и языковые модули должны выгружаться одновременно.

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

Plugin Manager

Управление пакетами

Менеджер пакетов контролирует обработку локальных и удаленных пакетов, сканируя предопределенные папки на наличие файлов-манифестов (называемых в Plugify «пакетами»). Он извлекает информацию об удаленных пакетах из ссылок на репозитории, указанных в plugify.pconfig. Используя эти данные, он может загружать, обновлять и управлять пакетами по мере необходимости.

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

Для управления удаленными пакетами Plugify использует WinHttp на Windows и CURL на других платформах. Пакеты архивируются в формате ZIP и извлекаются с помощью библиотеки miniz. Менеджер пакетов использует библиотеку glaze для обработки манифестов JSONC, которые должны строго соответствовать предопределенным схемам. Если функциональность удаленных пакетов не нужна, ее можно отключить во время сборки ядра.

Package Manager

Версионирование и обновления

Plugify придерживается семантического версионирования 2.0. Разработчики плагинов и модулей должны использовать увеличение мажорной версии для несовместимых изменений API, минорной версии — для добавлений, и патч-версии — для обратно совместимых исправлений. Эта практика обеспечивает совместимость и плавные обновления для пользователей.

Механизм коммуникации

Система использует функции, основанные на соглашении о вызовах C, поддерживая архитектуры x64 и AArch64. Этот подход выбран потому, что большинство встраиваемых языков предоставляют C или C++ API, что обеспечивает широкую совместимость в различных средах.

В некоторых языковых модулях функции динамически генерируются во время выполнения для обработки маршалинга параметров между C и управляемыми средами. Библиотека AsmJit облегчает этот процесс. Следовательно, Plugify поддерживает ограниченный, но всеобъемлющий набор типов параметров, охватывающий почти все типы данных C и некоторые пользовательские типы, такие как строки, массивы и варианты. Они передаются как объекты C++ по ссылке, в то время как поддерживаемые структуры C также передаются по ссылке для упрощения соблюдения соглашений о вызовах. Возвращаемые значения функций всегда передаются по значению, чтобы предотвратить утечки памяти и поддерживать четкое владение памятью при взаимодействии с C++.

Функции и делегаты между плагинами передаются через указатели на функции C. Языковые модули занимаются выделением памяти и обеспечивают валидность функций до их выгрузки. Каждая функция должна иметь строгое описание параметров и их порядок, при этом вариативные параметры явно не поддерживаются. Однако существуют альтернативные решения для гибкой обработки параметров. Поскольку плагины взаимодействуют напрямую друг с другом для максимизации производительности, необходимо проявлять осторожность при экспорте методов между языками, чтобы предотвратить сбои и утечки памяти. Некоторые языковые модули могут вносить незначительные накладные расходы из-за преобразования типов и проверок во время выполнения, в то время как другие, например C++, не создают дополнительных затрат на вызов, обеспечивая скорость выполнения, близкую к нативной. Более подробное обсуждение маршалинга и взаимодействия плагинов рассматривается в разделе разработки языковых модулей.