Jump to content

Runtime Script Compilation

From OpenBOR

OpenBOR can construct and manage complete script programs while a module is running. Source may be loaded from one or more files into an independently owned script object, compiled once, and executed repeatedly on demand. Because the calling script controls its lifecycle and execution, a runtime script can operate as a persistent subsystem without being tied to one of OpenBOR’s predefined events. OpenBOR’s file operations can even generate new source code for compilation, enabling runtime metaprogramming.

Potential uses include:

  • Combining movement, combat, and personality packages into specialized AI behavior.
  • Loading optional systems only when required by a game mode or campaign.
  • Providing development and diagnostic tools that remain unloaded during ordinary play.
  • Creating multiple instances of the same behavior with independent persistent state.
  • Replacing a subsystem by releasing its handle and compiling a new implementation from different source files.

Runtime compilation is not a single evaluation call. The feature uses an owned script object with a four-stage lifecycle:

  1. Create the object with allocscript().
  2. Load and parse source with loadscript().
  3. Compile the accumulated source with compilescript().
  4. Run it with executescript(), then release it with free() when finished.

The resulting handle owns a complete OpenBOR script interpreter and a private local-variable list. Compiled instructions remain available until the handle is freed, so one script can be compiled once and executed many times.

Important: Runtime scripts execute with the same native script API and engine access as ordinary module scripts. They are not sandboxed. Only compile trusted source.

Lifecycle

Stage Function Result
Allocate allocscript() Creates an owned script object and returns its pointer handle.
Load loadscript() Reads a loose or packed source file into memory and parses it into the handle.
Compile compilescript() Resolves functions, imports, constants, variables, and jump targets, then runs oncreate() when present.
Execute executescript() Runs top-level immediate code and calls main().
Release free() Calls ondestroy() when present, then releases the interpreter, variables, and handle.

Loading and compilation are separate by design. loadscript() parses source but does not make it executable. compilescript() finalizes the collected instructions into the interpreter's runtime representation. Calling executescript() before successful compilation is invalid.

Once compiled, the same handle may be passed to executescript() repeatedly. Compilation should normally occur only once. Reloading or recompiling a finalized handle is not a supported replacement mechanism - free the old handle and build a new one instead.

Walk-through

Place the following source in data/scripts/runtime_square.c:

void oncreate()
{
    setlocalvar("execution_count", 0);
}

void main()
{
    int execution_count = getlocalvar("execution_count") + 1;
    int input_value = getglobalvar("runtime.input");

    setlocalvar("execution_count", execution_count);
    setglobalvar("runtime.result", input_value * input_value);
    setglobalvar("runtime.executions", execution_count);
}

void ondestroy()
{
    setglobalvar("runtime.released", 1);
}

Another script can load, compile, execute, and release it:

void run_runtime_example()
{
    void script_handle = allocscript(
        "runtime_square",
        "data/scripts/runtime_square.c"
    );

    loadscript(script_handle, "data/scripts/runtime_square.c");
    compilescript(script_handle);

    setglobalvar("runtime.input", 12);
    executescript(script_handle);

    log("Result: " + getglobalvar("runtime.result") + "\n");
    log("Executions: " + getglobalvar("runtime.executions") + "\n");

    setglobalvar("runtime.input", 25);
    executescript(script_handle);

    log("Result: " + getglobalvar("runtime.result") + "\n");
    log("Executions: " + getglobalvar("runtime.executions") + "\n");

    free(script_handle);
    script_handle = NULL();
}

The first execution publishes 144 and an execution count of 1. The second publishes 625 and an execution count of 2. free() runs ondestroy(), which sets runtime.released to 1 before the object is destroyed.

Execution Model

Entry Point

Every executable runtime script needs a main() function. executescript() first evaluates top-level immediate code, such as file-scope declarations and initializers, then calls main() with no parameters.

main() runs once for every call to executescript(). The function's return value is not exposed to the calling script because executescript() itself does not return the runtime script's result.

Passing Data

executescript() accepts only the script handle, so arguments cannot be passed directly to main(). Use shared engine state or global variables to exchange data:

setglobalvar("runtime.input", value);
executescript(script_handle);
void result = getglobalvar("runtime.result");

Global variables are visible to both the caller and the runtime script. Namespacing exchange variables reduces accidental collisions with unrelated module code.

Persistent State

Each script object has its own local-variable list, accessed from inside that script with getlocalvar() and setlocalvar(). Values in this list survive repeated calls to executescript() and remain private to the handle.

Ordinary declared variables are part of the interpreter's execution state. Do not rely on them to preserve values between executions. Store persistent values in the local-variable list instead.

The caller cannot target another script handle with getlocalvar() or setlocalvar(). Publish values through global variables or another shared engine object when outside code needs them.

Lifecycle Callbacks

Runtime script objects recognize three special functions:

Function Invocation Purpose
oncreate() Once, immediately after successful compilation. Initialize the object's persistent local state or allocate resources.
main() Once per executescript() call. Perform the runtime script's requested work.
ondestroy() Once when the handle is released with free(), or during engine cleanup if it was never freed. Release resources owned by the script and perform final bookkeeping.

During oncreate(), the local variable iscopy is 0 for a newly compiled runtime script and localclear is 1. During ondestroy() for a freed runtime script, localclear is 2. OpenBOR clears these temporary local variables after the callback completes.

Keep the handle alive while resources or persistent state are still needed. Do not call free() on the currently executing handle from inside its own main(); let the owning script release it after executescript() returns.

