ABI / Host imports

0.4 NEW — точный контракт между WASM-плагином и lampac-go.

ABI стабилен начиная с abi_version: 1 (поле в манифесте). При изменении контракта в обратно-несовместимую сторону хост поднимет CurrentABIVersion, и плагины со старой версией будут видны в админке как “incompatible”.

Память и упаковка указателей

Плагины и хост обмениваются JSON-байтами через linear memory гостя. Хост никогда не аллоцирует у себя — он зовёт экспорт гостя alloc(size) i32 и пишет туда. Указатель и длина возвращаются из функций упакованными в один i64:

high32(result) = ptr   (offset в guest memory)
low32(result)  = len   (количество байт)

Хелперы:

// В Go-хосте:
func PackPtrLen(ptr, length uint32) uint64 { return (uint64(ptr) << 32) | uint64(length) }
func UnpackPtrLen(v uint64) (ptr, length uint32) { return uint32(v >> 32), uint32(v & 0xFFFFFFFF) }
// В JS-loader для client-target:
function packPtrLen(ptr, len) { return (BigInt(ptr) << 32n) | BigInt(len); }

Обязательные экспорты гостя

Экспорт Сигнатура Когда вызывается
alloc (size i32) -> i32 Хост резервирует буфер для входных данных и для возврата из host-импортов
handle (ptr i32, len i32) -> i64 Главный entry point. Вход — JSON Invocation, выход — pack(ptr,len) JSON Response

Опциональные экспорты:

Экспорт Что
abi_version () -> i32 — гость может отдать свою ABI; хост сверяет с manifest
_initialize TinyGo / Rust — reactor-mode инициализация
on_load / on_event Только для client-target; см. client

Хост-импорты (модуль lampac для server-target)

host_log(level i32, ptr i32, len i32)

level: 0=debug 1=info 2=warn 3=error

Пишет строку в logsink модуля (виден в админке) и в общий zerolog. Stdout/stderr плагина тоже перенаправлены сюда — fmt.Println работает.

host_http(req_ptr i32, req_len i32) -> i64

Синхронный HTTP-запрос. Гейтится permissions — см. permissions.

Вход (JSON):

{
  "method": "GET",
  "url": "https://api.example.com/v1/movies",
  "headers": {"User-Agent": "lampac"},
  "body": "raw string body",
  "body_b64": "base64 alt",
  "timeout_ms": 15000,
  "transport": "default"
}

Выход (JSON):

{
  "status": 200,
  "headers": {"content-type": "application/json"},
  "body_b64": "...base64...",
  "error": ""
}

Возврат — pack(ptr,len) JSON-ответа в guest memory. Cap на тело — 16 МиБ.

host_proxy_url(req_ptr i32, req_len i32) -> i64

Подписывает потоковый URL через proxylink.Manager, чтобы Lampa-клиент ходил к нему через /proxy/ (с правильными headers, cookies, Origin).

Вход:

{
  "uri": "https://cdn.example.com/movie.m3u8",
  "plugin": "my_plugin",            // опционально, по умолчанию = manifest.id
  "headers": {"Origin": "https://example.com"}
}

Выход:

{ "url": "https://lampac-host/proxy/<aes-blob>" }

host_cache_get(key_ptr, key_len) -> i64

Возвращает pack(ptr,len) сырых байт, которые гость записал через host_cache_set. На промахе возвращает 0. Хост не интерпретирует значение — гость сериализует/десериализует сам.

host_cache_set(key_ptr, key_len, val_ptr, val_len, ttl_sec)

TTL по умолчанию (если 0) — 300 сек. Кеш per-module, чистится при reload.

host_config() -> i64

Возвращает текущий снимок конфига модуля как JSON: manifest.config_schema.default + manifest.defaults + admin-overrides из wasm_modules/{id}/config.json. То же значение приходит как Invocation.config.

Хост-импорты (модуль lampac_client для client-target)

Используется в Lampa через wasm_loader.js.

Импорт Что делает
host_log(level, ptr, len) console.{debug,info,warn,error} с префиксом [<id>]
host_storage_get(key_ptr, key_len) -> i64 Lampa.Storage.get('wasm_plugin_<id>:'+key)
host_storage_set(key_ptr, key_len, val_ptr, val_len) Lampa.Storage.set(...)
host_listener_emit(event_ptr, event_len, data_ptr, data_len) Lampa.Listener.send(event, JSON.parse(data))
host_noty(msg_ptr, msg_len) Lampa.Noty.show(msg)
host_activity_push(json_ptr, json_len) Lampa.Activity.push(JSON.parse(...))

Каждому плагину выдаётся свой namespace в Lampa.Storage (wasm_plugin_<id>:*), чтобы плагины не топтали ключи друг друга.

Invocation / Response

Серверные плагины получают на вход:

{
  "query":     {"id": "123", "s": "1"},
  "headers":   {"user-agent": "..."},
  "host":      "https://example.com",
  "requestIP": "1.2.3.4",
  "path":      "my_plugin",
  "life":      false,
  "checksearch": false,
  "userAgent": "...",
  "config":    { /* host_config() snapshot */ }
}

Возвращают любую JSON-структуру, которую Lampa поймёт. Типичный формат для списков:

{
  "type": "movie",
  "data": [
    {"name": "Title", "url": "/proxy/...", "quality": {"1080p": "...m3u8"}}
  ],
  "voice": [{"name": "Дубляж", "url": "?id=123&voice=1", "active": true}]
}

Для прямого воспроизведения:

{ "method": "play", "url": "/proxy/...", "title": "...", "quality": "1080p" }

Middleware ABI

Когда target: middleware, хост до вызова handle() дёргает каждого upstream’а и складывает их ответы в inv.Config.__upstreams:

{
  "__upstreams": [
    {"balancer": "collaps", "status": 200, "body": {...}},
    {"balancer": "filmix",  "status": 503, "error": "timeout"}
  ]
}

В TinyGo SDK для этого есть lampac.Upstreams(inv) []UpstreamResult. См. middleware.

ABI-version bumps

abi_version Что меняется
1 (текущая) Базовый набор — host_log, host_http, host_proxy_url, host_cache_*, host_config для server; host_storage_* + host_noty + host_activity_push + host_listener_emit для client

Когда поднимется до 2, плагины с abi_version: 1 продолжат работать через shim — старые импорты остаются.