- Go 98.5%
- HTML 1.5%
|
|
||
|---|---|---|
| .woodpecker | ||
| docs | ||
| examples | ||
| pkg | ||
| tests | ||
| .gitignore | ||
| .woodpecker.yml | ||
| go.mod | ||
| go.sum | ||
| LICENSE | ||
| README.md | ||
go-interpret-agregate
Единый Go-интерфейс для выполнения скриптов на JavaScript (Goja), Lua (Golua) и Go (Yaegi) — с ограничением ресурсов, подключением host-функций и строгой валидацией типов.
Возможности
- Три бэкенда — один интерфейс — переключайтесь между Goja, Golua и Yaegi без изменения кода
- Ограничение ресурсов — CPU для всех бэкендов, память для Golua, блокировка горутин для Yaegi
- Host-функции — регистрация Go-функций, вызываемых из скриптов через
internal.*иexternal.* - Строгая валидация типов — опциональные схемы параметров для ранней проверки до выполнения
- Безопасность по умолчанию — файловая система, сеть и системные вызовы заблокированы
Быстрый старт
go get git.ymnuktech.ru/ymnuk/go-interpret-agregate
package main
import (
"fmt"
"git.ymnuktech.ru/ymnuk/go-interpret-agregate/pkg/factory"
"git.ymnuktech.ru/ymnuk/go-interpret-agregate/pkg/interpreter"
)
func main() {
// Создаём интерпретатор (замените TypeGolua → TypeGoja → TypeYaegi для смены бэкенда)
interp, err := factory.NewInterpreter(factory.TypeGolua)
if err != nil {
panic(err)
}
defer interp.Close()
// Загружаем скрипт
script := `
function greet(name)
return "Hello, " .. name
end
`
if err := interp.PrepareScript(script); err != nil {
panic(err)
}
// Вызываем функцию
result, err := interp.Execute("greet", []interface{}{"World"}, nil)
if err != nil {
panic(err)
}
fmt.Println(result.Value) // Hello, World
}
Поддерживаемые бэкенды
| Бэкенд | Язык | Лимит CPU | Лимит памяти | Блокировка потоков |
|---|---|---|---|---|
| Goja | JavaScript (ES5.1) | ✅ По таймеру | ❌ Игнорируется | ✅ Не применимо (синхронный) |
| Golua | Lua | ✅ По таймеру * | ❌ Игнорируется * | ✅ Не применимо (нет потоков) |
| Yaegi | Go | ✅ Через context | ❌ Игнорируется | ✅ AST-анализ |
* Golua: лимит CPU реализован через goroutine + таймер, как у Goja. При таймауте возвращается ErrTimeout, но фоновый Lua-код продолжает выполняться до завершения — это известное ограничение gopher-lua. Параметр MaxMemory валидируется, но принудительно не применяется ни для одного бэкенда. После ErrTimeout интерпретатор Golua требует повторного PrepareScript (ErrScriptNotPrepared) — состояние после таймаута не переиспользуется (BUG-6, docs/ROADMAP.md).
Важные ограничения
MaxMemoryнигде не применяется принудительно. Память управляется Go runtime и не может быть ограничена для отдельной горутины; для Golua параметр только валидируется. Если ограничение критично — изолируйте выполнение в отдельном процессе (cgroups/ulimit).Threadsприменяется только к Yaegi (блокируетgo/selectчерез AST-анализ). У Goja нет event loop, у Lua нет многопоточности.- Yaegi: таймаут через context не останавливает выполняемый код — goroutine продолжает работать после
ErrTimeout(как и у Golua). Прерывает выполнение только Goja. Это технический долг — см. «Известные ограничения» вdocs/ROADMAP.md. - Goja гарантирует поддержку ES5.1. Возможности ES6+ (
let,const,class,async/await) — в разработке. Для гарантированного выполнения используйте транспиляцию черезtsc --target ES5 --module none. - Yaegi (BUG-2): host-функции возвращают
(interface{}, error). Однозначное присваиваниеreturn tools.Echo(...)вызывает панику внутри yaegi — она перехватывается и возвращается какErrExecution, но полагаться на это не стоит: обязательно присваивайте оба значения —v, err := tools.Echo(...). - Yaegi (BUG-3): host-namespace, совпадающий со stdlib-пакетом (
os,math,fmt, …), «затеняет» его:import "<namespace>"даёт только host-функции, настоящий stdlib-пакет в этот интерпретатор не загружается. - Реализовано (REQ-1): перехват печати из скриптов (
print,fmt.Println,console.*) в буфер хоста черезConfig.Stdout/Stderrи опциюWithOutput— см. раздел «Перехват вывода».
Использование
Фабрика
// Простое создание
interp, err := factory.NewInterpreter(factory.TypeGoja)
// С конфигурацией (лимиты + host-функции)
config := factory.Config{
Type: factory.TypeGolua,
Limits: interpreter.Limits{
MaxCPUTime: time.Second,
MaxMemory: 64 * 1024 * 1024,
Threads: false, // безопасный режим по умолчанию
},
Functions: []factory.FunctionConfig{
{
Namespace: "internal",
Name: "ReadFile",
Fn: myReadFileFn,
Params: []interpreter.Param{{Type: "string"}},
},
},
}
interp, err := factory.NewInterpreterFromConfig(config)
Жизненный цикл
1. NewInterpreter() → создание экземпляра
2. AddFunction(...) → регистрация host-функций (опционально)
3. PrepareScript(script) → парсинг и загрузка скрипта
4. Execute("funcName", args) → вызов функции (повторяемый)
5. Close() → освобождение ресурсов
Host-функции
Регистрация Go-функций, вызываемых из скриптов:
// Регистрируем функцию со строгой валидацией типов
interp.AddFunction("internal", "ReadFile", func(args []interface{}) (interface{}, error) {
path := args[0].(string)
return os.ReadFile(path)
}, []interpreter.Param{{Type: "string"}})
// Вызов из JavaScript:
// var data = internal.ReadFile("/path/to/file")
// Вызов из Lua:
// local data = internal.ReadFile("/path/to/file")
// Вызов из Go (Yaegi):
// import "internal"
// data, err := internal.ReadFile("/path/to/file")
// // ВАЖНО: обязательно присваивайте оба значения (v, err := ...).
// // Однозначное присваивание (v := internal.ReadFile(...)) → ErrExecution.
Порядок экспорта (BUG-5): host-функции экспортируются в интерпретатор до исполнения top-level кода скрипта, поэтому доступны не только внутри вызываемой функции, но и в верхнеуровневом коде (например
console.log("...")на top-level с host-неймспейсомconsole.*). Если top-level код падает во времяPrepareScript, ошибка возвращается какErrScriptRuntime(неErrScriptParse— скрипт синтаксически валиден).
Строгая валидация типов
При указании params метод Execute проверяет типы аргументов до выполнения скрипта:
// Корректно: float64(5.0) → int (без потери точности)
interp.Execute("fn", []interface{}{float64(5.0)}, nil)
// Ошибка: float64(5.1) → int (потеря дробной части)
// → error: argument 0: expected int, got 5.1
Поддерживаемые типы: int, int8–int64, uint–uint64, float32, float64, string, bool, []byte, []string, []int, []int64, []float64, map[string]interface{}, interface{}.
Ограничение ресурсов
result, err := interp.Execute("heavyFn", args, &interpreter.ExecuteOptions{
Limits: interpreter.Limits{
MaxCPUTime: 500 * time.Millisecond,
MaxMemory: 32 * 1024 * 1024, // ⚠️ Валидируется, но не применяется ни одним бэкендом
},
})
if errors.Is(err, interpreter.ErrTimeout) {
// Обработка таймаута
}
Перехват вывода (stdout/stderr)
Печать из скриптов (print, fmt.Println, console.*) перенаправляется в
заданные io.Writer вместо реального os.Stdout/os.Stderr (важно для
transport stdio, где stdout занят JSON-RPC). Настраивается при создании
интерпретатора:
var logs bytes.Buffer
// Через функциональную опцию
interp, _ := factory.NewInterpreter(factory.TypeYaegi, factory.WithOutput(&logs, &logs))
// Или через Config
config := factory.Config{
Type: factory.TypeGolua,
Stdout: &logs, // Golua: только stdout (в Lua нет stderr)
}
Хост читает буфер после выполнения (сделайте Reset() перед каждым Execute —
интерпретатор не concurrency-safe):
| Бэкенд | Что перехватывается |
|---|---|
| Yaegi | fmt.Print/Printf/Println, встроенные print/println, log.* (log → stderr) |
| Golua | встроенный print (только stdout; stderr игнорируется) |
| Goja | console.log/info/debug → stdout, console.warn/error → stderr — регистрируются автоматически, если задан Stdout |
Goja (правило условной регистрации): если хост сам регистрирует
console.*черезAddFunction/Config.Functions, его версия имеет приоритет (per-name: задалиconsole.log— остальные методы остаются нашими).Внимание: при таймауте (
ErrTimeout) фоновая горутина Golua/Yaegi может продолжать писать в буфер после возврата изExecute. Не переиспользуйте буфер, пока выполнение гарантированно не завершилось. Подробнее —docs/ROADMAP.md.Golua (BUG-6): лимитный
Executeисполняет top-level код ровно один раз (вPrepareScript), без повторного запуска скрипта; послеErrTimeoutинтерпретатор переходит в состояние "не подготовлен" и требует повторногоPrepareScript. Подробнее —docs/ROADMAP.md→ BUG-6.
Yaegi: блокировка горутин
По умолчанию Threads=false — скрипты Yaegi не могут использовать go и select:
// Этот скрипт будет отклонён:
script := `package main; func Run() { go func(){}() }`
err := interp.PrepareScript(script)
// → ErrThreadsNotAllowed
Для разрешения горутин (небезопасно — фоновые горутины не останавливаются при таймауте):
y := yaegi.New()
y.AllowThreads()
Обработка ошибок
Все бэкенды возвращают унифицированный interpreter.Result:
type Result struct {
Value interface{} // Возвращаемое значение (скаляр, map, слайс и т.д.)
Error string // Текст ошибки (пусто при успехе)
TimedOut bool // True если выполнение прервано по таймауту
}
Сентинел-ошибки для type checking:
errors.Is(err, interpreter.ErrTimeout) // выполнение прервано по таймауту
errors.Is(err, interpreter.ErrThreadsNotAllowed) // go/select заблокированы
errors.Is(err, interpreter.ErrMemoryLimit) // превышен лимит памяти (Golua)
errors.Is(err, interpreter.ErrFunctionNotFound) // функция не найдена
errors.Is(err, interpreter.ErrScriptParse) // синтаксическая ошибка
errors.Is(err, interpreter.ErrScriptRuntime) // runtime-ошибка top-level кода при PrepareScript
errors.Is(err, interpreter.ErrNameConflict) // скрипт определяет namespace.name или перезаписывает namespace (Goja/Golua)
Структура проекта
├── pkg/
│ ├── interpreter/ # Базовый интерфейс, типы, валидация
│ ├── goja/ # JavaScript бэкенд (Goja)
│ ├── golua/ # Lua бэкенд (Golua)
│ ├── yaegi/ # Go бэкенд (Yaegi)
│ └── factory/ # Фабричные функции
├── tests/
│ ├── integration/ # Интеграционные тесты
│ └── benchmarks/ # Кросс-бэкенд бенчмарки
├── examples/ # Примеры использования
└── docs/ # Проектная документация
Тестирование
# Unit-тесты
go test -v ./pkg/...
# Интеграционные тесты
go test -v ./tests/integration/...
# Бенчмарки
go test -bench=. -benchmem ./tests/benchmarks/...
# Покрытие
go test -coverprofile=coverage.out ./pkg/... ./tests/...
go tool cover -html=coverage.out
CI/CD
Woodpecker CI (.woodpecker.yml):
- Линтинг —
go fmt+go vet - Unit-тесты —
go test -short -v ./... - Покрытие —
go test -coverprofile(только push) - Бенчмарки —
go test -bench=. -benchmem(только main) - Email-уведомления — отчёты по покрытию и бенчмаркам
Документация
docs/ANALYSIS.md— детальный анализ каждого бэкендаdocs/ROADMAP.md— дорожная карта и архитектурные решения
Лицензия
См. файл LICENSE.