No description
  • Go 98.5%
  • HTML 1.5%
Find a file
Ymnuk f50cfb55a4
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
fix: CI
2026-08-04 20:41:12 +03:00
.woodpecker Документация 2026-04-07 13:37:25 +03:00
docs fix: Некоторое исправления двойного запуска в Lua 2026-08-04 20:39:52 +03:00
examples fix: Runtime-ошибки top-level кода не маскируются под синтаксические; host-функции экспортируются до исполнения top-level кода 2026-08-04 19:34:12 +03:00
pkg fix: Некоторое исправления двойного запуска в Lua 2026-08-04 20:39:52 +03:00
tests fix: Некоторое исправления двойного запуска в Lua 2026-08-04 20:39:52 +03:00
.gitignore fix: Потеря host-функций в одном namespace, Однозначное присваивание двухвозвратной host-функции паникует, Host-namespace, совпадающий со stdlib-пакетом, открывает настоящий stdlib в обход песочницы 2026-08-02 20:24:40 +03:00
.woodpecker.yml fix: CI 2026-08-04 20:41:12 +03:00
go.mod fix: Runtime-ошибки top-level кода не маскируются под синтаксические; host-функции экспортируются до исполнения top-level кода 2026-08-04 19:34:12 +03:00
go.sum fix: Runtime-ошибки top-level кода не маскируются под синтаксические; host-функции экспортируются до исполнения top-level кода 2026-08-04 19:34:12 +03:00
LICENSE Документация 2026-04-06 13:16:46 +03:00
README.md fix: Некоторое исправления двойного запуска в Lua 2026-08-04 20:39:52 +03:00

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, int8int64, uintuint64, 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):

  1. Линтингgo fmt + go vet
  2. Unit-тестыgo test -short -v ./...
  3. Покрытиеgo test -coverprofile (только push)
  4. Бенчмаркиgo test -bench=. -benchmem (только main)
  5. Email-уведомления — отчёты по покрытию и бенчмаркам

Документация

  • docs/ANALYSIS.md — детальный анализ каждого бэкенда
  • docs/ROADMAP.md — дорожная карта и архитектурные решения

Лицензия

См. файл LICENSE.