Встраивание Janet в программу на C
открытый урокЭтот раздел читается без входа. Войди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти
Встраивание Janet в программу на C
Обратное направление: у тебя есть программа на C (редактор, игровой движок, синтезатор, демон), и ты хочешь дать пользователю возможность её скриптовать. Это ниша, ради которой Janet во многом и создавался.
Представь: ты написал на C синтезатор. Пользователь просит дать ему возможность описывать цепочки эффектов кодом, а не мышкой. Переписывать синтезатор на другой язык из-за этого никто не будет. Вместо этого твоя программа становится хостом: поднимает внутри себя виртуальную машину Janet и выполняет в ней пользовательские скрипты.
Вся картина урока помещается в одну схему. Хост кладёт функции и данные вниз, скрипт выполняется, хост забирает результат наверх:
┌──────────────────────────────┐
│ хост: твоя программа на C │
└──────────────────────────────┘
│ janet_cfuns, janet_def ▲ janet_unwrap_*
│ (функции и данные вниз) │ (результат наверх)
▼ │
┌──────────────────────────────┐ │
│ VM Janet: окружение-таблица │ ────┘
│ выполняет скрипт │ janet_dostring
└──────────────────────────────┘
Каждую стрелку разберём кодом.
Минимальное встраивание
#include <janet.h>
#include <stdio.h>
int main(void) {
janet_init();
JanetTable *env = janet_core_env(NULL);
janet_dostring(env, "(print \"привет из встроенного Janet\")", "встроенный", NULL);
janet_deinit();
return 0;
}
Четыре вызова:
janet_initподнимает виртуальную машину, один раз на процесс.janet_core_env(NULL)создаёт окружение со стандартной библиотекой.janet_dostring(env, code, source_name, out)компилирует и выполняет строку. Третий аргумент это имя, которое появится в сообщениях об ошибках.janet_deinitосвобождает всё.
cc -O2 -o host host.c -ljanet
Заметь: в C-листингах этого урока идентификаторы латиницей. Janet принимает кириллицу в именах всегда, а вот компиляторы C понимают её не все, поэтому кириллица останется только внутри строк с Janet-кодом.
Коды возврата
| Код | Что случилось |
|---|---|
0 | успех |
1 | ошибка во время выполнения |
2 | ошибка компиляции |
Ошибка в скрипте не роняет программу. Если скрипт вызвал (error "сломалось"), Janet напечатает трассировку в stderr, вернёт 1, и твоя программа продолжит работать. Это ровно то поведение, которое нужно от встроенного языка: кривой пользовательский скрипт не должен убивать приложение.
Значения между C и Janet
Четвёртый аргумент janet_dostring это куда положить результат:
Janet out;
janet_dostring(env, "(+ 20 22)", "встроенный", &out);
printf("тип: %s\n", janet_type_names[janet_type(out)]); /* number */
printf("значение: %g\n", janet_unwrap_number(out)); /* 42 */
Обрати внимание: имя типа берётся из массива janet_type_names, а не из функции.
Строки распаковываются похоже и приходят завершёнными нулём, так что годятся для %s:
Janet s;
janet_dostring(env, "(string \"Janet \" janet/version)", "встроенный", &s);
printf("%s\n", (const char *)janet_unwrap_string(s)); /* Janet 1.41.2 */
Семейства функций, которые понадобятся:
janet_wrap_*упаковывает значение C вJanet:janet_wrap_number,janet_wrap_nil,janet_wrap_table.janet_unwrap_*распаковывает обратно.janet_c*vсобирает значение из строки C:janet_cstringv,janet_ckeywordv.
Свои функции в скрипте
Чтобы скрипт мог управлять приложением, зарегистрируй C-функции в его окружении тем же способом, что и в нативном модуле:
static double volume = 0.5;
static Janet cfun_set_volume(int32_t argc, Janet *argv) {
janet_fixarity(argc, 1);
volume = janet_getnumber(argv, 0);
return janet_wrap_nil();
}
static const JanetReg app_funs[] = {
{"set-volume", cfun_set_volume, "(set-volume x)"},
{NULL, NULL, NULL}
};
/* в main, после janet_core_env: */
janet_cfuns(env, "app", app_funs);
Префикс в janet_cfuns не попадает в имя. Второй аргумент это имя для реестра, а не префикс символа. В окружение имена кладутся голыми:
(set-volume 0.8) # так работает
(app/set-volume 0.8) # unknown symbol
В нативном модуле из прошлого урока префикс g/ брался вовсе не отсюда, его давал import с опцией :as g. При встраивании импорта нет, поэтому и префикса нет. Хочешь пространство имён, включай его прямо в имя: {"app/set-volume", ...}.
Данные из C в скрипт
Отдельные значения удобно класть в окружение через janet_def:
JanetTable *config = janet_table(2);
janet_table_put(config, janet_ckeywordv("name"), janet_cstringv("моё-приложение"));
janet_table_put(config, janet_ckeywordv("version"), janet_wrap_number(3));
janet_def(env, "config", janet_wrap_table(config), "Конфигурация от хоста.");
Скрипт видит это как обычное определение:
(printf "%V версии %p" (config :name) (config :version))
# моё-приложение версии 3
Четвёртый аргумент janet_def это докстрока, и она работает: (doc config) её покажет.
Песочница
Пользовательскому скрипту нужна песочница. Доступа ко всей стандартной библиотеке у него быть не должно: читать файлы и запускать процессы ему ни к чему.
Решение простое: не давай ему полное окружение. Собери своё, положив только разрешённое.
Сначала на самом Janet
Механику удобнее понять без C: тут её можно потрогать в REPL. Окружение это обычная таблица, поэтому песочница строится вручную:
(defn make-sandbox [allowed]
(def sandbox @{})
(each name allowed
(when-let [binding (get root-env name)]
(put sandbox name binding)))
sandbox)
(def sb (make-sandbox ['+ '* 'print]))
Выполнение в этом окружении идёт через compile из урока про окружения:
(defn sandbox-eval [sandbox form]
(def result (compile form sandbox))
(if (function? result)
(let [[ok value] (protect (result))]
(if ok [:ok value] [:error value]))
[:error (get result :error)]))
(sandbox-eval sb '(* (+ 1 2) 3)) # (:ok 9)
(sandbox-eval sb '(os/shell "echo")) # (:error "unknown symbol os/shell")
Схема из трёх шагов: урезанное окружение, компиляция в нём, перехват ошибок. Обрати внимание, что ошибки бывают двух видов и обрабатываются в разных местах: компиляция вернула не функцию, а таблицу с описанием, а выполнение упало уже внутри protect.
Теперь перекладываем в C
Со стороны хоста те же три шага выглядят так:
JanetTable *sandbox = janet_table(0);
janet_def(sandbox, "+", janet_resolve_core("+"), NULL);
janet_def(sandbox, "print", janet_resolve_core("print"), NULL);
janet_dostring(sandbox, "(print (+ 1 2))", "песочница", NULL); /* 3 */
janet_dostring(sandbox, "(os/shell \"rm -rf /\")", "песочница", NULL); /* 2 */
Последний вызов вернёт 2, ошибку компиляции unknown symbol os/shell. Опасной функции в окружении нет, а значит скрипт до неё не дотянется.
Пособирай окружение сам и посмотри, что проходит:
Белый список, а не чёрный. Перечисляй разрешённое, а не запрещённое. Список запретов всегда неполон: кроме os/shell есть os/execute, file/open, net/connect, ffi/native, require, и любая забытая функция это дыра. Тот же принцип мы формулировали в разделе про безопасность: валидация по белому списку, отказ по умолчанию.
Обрати особое внимание на require: его тоже надо держать вне песочницы, иначе скрипт подтянет что угодно из файловой системы и обойдёт весь твой белый список за одну строку.
Образы для быстрого старта
Последний трюк хоста. Если скриптовый слой при каждом запуске приложения делает одну и ту же дорогую подготовку (компилирует грамматики, строит таблицы, загружает модули), результат можно сохранить один раз и потом загружать готовым:
Сам Janet устроен так же: его стандартная библиотека вкомпилирована в исполняемый файл как образ, и потому интерпретатор стартует за миллисекунды. Со стороны C загрузка образа делается через janet_unmarshal: это пара к janet_dostring, только вместо прогона скриптов подготовки хост восстанавливает готовое состояние из байтов.
Что запомнить
- Минимум это
janet_init,janet_core_env,janet_dostring,janet_deinit. - Коды возврата: 0 успех, 1 ошибка выполнения, 2 ошибка компиляции. Ошибка скрипта программу не роняет.
- Обмен значениями идёт через
janet_wrap_*,janet_unwrap_*,janet_c*v. Имя типа лежит в массивеjanet_type_names. janet_cfunsкладёт имена голыми, префикс надо писать прямо в имени.janet_defкладёт значение и докстроку в окружение.- Песочница это урезанное окружение, а не список запретов.
requireдержи снаружи. - Схема песочницы одна и та же на Janet и в C: своё окружение, компиляция в нём, перехват обоих видов ошибок.
- Образ ускоряет старт хоста, потому что дорогая подготовка делается один раз.
Упражнения
Дальше
Граница с C пройдена в обе стороны. Дальше три прикладных урока: тестирование и отладка, скриптинг и макросы, которые пишут определения.
домашка