Loading Source

loadscript() reads the complete source file into memory. OpenBOR first checks for a loose file at the supplied path, then checks the active packfile. Standard script preprocessing remains available, including macros and supported include or import directives.

One handle may receive more than one loadscript() call before compilation. Each successfully parsed file contributes source to the same interpreter:

void script_handle = allocscript("runtime_module");

loadscript(script_handle, "data/scripts/runtime/common.c");
loadscript(script_handle, "data/scripts/runtime/main.c");

compilescript(script_handle);
executescript(script_handle);
free(script_handle);

Every source fragment must parse successfully, and the combined program must compile as one script. Compile only after the final source file has been loaded.

Generating Source at Runtime

Filestream functions can construct a source file before it is loaded. This permits code generation from trusted module data:

void execute_generated_script()
{
    void source_stream = createfilestream();

    filestreamappend(
        source_stream,
        "void main() { setglobalvar(\"runtime.generated\", 1); }"
    );

    savefilestream(
        source_stream,
        "generated.c",
        "data/generated/"
    );

    closefilestream(source_stream);

    void script_handle = allocscript(
        "generated_runtime_script",
        "data/generated/generated.c"
    );

    loadscript(script_handle, "data/generated/generated.c");
    compilescript(script_handle);
    executescript(script_handle);
    free(script_handle);
}

The custom filestream pathname is relative to the engine's working directory, and loadscript() must be able to read the same loose path. Writable locations and working directories can vary by platform and launch environment. Test the complete save-and-load path on every supported target before relying on generated source in a distributed module.

Generated source is code, not ordinary save data. Never construct it from untrusted files, downloaded text, player names, or other uncontrolled input. Runtime scripts have access to the full OpenBOR script API.

Functions

allocscript()

void script_handle = allocscript(name[, comment]);

Allocates a script object and returns its pointer handle. name should be a nonempty string. OpenBOR uses it to identify the interpreter in diagnostic output. The optional comment provides additional diagnostic context and is useful for recording the source path or the system that owns the handle.

Allocation alone does not load, compile, or execute source. The returned object is registered with OpenBOR's managed script heap and remains allocated until passed to free() or released during engine shutdown.

loadscript()

loadscript(script_handle, path);

Loads and parses the source at path into a handle returned by allocscript(). Loose files take priority over files with the same path in the current packfile.

The function does not compile the source and does not return a success flag to script code. Missing files are not reliably reported through a script-visible result. Parse failures terminate execution with a script error. Use known paths and ship or generate every required source file before calling this function.

compilescript()

compilescript(script_handle);

Finalizes all source previously added to the handle. Compilation resolves native and script functions, imported functions, variables, constants, parameter references, branches, and the special entry points main(), oncreate(), and ondestroy().

Successful compilation immediately calls oncreate() when that function exists. Compilation does not produce a script-visible return value. Parse or compile errors are fatal rather than recoverable exceptions.

executescript()

executescript(script_handle);

Executes top-level immediate code and calls main(). The handle must refer to a successfully compiled script, and the compiled source must provide main().

The function accepts no arguments beyond the handle and does not return the value produced by main(). Use global variables or shared engine state for input and output. Repeated calls reuse the compiled program and the handle's persistent local-variable list.

OpenBOR tracks whether a script interpreter is already executing. Recursive re-entry into the same handle can interfere with its current interpreter state and should be avoided. The nonestedscript setting in data/script.txt can suppress an attempt to execute a script that is already active, but clear ownership and nonrecursive control flow remain preferable.

free()

free(script_handle);
script_handle = NULL();

Releases a runtime script object created by allocscript(). OpenBOR calls ondestroy() first when it exists, then clears the script's local variables, compiled interpreter, diagnostic comment, and object storage.

The old pointer becomes invalid immediately. Clear every stored copy and never execute, compile, load, or free it again. Engine shutdown releases handles that were not explicitly freed and reports outstanding managed script objects, though explicit ownership is recommended.

Error Behavior

These functions were designed as an advanced engine facility rather than a recoverable evaluator. Their public wrappers do not provide detailed status values:

  • loadscript(), compilescript(), and executescript() return no useful result to the calling script.
  • Invalid handles cause script-function exceptions.
  • Syntax errors are reported while loading and stop the module.
  • Compilation errors identify the script name, diagnostic comment, source, line, and column when available, then stop the module.
  • Runtime exceptions identify the executing script and the failing native call when available, then stop the module.
  • A missing or invalid main() causes execution to fail.

Runtime compilation is therefore best used with source that has already been validated during development. It is not suitable for probing arbitrary text to discover whether it compiles.

Practical Guidance

  • Allocate once, load all source, compile once, and execute as often as needed.
  • Give every handle a unique, descriptive name and a useful comment for diagnostics.
  • Keep the handle in a global or other long-lived owner when execution spans multiple events.
  • Use the handle's local-variable list for private persistent state.
  • Use namespaced global variables for arguments and results shared with the caller.
  • Free the handle when its compiled code and owned resources are no longer needed.
  • Clear every reference after free(); pointer handles are not self-invalidating.
  • Avoid recursive execution of the same handle.
  • Do not compile on every update tick. Parsing and compilation are setup work, while execution is the reusable stage.
  • Treat generated or externally stored source as trusted executable code.
  • Test loose-file and writable-path behavior on every target platform when using generated scripts.

See Also