This guide is for application authors who want to add a scripting layer to their software. It walks from the decision to embed, through a working implementation, in roughly the order you will encounter each question.
libkonsolscript ships two API surfaces:
kse.hpp) - full access: Engine, PluginClass, FileWatcher, lambdaskonsolscript.h) - plain C functions, callable from any language with a C FFI: Python, C#, Go, Rust, Node.js, Swift, Zig, Ruby, and moreThe steps in this guide use the C++ API. For other languages, see Embedding from Python and other languages at the bottom.
If you are a script author looking for the language reference, see Scripting Guide instead.
The honest answer depends on what you need from a scripting layer.
You are building an application - a game engine, an automation platform, a desktop tool, an AI-driven service - and you want users, plugin authors, or AI-generated code to extend it at runtime. The key word is extend: they write scripts that call the capabilities you expose, not arbitrary system calls.
KonsolScript fits well when:
File:, OS:, Curl: - if you did not register them, they do not exist inside that engine instance. setEvalGuard lets you intercept execution before tokenization even begins.Lua is the dominant embeddable scripting language and the most direct comparison. The tradeoff is real:
| Lua | KonsolScript | |
|---|---|---|
| Ecosystem | Large (LuaRocks, decades of libraries) | Small (built-in modules + plugins) |
| Embedding API | lua_State* C API, verbose binding code |
C++ (PluginClass builder) or plain C API (konsolscript.h) for Python, Go, Rust, C# |
| Surface-area control | Possible, requires careful sandboxing | Built-in: register only what you allow |
| Eval guard | Not built-in | setEvalGuard - runs before tokenization |
| Type system | Dynamic, flexible | Zero-null, explicit types - predictable for AI-generated code |
The ecosystem gap is real - though a little funding goes a long way. Sponsoring the project helps bring the specific plugins your project needs while growing the ecosystem for everyone. If you want minimal embedding friction and hard surface-area control, KonsolScript has a real advantage.
Before any code: the mental model that makes the rest of this guide make sense.
A libkonsolscript engine is an isolated interpreter. It knows nothing about your application until you tell it. You tell it by registering modules - named collections of methods backed by C++ lambdas. A script running inside that engine can call exactly those methods and nothing else.
Your app (C++, Python, Go, C#, ...)
│
├─ Engine engine; (C++) / ks_create() (C API)
│
├─ engine.registerClass("Game", ...) ← you define this
├─ engine.registerClass("Player", ...) ← you define this
│ (built-in modules are registered by default)
│
└─ engine.eval(script_from_user_or_ai)
│
▼
Script can call:
Game:GetScore(out) ✓ you registered it
Player:SetName("Alice") ✓ you registered it
File:Read("x.txt", out) ✓ built-in, registered by default
OS:Exec("rm -rf /") ✓ built-in, registered by default
The eval guard (covered in Step 4) lets you block built-in modules you did not explicitly want to expose. The two layers together - registration and guard - give you defense in depth.
Build libkonsolscript from the minks/ directory:
cd minks
make # builds libkonsolscript.dll / .so / .dylib + the minks CLI
make static # builds libkonsolscript.a if you prefer static linking
Three headers are the entire public API:
| Header | What it gives you |
|---|---|
kse.hpp |
Engine class |
kse_types.hpp |
Value, Token, collection types, toString(), toDouble(), toBool() |
kse_watch.hpp |
FileWatcher for hot-reload (header-only, no extra linking) |
Makefile - Windows (MinGW):
CXX = g++
CXXFLAGS = -std=c++17 -I/path/to/minks -I/path/to/minks/core
myapp.exe: main.cpp
$(CXX) $(CXXFLAGS) -o $@ main.cpp /path/to/minks/libkonsolscript.dll
cp /path/to/minks/libkonsolscript.dll .
Makefile - Linux:
CXX = g++
CXXFLAGS = -std=c++17 -I/path/to/minks -I/path/to/minks/core
myapp: main.cpp
$(CXX) $(CXXFLAGS) -o $@ main.cpp \
-L/path/to/minks -lkonsolscript -Wl,-rpath,'$$ORIGIN'
macOS: replace -Wl,-rpath,'$$ORIGIN' with -Wl,-rpath,@loader_path.
Static linking (all platforms): link against libkonsolscript.a directly. No rpath needed.
The Engine constructor registers all built-in modules. No extra initialization is needed.
#include "kse.hpp"
int main() {
Engine engine;
engine.eval(R"(
Var:Number x = 6 * 7;
Konsol:Print(x);
)");
return 0;
}
eval() preserves state between calls. Variables declared in one call are visible in the next. This is how you build an interactive or event-driven loop.
To load and run a file instead:
#include "kse.hpp"
#include <fstream>
#include <sstream>
int main(int argc, char* argv[]) {
Engine engine;
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;
}
The dir argument tells the engine where to look when a script uses #include.
Send data into a script by setting globals before eval() or run():
engine.setVar("player_name", Value(std::string("Alice")));
engine.setVar("player_score", Value(0.0));
engine.setVar("debug_mode", Value(true));
The script reads them as ordinary variables:
Konsol:Print("Hello, " + player_name);
Read data back from the engine after eval() or run() 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 - no exception, no crash.
Call a function defined in a script from the host:
Value result = engine.callFunction("add", { Value(3.0), Value(4.0) });
double n = toDouble(result); // 7
This is the point where KonsolScript becomes genuinely useful to your application. You define a named module, and every method in it is a C++ lambda with direct access to your application's state.
Use the PluginClass builder from minks_plugin.h - it handles all token parsing for you:
#include "kse.hpp"
#include "minks_plugin.h"
struct GameState {
int score = 0;
bool paused = false;
};
int main() {
GameState gs;
Engine engine;
PluginClass(engine, "Game")
// byRefMethod: script calls Game:GetScore(outVar) - result written to outVar
.byRefMethod("GetScore", [&gs](auto) { return Value((double)gs.score); })
.byRefMethod("IsPaused", [&gs](auto) { return Value(gs.paused); })
// voidMethod: script calls Game:AddPoints(10) - no return value
.voidMethod ("AddPoints", [&gs](auto a) { gs.score += (int)toDouble(a[0]); })
.voidMethod ("Pause", [&gs](auto) { gs.paused = true; })
.voidMethod ("Resume", [&gs](auto) { gs.paused = false; });
engine.eval(R"(
Var:Number score;
Game:AddPoints(50);
Game:GetScore(score);
Konsol:Print("Score is now: " + score);
)");
return 0;
}
The byRefMethod / voidMethod naming follows KonsolScript's ByRef convention: every method that returns a value writes it into the last argument, which must be a pre-declared variable in the script. Methods that only produce side effects use voidMethod.
For lower-level control (custom argument counts, complex parsing), see Embedding API - Registering a host class.
By default, the engine registers all built-in modules. A script can call File:, OS:, Curl:, and everything else. For developer tools and internal scripts this is the right default.
For scripts that come from users, AI APIs, or the network, apply the two control layers.
The eval guard runs before every eval() and reloadFile(), before tokenization. Return false to reject the script entirely - engine state is left completely unchanged.
engine.setEvalGuard([](const std::string& src) -> bool {
// Allow only Game: calls. Reject anything that touches the filesystem or network.
static const std::vector<std::string> banned = {
"File:", "OS:", "Curl:", "Net:", "SQLite:", "MySQL:", "PG:", "Redis:"
};
for (const auto& b : banned) {
if (src.find(b) != std::string::npos) return false;
}
return true;
});
For AI-generated scripts delivered over a network, add cryptographic verification so you know the script came from your system and was not tampered with in transit. See Embedding API - Security and trust model for a complete HMAC-SHA256 guard example.
The eval guard operates on raw text - a clever script could in principle construct a forbidden method name from string operations to evade a keyword scan. The registration layer closes that gap: a module that was never registered does not exist in the engine, regardless of what the script text says.
Use both layers together: the guard as the primary policy filter, and your intentional set of registerClass calls as the hard boundary. The two complement each other - the guard catches source-text violations early, and registration ensures no unregistered module can ever be reached at runtime.
Engine engine;
// Eval guard: cheap source-text filter, runs before tokenization
engine.setEvalGuard([](const std::string& src) -> bool {
static const std::vector<std::string> banned = {
"File:", "OS:", "Curl:", "Net:", "SQLite:", "MySQL:", "PG:", "Redis:"
};
for (const auto& b : banned) {
if (src.find(b) != std::string::npos) return false;
}
return true;
});
// Your module: the only domain-specific capability the script can use
PluginClass(engine, "Game")
.byRefMethod("GetScore", [&gs](auto) { return Value((double)gs.score); })
.voidMethod ("AddPoints",[&gs](auto a){ gs.score += (int)toDouble(a[0]); });
// Now run a script from an external source with confidence
engine.eval(script_from_ai_or_network);
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.
// Reload a changed file on demand
engine.reloadFile("scripts/behavior.ks");
To reload automatically when the file changes on disk, add a FileWatcher:
#include "kse_watch.hpp"
FileWatcher watcher;
watcher.watch("scripts/behavior.ks", [&](const std::string& path) {
engine.reloadFile(path);
});
// In your update loop - callbacks fire on the same thread, safe to call engine directly
watcher.poll();
For multi-threaded hosts (callbacks on the watcher thread, engine on the main thread):
std::atomic<bool> dirty{ false };
std::string dirtyPath;
watcher.watch("scripts/behavior.ks", [&](const std::string& path) {
dirtyPath = path;
dirty = true; // signal only - do not call engine here
});
watcher.start(); // spawns background thread, polls every 500 ms
// In your main-thread frame update:
if (dirty.exchange(false))
engine.reloadFile(dirtyPath);
libkonsolscript exports a plain C API in konsolscript.h. Any language with a C FFI can load the shared library and call it directly - no C++ compiler needed.
The bindings/python/konsolscript.py module wraps the C API using ctypes. No build step, no third-party packages.
from konsolscript import Engine
engine = Engine()
# Pass data in
engine.set("limit", 100.0)
# Run a script
engine.eval("""
Var:Number result = limit / 4;
Konsol:Print(result);
""")
# Read data out
print(engine.get_number("result")) # 25.0
Install the eval guard for scripts from untrusted sources:
def my_guard(source: str) -> bool:
blocked = ["File:", "OS:", "Net:"]
return not any(b in source for b in blocked)
engine.set_guard(my_guard)
See bindings/python/README.md for the full API reference.
The same konsolscript.h C API is the entry point for any language:
| Language | Mechanism |
|---|---|
| C# / Unity | P/Invoke |
| Go | cgo |
| Rust | bindgen against konsolscript.h |
| Node.js | ffi-napi or N-API native addon |
| Swift | Bridging header |
| Ruby | ffi gem |
| Zig | Built-in C interop |
The C API functions follow the pattern ks_<action>:
#include "konsolscript.h"
KSEngine e = ks_create();
ks_set_number(e, "x", 42.0);
ks_eval(e, "Konsol:Print(x);");
ks_destroy(e);
You have a working embedded engine. The resources below cover the parts of the API not detailed here:
.dll/.so plugins your scripts can load with #include