Глава 40 / 51
Том 5 · Расширения, эксплуатация и практика

Вложенный TCC и система плагинов: компилятор внутри скрипта

xsCreateTCC выдаёт скрипту полностью подготовленный компилятор — динамические правила, вычисление выражений, сторонние плагины идут дорогой «компиляции нативного кода в рантайме».

Обзор

Позиционирование: «возвышенная» форма управляющего маршрута из гл. 27 — когда плоскость управления начинает компилировать код, она становится платформой плагинов. В func_test есть реальный кейс конкурентности вложенного TCC с reload (4 потока × 12 запросов); глава берёт его за планку приёмки.

40.1 Введение

Есть класс потребностей, не закрываемых «зашитым кодом»: правила риск-контроля меняются еженедельно, формулы отчётов задаёт пользователь, третьи лица хотят писать расширения для вашей платформы. Традиционный ответ — встроить интерпретатор (Lua/JS) — ценой второго рантайма и накладных расходов на границе. Ответ XServer напрашивается: хост компилирует скрипты TCC, и скрипты могут компилировать код TCC — один язык, один набор символов, ноль границы. xsCreateTCC предустанавливает пути include и все символы хоста: вложенный код с рождения умеет звать ваши API.

40.2 Пять шагов компиляции плагина

Плагин-выражение (полный рабочий паттерн)
/* потребность: пользователь даёт C-выражение, сервер вычисляет */
typedef int (*ExprFn)(int);

static bool CompileExpr(const char* sExpr, ExprFn* ppFn)
{
    char sSource[1024];
    TCCState* pTcc;

    snprintf(sSource, sizeof(sSource),
        "int eval(int x) { return %s; }", sExpr);
    /* 1. создать (include и символы предустановлены) */
    pTcc = xsCreateTCC();
    if ( pTcc == NULL ) return false;
    /* 2. скомпилировать строку */
    if ( tcc_compile_string(pTcc, sSource) < 0 ) {
        xsDestroyTCC(pTcc);
        return false;             /* синтаксис — назад 400 */
    }
    /* 3. перелокировать в память */
    if ( tcc_relocate(pTcc, TCC_RELOCATE_AUTO) < 0 ) {
        xsDestroyTCC(pTcc);
        return false;
    }
    /* 4. взять символ (тип указателя объявите заранее — сигнатурный договор ваш) */
    *ppFn = (ExprFn)tcc_get_symbol(pTcc, "eval");
    /* 5. pTcc держит вызывающий: пока символ используется — не уничтожать */
    return *ppFn != NULL;
}
/* по завершении: xsDestroyTCC(pTcc) — после него ppFn мёртв немедленно */

Что только что произошло: ключевая дисциплина пяти шагов (создать→скомпилировать→перелокировать→взять символ→уничтожить) в том, что жизнь символа привязана к TCCState — результат живёт в памяти, которой владеет TCCState; Destroy его убивает. Поэтому правильная структура кэша плагинов — «кэшировать TCCState», а не «кэшировать указатель на функцию».

40.3 Колбэки ошибок и внедрение символов

  • Перехват вывода ошибок: tcc_set_error_func направляет диагностику компиляции в ваш сборщик (умолчание — stderr); вернуть диагностику пользователю, чьё выражение не скомпилировалось, — часть качества продукта
  • Внедрение символов: tcc_add_symbol позволяет плагину звать определённые вами функции хоста — маленький набор с префиксом plugin_api_* и есть ваш плагинный SDK
  • Права по умолчанию полностью открыты: xsCreateTCC внедряет весь набор символов хоста — сужайте сами (add_symbol только белого списка); это сырьё для границы песочницы плагинов (полное обсуждение — гл. 44)
  • VFS доступен: код плагина так же включает заголовки из VFS; tcc_vfs_mount_memory умеет в рантайме подкидывать «виртуальные файлы» — именно так хост загружает скрипты

40.4 Конкурентность с горячей перезагрузкой

Вопрос безопасности при гонке вложенного компилятора с reload: не будет ли функция хоста, на которую разрешался символ плагина, утилизирована при смене поколения? Кейс func_test даёт ответ и планку: 4 потока конкурентно гоняют вложенную компиляцию × 12 запросов при непрерывных reload — на всём пути ноль отказов. Механика:

  • TCCState плагина — состояние вашего поколения: положен в реестр и уничтожен со сменой поколения — безопасность автоматическая (DestroyTCC в ServiceUnit)
  • Символы, разрешённые плагином старого поколения, указывают на функции старого — lease старого поколения держит их до совместного выхода на пенсию
  • Новое поколение перекомпилирует плагины (или лениво по требованию) — изоморфно пересборке таблицы маршрутов

40.5 Контракт

  • Жизненный цикл: указатели на функции живут в TCCState; единица кэша — TCCState; после Destroy указатели мертвы.
  • Поколения: состояние плагина ведётся по поколению (создание в Init / уничтожение в Unit) — тогда гонка с reload безопасна.
  • Сигнатуры: тип указателя после выборки объявляете вы — фиксируйте договор в документации; ошибка = неопределённое поведение.

Частые ошибки

Закэширован указатель на функцию, компилятор уничтожен
Симптом
Попадание кэша плагина прыгает в освобождённую память.
Причина
Память результата принадлежит TCCState. И ключ, и значение кэша обязаны быть «выражение → TCCState», время жизни — как у кэша.
Вызов с неверной сигнатурой указателя
Симптом
Аргументы смещаются, стек портится, случайные падения.
Причина
tcc_get_symbol даёт только адрес, сигнатуры не проверяет — типовой договор целиком ваш. Зафиксируйте в SDK один шаблон сигнатуры (например int fn(int)) и жёстко задокументируйте.

Упражнения

БазовыйСервис выражений

Реализуйте POST /expr {code:"x*2+1", x:5} → 11.

Приёмка: верное значение; синтаксическая ошибка даёт 400 + диагностику.
ПродвинутыйКэш компиляций

Добавьте кэш «выражение → TCCState» (LRU ≤ 100) со статистикой попаданий.

Приёмка: повторное выражение не компилируется; при заполнении уничтожается наиболее давно использованный.
ВызовШторм конкурентности + reload

Повторите кейс func_test: 4 потока конкурентных вычислений × 12 запросов, между делом 5 смен поколения.

Приёмка: ноль отказов; в Unit старого поколения видно вызовы DestroyTCC.

Итоги

ТезисВывод
ВходxsCreateTCC: предустановленные include + весь набор символов хоста
Пять шаговсоздать→скомпилировать→перелокировать→взять символ→(по завершении) уничтожить
Единица кэшаTCCState (не указатель на функцию)
Конкурентная безопасностьсостояние плагина по поколению; в func_test есть конкурентный приёмочный кейс
Границаправа открыты по умолчанию; сужение — собственный белый список символов (см. гл. 44)
API, затронутые в главе
Что дальше по xrt