Вложенный TCC и система плагинов: компилятор внутри скрипта
xsCreateTCC выдаёт скрипту полностью подготовленный компилятор — динамические правила, вычисление выражений, сторонние плагины идут дорогой «компиляции нативного кода в рантайме».
Обзор
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.
Добавьте кэш «выражение → TCCState» (LRU ≤ 100) со статистикой попаданий.
Повторите кейс func_test: 4 потока конкурентных вычислений × 12 запросов, между делом 5 смен поколения.
Итоги
| Тезис | Вывод |
|---|---|
| Вход | xsCreateTCC: предустановленные include + весь набор символов хоста |
| Пять шагов | создать→скомпилировать→перелокировать→взять символ→(по завершении) уничтожить |
| Единица кэша | TCCState (не указатель на функцию) |
| Конкурентная безопасность | состояние плагина по поколению; в func_test есть конкурентный приёмочный кейс |
| Граница | права открыты по умолчанию; сужение — собственный белый список символов (см. гл. 44) |
- справочник xrt · memory — сверка памяти для кэшей плагинов и путей уничтожения