Developing a Salamatrix language runtime provider

Salamatrix language runtimes are optional Open Salamander plugins. A runtime provider registers one language adapter with Salamatrix.Runtime, starts the language interpreter out of process, and translates between the interpreter and Salamander through the language-neutral SMX1 protocol.

This design keeps language engines out of SALAMATRIX.SPL and the Automation plugin. It also means that a provider can support a system-installed interpreter without linking its SDK or redistributing it. The built-in PowerShell, CPython, PHP CLI, Node, and Lua providers are working reference implementations.

Choose the process model

An out-of-process CLI adapter is the recommended starting point for a new language:

An in-process adapter is possible through IRuntimeAdapter, but it owns the additional ABI, allocator, threading, shutdown, and interpreter-lifetime risks. Do not expose Salamander C++ objects or COM pointers directly to scripts.

Components

A complete provider contains four parts:

1. A normal .SPL plugin that owns an IRuntimeAdapter. 2. Interpreter discovery and process/session management. 3. A language-specific worker bootstrap that exposes the Salamander facade. 4. Build, packaging, catalog, documentation, and test integration.

Use these files as references:

1. Describe the adapter

Implement Salamatrix::Runtime::IRuntimeAdapter and return a stable RuntimeAdapterDescriptor:

m_descriptor.RuntimeId = "Example.CLI";
m_descriptor.DisplayName = "Example CLI runtime provider";
m_descriptor.LanguageId = "example";
m_descriptor.FileExtensions = ".example";
m_descriptor.RuntimeVersion = 0x00010000;
m_descriptor.Flags =
    Salamatrix::Runtime::RuntimeAdapterFlagOutOfProcess |
    Salamatrix::Runtime::RuntimeAdapterFlagPersistentExtensions;

RuntimeId is part of extension manifests and AI output, so changing it breaks existing packages. Use a language-neutral dotted identifier when several engines could implement the same language. FileExtensions is a semicolon-separated list if the adapter accepts more than one suffix.

IsAvailable() must be cheap after its first call. The existing providers resolve an explicit SALAMATRIX_<LANGUAGE> environment variable first and then search a short list of executable names with SearchPathW. An environment variable may contain either a full path or a command name.

SupportsEntryPoint() should reject unavailable runtimes and compare the entry-point suffix case-insensitively.

2. Register without taking ownership of the framework

The provider and SALAMATRIX.SPL have independent plugin lifetimes. Query SALAMATRIX_SERVICE_RUNTIME with minimum version SALAMATRIX_RUNTIME_VERSION_1_0, then register the adapter through RuntimeProviderRegistration.

Registration may not be possible during SalamanderPluginEntry when the framework has not loaded yet. Retry from Connect and Event. Before registering, use FindAdapter(runtimeId, 0) to avoid a duplicate.

On Release, unregister only when the currently published runtime service is the exact service stored by the registration helper. If the framework has already unloaded, discard the borrowed registration state without calling through the stale interface. The standalone providers demonstrate this unload guard in UnregisterRuntimeProvider.

The provider must set SetFlagLoadOnSalamanderStart(TRUE). Its adapter object and descriptor strings must remain alive for the whole registration.

3. Implement one-shot execution

Execute() is the direct CLI path:

1. Validate StructSize, pointers, availability, and entry-point suffix. 2. Build a correctly quoted Unicode command line. 3. Set the working directory to WorkingDirectory, or to the entry-point directory when it is absent. 4. redirect stdin to NUL and capture stdout/stderr through a pipe; 5. start the interpreter with CREATE_NO_WINDOW; 6. enforce the request timeout with an absolute upper bound; 7. drain bounded output, preserve the process ID and exit code, and fill every field in RuntimeExecutionResult.

Never pass an untrusted command line through cmd.exe. Reject embedded quotes unless the provider has a complete Windows argument quoting implementation. Keep output bounded; the reference providers capture at most 1 MiB and copy a bounded summary into the ABI result.

4. Implement persistent sessions

Manifest extensions normally use StartPersistent(). Create two anonymous pipes:

When RuntimeExecutionFlagUseWorkerBootstrap is set, start the provider's worker file and pass the entry point plus optional command ID and handler. When RuntimeExecutionFlagOneShotWorker is also set, ask the worker to exit after the entry point returns.

The returned IRuntimeSession owns the process, thread, and pipe handles. It must:

