libkonsolscript is the interpreter as a shared library. Any C++17 application can link against it to make itself scriptable - no dependency on the minks CLI.
For embedding from Python, Go, Rust, C#, or any other language with a C FFI, use konsolscript.h instead of the C++ headers. See Embedding Guide - other languages.
libkonsolscript.dll (Windows)
libkonsolscript.so (Linux)
libkonsolscript.dylib (macOS)
libkonsolscript.a (all platforms - static, built with `make static`)
The public API surface:
| Header | Language | Contents |
|---|---|---|
konsolscript.h |
C (any FFI) | Plain C functions - ks_create, ks_eval, ks_set_, ks_get_, ks_set_guard |
kse.hpp |
C++17 | Engine class - create, configure, load, run, query, hot-reload |
kse_types.hpp |
C++17 | Value variant, Token, ClassDef, collection types, toString(), toDouble() |
kse_watch.hpp |
C++17 | FileWatcher - header-only file watcher for hot-reload |
Makefile (Windows/MinGW):
CXX = g++
CXXFLAGS = -std=c++17 -I/path/to/minks -I/path/to/minks/core
sample_app.exe: main.cpp
$(CXX) $(CXXFLAGS) -o $@ main.cpp /path/to/minks/libkonsolscript.dll
# Copy libkonsolscript.dll next to the exe so Windows can find it at runtime
cp /path/to/minks/libkonsolscript.dll .
MinGW note: Use the DLL path directly rather than -lkonsolscript. MinGW's
linker searcheslibkonsolscript.a(static archive) beforelibkonsolscript.dll,
and if a stale static build is present it will silently link the wrong copy.
Makefile (Linux):
CXX = g++
CXXFLAGS = -std=c++17 -I/path/to/minks -I/path/to/minks/core
sample_app: main.cpp
$(CXX) $(CXXFLAGS) -o $@ main.cpp \
-L/path/to/minks -lkonsolscript -Wl,-rpath,'$$ORIGIN'
# rpath=$ORIGIN means libkonsolscript.so is found next to the binary
macOS: replace -Wl,-rpath,'$$ORIGIN' with -Wl,-rpath,@loader_path.
Static linking (all platforms):
Build the archive first (make static in the minks directory), then link directly against it. No rpath needed; the library is compiled into the executable:
CXX = g++
CXXFLAGS = -std=c++17 -I/path/to/minks -I/path/to/minks/core
sample_app: main.cpp
$(CXX) $(CXXFLAGS) -o $@ main.cpp /path/to/minks/libkonsolscript.a
#include "kse.hpp"
Engine engine; // ctor registers all stdlib modules
engine.setDebug(true); // optional: trace each statement to stderr
The Engine constructor registers all built-in modules (Konsol, Math, String, File, Time, List, Dictionary, JSON, OS, Path, Hash, Date, CSV, Regex). No extra initialization is needed.
Inline / REPL-style - append a source fragment and execute immediately:
engine.eval(R"(
Var:Number x = 6 * 7;
Konsol:Print(x);
)");
eval() preserves engine state between calls, so variables declared in one call are visible in the next.
File-style - parse a complete script, then run it:
std::ifstream f("game.ks");
std::ostringstream ss; ss << f.rdbuf();
engine.loadScript(ss.str(),
"/path/to/scripts", // base dir for #include resolution
"game.ks"); // filename shown in error messages
engine.run();
Scripts can be reloaded into a live engine without restarting the host process. Function definitions in the new source overwrite the old ones; variable state is preserved. This is the basis for patching game logic, tool scripts, or AI workflows while the application is running.
reloadFile(path)engine.reloadFile("scripts/ai.ks");
Reads the file from disk and passes its source to eval(). Any functions defined in the file are redefined in the running engine. Returns false (and prints to stderr) if the file cannot be opened or the source contains a runtime error.
FileWatcher - detect on-disk changeskse_watch.hpp is a header-only utility that watches files for modification-time changes and fires a callback when one is detected. No extra linking required.
#include "kse_watch.hpp"
Poll mode - call poll() from your existing update loop. Callbacks fire on the same thread, so calling engine.reloadFile() directly is safe:
FileWatcher watcher;
watcher.watch("scripts/ai.ks", [&](const std::string& path) {
engine.reloadFile(path);
});
// in your game/tool update loop:
watcher.poll();
Background thread mode - start() spawns a thread that polls every 500 ms (configurable). Callbacks fire on the watcher thread, so synchronize Engine access at your frame boundary:
FileWatcher watcher;
std::atomic<bool> dirty{ false };
std::string dirtyPath;
watcher.watch("scripts/ai.ks", [&](const std::string& path) {
dirtyPath = path;
dirty = true; // signal main thread; do not call engine here
});
watcher.start(); // default: 500 ms interval
// watcher.start(std::chrono::milliseconds(200)); // or a custom interval
// in your frame update (main thread):
if (dirty.exchange(false))
engine.reloadFile(dirtyPath);
// on shutdown:
watcher.stop();
Watching multiple files works the same way - one watch() call per path, with independent callbacks:
watcher.watch("scripts/ai.ks", [&](auto& p){ engine.reloadFile(p); });
watcher.watch("scripts/combat.ks", [&](auto& p){ engine.reloadFile(p); });
watcher.watch("scripts/ui.ks", [&](auto& p){ engine.reloadFile(p); });
When reloadFile() (or eval()) is called on source that contains a function already known to the engine:
buildSymbols() rescans all tokens - the new definition appears last andoverwrites the old entry in the function table.
execute() skips function bodies at the top level, so the new body isregistered but not double-executed.
callFunction("onUpdate", ...) uses the new definition.Variable state (setVar values, globals declared earlier) is untouched. The old function tokens remain in the stream as unreachable dead code - negligible for normal hot-patch rates, but worth noting for very long-running processes that patch thousands of times.
KonsolScript gives the host application full control over what scripts can do and where they come from. This matters most when scripts arrive from external sources - AI APIs, network peers, or user-supplied files.
minks registers all modules and installs no eval guard. It is a developer tool; scripts are written by the developer running it, so full access is the right default.
Production host applications are different. A game engine, an automation platform, or a moddable app typically wants to limit scripts to only the modules that make sense in that context, and to validate scripts from external sources before running them.
The two control layers are:
| Layer | What it controls | When it applies |
|---|---|---|
| Module registration | Which modules exist in the engine | At construction - a module never registered cannot be called |
| Eval guard | Whether a specific script source is allowed to run | Before every eval() and reloadFile() |
These are independent and complementary. A script that passes the guard can only call modules that were registered. A module that was never registered does not exist in the engine regardless of what the guard allows.
Install a guard that is called before every eval() and reloadFile(). If the guard returns false, the script is rejected and engine state is left completely unchanged.
engine.setEvalGuard([](const std::string& src) -> bool {
// Reject any script that references File: or OS: methods.
if (src.find("File:") != std::string::npos) return false;
if (src.find("OS:") != std::string::npos) return false;
return true;
});
The guard receives the raw source string before tokenization - no parse tree is built on rejection. Use it to enforce keyword bans, length limits, allowlists, or signature verification:
// HMAC-SHA256 signature check using OpenSSL (link with -lssl -lcrypto).
// Convention: the AI service prepends "# sig:<hex>\n" as the first line of
// every generated script and signs the body with a shared secret.
// The host verifies the signature before running the script.
#include <openssl/hmac.h>
#include <sstream>
#include <iomanip>
engine.setEvalGuard([&sharedSecret](const std::string& src) -> bool {
auto nl = src.find('\n');
if (nl == std::string::npos) return false;
std::string firstLine = src.substr(0, nl);
if (firstLine.rfind("# sig:", 0) != 0) return false; // no signature header
std::string sig = firstLine.substr(6); // hex after "# sig:"
std::string body = src.substr(nl + 1); // script body without first line
unsigned char digest[32]; unsigned int dlen = 32;
HMAC(EVP_sha256(),
sharedSecret.data(), (int)sharedSecret.size(),
(const unsigned char*)body.data(), (int)body.size(),
digest, &dlen);
std::ostringstream hex;
for (unsigned int i = 0; i < dlen; ++i)
hex << std::hex << std::setw(2) << std::setfill('0') << (int)digest[i];
return hex.str() == sig;
});
Remove the guard at any time:
engine.setEvalGuard({}); // no guard - all eval() calls pass through
The guard inspects raw source text before parsing. A script could in principle construct a forbidden method name at runtime using string operations and avoid the keyword scan. For stronger isolation, pair the guard with module registration: if File: was never registered, the module does not exist in the engine and cannot be called regardless of what text the script contains.
Explicit module selection at construction time (engine.addModule("Math")) is planned. Until then, use the eval guard as the primary policy layer for scripts from external sources.
For AI-generated or network-delivered scripts, apply both layers together:
Engine engine;
// Engine currently registers all built-in modules by default.
// Eval guard: reject at source if the script references anything outside
// the intended scope. Runs before tokenization - cheap.
engine.setEvalGuard([](const std::string& src) -> bool {
static const std::vector<std::string> banned = {
"File:", "OS:", "Curl:", "SQLite:", "MySQL:", "PG:", "Redis:"
};
for (const auto& b : banned) {
if (src.find(b) != std::string::npos) return false;
}
return true;
});
// Scripts that pass the guard can still only reach modules that were
// registered. Everything else is structurally absent.
The demo_ai_bridge example passes AI-generated scripts directly to engine.eval(). In production, always install a guard before the event loop - see Worked example: demo_ai_bridge.
Host → script (set a global before running the script):
engine.setVar("player_name", Value(std::string("Alice")));
engine.setVar("player_score", Value(0.0));
engine.setVar("debug_mode", Value(true));
Script → host (read a global after run() or eval() returns):
Value score = engine.getVar("player_score");
double s = toDouble(score);
Value status = engine.getVar("status");
std::string st = toString(status);
getVar returns a zero-value (0, "", false) for names that do not exist.
ValueValue(42.0) // Number
Value(std::string("hello")) // String - always use std::string, not const char*
Value(true) // Boolean
Value result = engine.callFunction("add", { Value(3.0), Value(4.0) });
double n = toDouble(result); // 7
The function must be defined in a script that was already loaded/eval'd. If the function is not found, callFunction returns Value(0.0) - no exception is thrown.
Host-side classes use the same handler mechanism as built-in modules. The handler receives the raw token stream and must advance past the full statement.
engine.registerClass("Game", [](Engine& e, size_t i) -> size_t {
++i; // skip "Game"
if (e.tokenText(i) != ":") return e.skipOptSemi(i);
++i; // skip ":"
std::string method = e.tokenText(i++); // read method name
if (e.tokenText(i) == "(") ++i; // skip (
if (method == "GetScore") {
std::string outVar = e.tokenText(i++);
if (e.tokenText(i) == ")") ++i;
e.setVar(outVar, Value(current_score)); // ByRef pattern
}
return e.skipOptSemi(i);
});
For simpler class registration without token parsing, use the PluginClass builder from minks_plugin.h (see Plugin System).
When a script uses #include "foobar", the engine searches its plugin path list. The host can prepend directories to this list before running any script:
engine.addPluginPath("/opt/mygame/plugins");
For iOS / embedded targets where dlopen is not available, register plugins statically before running the script:
// foobar_plugin.cpp exports: extern "C" void minks_register(Engine&);
engine.addPluginPreload("foobar", foobar_plugin_init);
// Now #include "foobar" in a script calls foobar_plugin_init instead of dlopen
Compile with -DMINKS_NO_DYNLOAD to strip all dynamic-loading code from libkonsolscript entirely.
sample_appsample_app/ in the minks repository is a minimal host application that demonstrates the complete embedding workflow. It has no dependency on the minks CLI source.
sample_app/main.cpp (abridged):
#include "kse.hpp"
#include <fstream>
#include <sstream>
int main(int argc, char* argv[]) {
Engine engine;
// Host → script: expose app metadata as globals
engine.setVar("app_name", Value(std::string("SampleApp")));
engine.setVar("app_version", Value(1.0));
if (argc >= 2) {
// File mode: load and run a .ks script
std::ifstream f(argv[1]);
std::ostringstream ss; ss << f.rdbuf();
std::string path = argv[1];
size_t sep = path.find_last_of("/\\");
std::string dir = (sep != std::string::npos) ? path.substr(0, sep) : ".";
engine.loadScript(ss.str(), dir, path);
return engine.run() ? 0 : 1;
}
// Inline mode: eval fragments, read results back
engine.eval("Var:Number result = 0; for (Number i=1;i<=10;i++){result=result+i;}");
double sum = toDouble(engine.getVar("result")); // 55
return 0;
}
sample_app/demo.ks - the matching script:
Var:String banner = "Running inside: " + app_name + " v" + app_version;
Konsol:Print(banner);
Var:Number pw = 0;
Math:Power(2, 10, pw);
Konsol:Print(pw); // 1024
Build and run:
cd sample_app
make
./sample_app # inline demo
./sample_app demo.ks # file mode
demo_hotreloaddemo_hotreload/ is the minimal proof-of-concept for live patching. It is the fastest way to see hot-reload in action: edit a .ks file, save it, and the running host picks up the change within one second - no restart, no rebuild.
How it works:
loadScript + run() load and execute behavior.ks once (runs main()).FileWatcher in poll mode watches behavior.ks for changes.callFunction("tick", {}) every second.watcher.poll() fires reloadFile() on the samethread - safe, no locking needed.
tick() call uses the newly defined function.behavior.ks - the file you edit live:
function tick() {
Konsol:Print("Weather: sunny");
}
function main() {
Konsol:Print("Hot-reload demo started.");
Konsol:Print("Edit tick() in behavior.ks and save to see live changes.");
}
Build and run:
cd minks
make
cd demo_hotreload
make run
While it is running, open behavior.ks in any editor, change the string inside tick(), and save. The output changes within one second.
demo_ai_bridgedemo_ai_bridge/ is a more advanced demo that shows three embedding techniques working together: a custom host module, a live eval() event loop, and getVar() polling - all driven by AI-generated KonsolScript arriving over the network.
Two programs. Zero AI code in the game.
You (prompt)
│
▼
game_master.ks ──Curl:Post──► Claude API
◄────────────── KonsolScript event script
game_master.ks ──Net:Send──► demogame_host (C++ host)
└─ engine.eval(script)
└─ DemoGame:SpawnWave(...)
DemoGame:SetWeather(...)
...
demogame_host is a C++ stub that embeds libkonsolscript. It knows nothing about AI. game_master.ks calls the Claude API, gets back a KonsolScript event script, and ships it over the LAN. The host runs it via engine.eval().
PluginClassdemogame_host.cpp exposes a DemoGame: module backed by a C++ GameState struct. Each method is a lambda that reads or mutates live host state:
struct GameState { int wave = 7; int health = 68; bool is_night = true; int spawned = 0; };
GameState gs;
Engine engine;
// Only allow scripts that call DemoGame: methods - nothing else.
engine.setEvalGuard([](const std::string& src) -> bool {
static const std::vector<std::string> banned = {
"File:", "OS:", "Curl:", "SQLite:", "MySQL:", "PG:", "Redis:", "Net:"
};
for (const auto& b : banned) {
if (src.find(b) != std::string::npos) return false;
}
return true;
});
PluginClass(engine, "DemoGame")
.byRefMethod("GetWave", [&gs](auto) { return Value((double)gs.wave); })
.byRefMethod("GetHealth", [&gs](auto) { return Value((double)gs.health); })
.byRefMethod("IsNight", [&gs](auto) { return Value(gs.is_night); })
.voidMethod ("Announce", [](auto a) { std::cout << "[ANNOUNCE] " << toString(a[0]) << "\n"; })
.voidMethod ("SpawnWave", [&gs](auto a) {
std::cout << "[SPAWN] " << toString(a[0]) << " x " << (int)toDouble(a[1]) << "\n";
gs.spawned += (int)toDouble(a[1]);
})
.voidMethod ("SetWeather", [](auto a) { std::cout << "[WEATHER] " << toString(a[0]) << "\n"; })
.voidMethod ("SetModifier",[](auto a) {
std::cout << "[MODIFIER] " << toString(a[0]) << " = " << toString(a[1]) << "\n";
});
KonsolScript code running inside this engine can now call DemoGame:GetWave(out), DemoGame:SpawnWave("goblin", 3), etc. - and those calls mutate gs directly.
eval() event loop with getVar() pollingRather than running a single script and exiting, the host drives a game loop. Network state is declared once via eval(), then polled each tick. When a message arrives its contents are passed straight back to engine.eval():
// Declare persistent network vars once
engine.eval(R"(
Var:Number _h = 0;
Var:String _sender = "";
Var:String _msg = "";
Var:Boolean _got = false;
Net:Host("demogame", 1, "EventBridge", _h);
)");
while (true) {
engine.eval(R"(
_sender = ""; _msg = ""; _got = false;
Net:Check(_h);
Net:GetMessage(_h, _sender, _msg, _got);
)");
if (toBool(engine.getVar("_got"))) {
std::string script = decode_nl(toString(engine.getVar("_msg")));
engine.eval(script); // run AI-authored KonsolScript live
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
The getVar() calls read _got and _msg back from the engine after each eval(). The AI-generated script then runs inside the same engine, so it has full access to the DemoGame: module and to gs via the registered lambdas.
Prerequisites: minks built, kse_curl and kse_net plugins installed.
cd minks/demo_ai_bridge
make
Terminal A - start the host:
./demogame_host
# Game state: wave=7 health=68% night=yes
# Listening on port 2310...
Terminal B - send an AI event:
minks --plugin-path . game_master.ks "blizzard event for wave 10"
# Event deployed: ok
Terminal A reacts immediately:
--- event from gamemaster ---
[ANNOUNCE] Blizzard incoming! Prepare your defenses.
[WEATHER] blizzard
[SPAWN] ice_golem x 4
[MODIFIER] speed = 0.6
--- done (total spawned: 4) ---
Replace DemoGame: with any module your app exposes, replace demogame_host.cpp with your own host, replace game_master.ks with any orchestrator - a cron job, a webhook receiver, another AI. The host never needs to know what the script will do ahead of time; it only defines what the script is allowed to do via the registered module.
| Platform | Library | Notes |
|---|---|---|
| Windows | libkonsolscript.dll |
make install places it in BINDIR alongside minks.exe; for embedding, copy it next to your host exe (no rpath on Windows) |
| Linux | libkonsolscript.so |
Link with -Wl,-rpath,'$$ORIGIN' so the .so is found next to the exe |
| macOS | libkonsolscript.dylib |
Link with -Wl,-rpath,@loader_path |
| iOS / embedded | static libkonsolscript.a |
Build with -DMINKS_NO_DYNLOAD; use addPluginPreload for plugins |