Release() deletes the session. Stop() must be idempotent. A provider may terminate an unresponsive worker, but it must first close the input channel and use bounded waits.

5. Write the worker bootstrap

SMX1 is a line protocol:

SMX1<TAB>kind<TAB>decimal-id<TAB>compact-json<LF>

Each encoded frame, including the newline, is limited to 1 MiB. Worker stdout is reserved exclusively for these frames. Redirect or replace the language's normal print facility so extension output cannot corrupt the transport.

The worker starts with:

SMX1	hello	0	{"protocol":1,"runtime":"example"}

It waits for result ID 0, builds the language-native Salamander object, and executes the entry point. Host calls use monotonically increasing IDs and a payload containing method plus its arguments. While waiting for a result, the worker must dispatch interleaved event frames. A shutdown frame ends the persistent loop.

Implement JSON inside the worker without requiring an optional language package, or ship and license a dependency beside the provider. Preserve JSON strings, booleans, signed integers, arrays, objects, and null. Reject malformed, oversized, or non-SMX1 input instead of attempting to recover silently.

The facade should cover the same method vocabulary as the other modern workers: commands, storage, file operations, sides/tabs, UI and dialogs, clipboard, AI, events, runtime enumeration, and host appearance/language. Keep language naming idiomatic, but do not change host method names or payload fields.

For dialogs, implement the full generic addControl equivalent rather than only convenience helpers. It must forward explicit x/y/width/height and the common extended properties: style flags, path separator, tooltip, hyperlink open/command/hint actions, known and 64-bit progress values, indeterminate timing, text/background colors, and toolbar-header alignment and button mask. Support the complete control-kind set documented in doc/salamatrix-ui.md. Dialog creation, ownership, native rendering, event delivery, dark mode, and DPI belong to Salamatrix Framework; a runtime provider must not implement a parallel UI toolkit or depend on Automation.

6. Add the project and package

Follow the standalone provider layout:

src/plugins/<language>runtime/
  <language>runtime.cpp
  <language>runtime.h
  <language>runtime.def
  <language>runtime.rc
  runtime/salamatrix_worker.<suffix>
  vcxproj/<language>runtime.props
  vcxproj/<language>runtime.vcxproj

The project output belongs under:

plugins/extension-runtimes/<project-name>/

Copy the worker to the provider's runtime subdirectory after the build. Add the project to src/vcxproj/salamand.sln, include both the .spl and worker in the installer, declare a dependency on salamatrix, and add a catalog entry. Do not make the runtime an Automation dependency and do not bundle an interpreter unless its redistribution, updates, architecture, and licenses are an explicit product decision.

An extension selects the adapter by its stable runtime ID:

{
  "id": "example.demo",
  "version": "1.0.0",
  "runtime": "Example.CLI",
  "entryPoint": "main.example",
  "capabilities": ["panels.read", "ui.dialogs"]
}

7. Verify the provider

Verification should include:

Use a disposable worker root containing only the bootstrap files under test. The process-runtime tests do not require starting Salamander. Use the same 463 x 236 gallery geometry and options as the five bundled demos when adding a provider so parity can be reviewed source-to-source. The current layout and examples are described in Salamatrix.UI framework and custom dialog guide. After building or staging the package, verify its PE exports and colocated worker without loading the plugin:

.\tools\verify_runtime_packages.ps1 `
  -SalamanderPath .\build\salamander\Release_x64 `
  -Architecture x64 -Plugin <language>runtime

Lua reference provider

LuaRuntime.SPL follows this model with runtime ID Lua, language ID lua, and .lua entry points. It resolves SALAMATRIX_LUA, then its bundled runtime\lua.exe, then lua.exe, lua55.exe, or lua54.exe on PATH. The bundled Lua 5.5.0 interpreter and DLL come from the pinned vcpkg lua[tools] port. Its complete MIT notice is copied into the package as runtime\LICENSE-LUA.txt and Lua is listed in every localized third_party.md variant.

The worker supports stock Lua without external modules. It supplies its own bounded JSON codec, reserves stdout for SMX1, exposes the global Salamander table, and executes the entry point with dofile. A package can therefore use:

local path = Salamander.sides.context("source").path
Salamander.ui.notify("Current path: " .. path, "Lua extension")

Users who need another Lua build can set SALAMATRIX_LUA to its executable path before starting Salamander; this explicit override takes precedence over the bundled interpreter.