Running a script

Natiny can run Lua scripts. NatinyScript is a handle for a Lua state. You create it, run a resource, call its functions, and destroy it.

You can use a script for game content, tools, or a mod. The same API runs all of them.

This guide starts with a small example. It then shows how to pass functions and tables, call the script every frame, and handle errors.

What a script is

Most script functions take NatinyScript as their first argument.

A new script has no standard library or engine modules. Open the libraries and modules that it needs. Modules that you do not open are not linked into the program.

The smallest program that runs a script

Create a script, open its libraries, load a file, and run it:

C
#include "natiny.h"
#include <stdio.h>

int main(void)
{
    NatinyScript   script;
    NatinyResource source;
    int            status;

    natiny_resource_mount("data", "");

    script = natiny_script_create();
    if (!script) return 1;

    natiny_script_open_std(script);
    natiny_script_open_color(script);
    natiny_script_open_render(script);
    natiny_script_open_window(script);
    natiny_script_open_backend(script);

    source = natiny_resource_load("main.lua");
    if (!source) {
        fprintf(stderr, "no data/main.lua\n");
        return 1;
    }

    status = natiny_script_run(script, source);
    natiny_resource_destroy(source);

    if (status != NATINY_SCRIPT_OK)
        fprintf(stderr, "script: %s\n", natiny_script_get_error(script));

    natiny_script_destroy(script);
    return 0;
}

Use the Lua program from Hello, Natiny as data/main.lua. The program opens a window and draws a moving square.

NATINY_SCRIPT_LOAD_ERROR means that Lua could not compile the file. NATINY_SCRIPT_RUN_ERROR means that an error happened while it was running. Use natiny_script_get_error to get the error message.

What those calls did

natiny_resource_mount adds data to the resource search stack. After this call, "main.lua" can load data/main.lua. An empty key reads loose files and unencrypted packs.

natiny_script_open_std opens the Lua standard library. Its require() reads modules from mounted resources.

The four open_* calls open the engine modules used by the Lua file. Use natiny_script_open_all when the file needs all engine modules.

natiny_script_run runs a NatinyResource. The resource keeps its path, so Lua can include it in error messages.

natiny.backend.init() resets the engine. If the Lua file calls it, create engine objects after that call.

Giving the script a function written in C

A Lua script can call a function written in C. A script function written in C has this form:

A C function the script can call has one shape:

C
static uint32_t step_agents(NatinyScript script,
                            const NatinyScriptValue* args, uint32_t count,
                            NatinyScriptValue* results)
{
    float    dt;
    uint32_t alive;

    if (count != 1 || args[0].type != NATINY_SCRIPT_NUMBER) {
        natiny_script_fail(script, "step_agents: expected dt");
        return 0;
    }

    dt    = (float)natiny_script_to_number(args[0]);
    alive = simulate_all(dt);

    results[0] = natiny_script_make_number(alive);
    return 1;      /* how many results we filled in */
}

natiny_script_make_function creates a script value for the C function. natiny_script_set gives it a name:

C
natiny_script_set(script, "game.step_agents",
                  natiny_script_make_function(script, step_agents));
Lua
-- in the script
local alive = game.step_agents(dt)

The dotted path creates missing tables. In this example, it creates game if the table does not exist. A function can also be stored in another table or passed to a call.

natiny_script_fail reports an error but still returns normally from the C function. The error is raised in Lua after the C function returns.

Strings in args belong to the Lua state. They are valid until the C function returns. Copy a string if you need to keep it.

Calling into the script

Use natiny_script_call to call a Lua function from C:

C
NatinyScriptValue args[2];
NatinyScriptValue results[NATINY_SCRIPT_MAX_RESULTS];
uint32_t          got = 0;

args[0] = natiny_script_make_string("autosave");
args[1] = natiny_script_make_number(3);

if (natiny_script_call(script, "game.save", args, 2, results, &got)) {
    if (got > 0 && natiny_script_to_bool(results[0]))
        printf("saved\n");
} else {
    fprintf(stderr, "save failed: %s\n", natiny_script_get_error(script));
}

Arguments and results use NatinyScriptValue. This type can also hold a table or function. Use natiny_script_make_* to create a value and natiny_script_to_* to read it.

The call returns false when the name is missing, the value is not a function, or the Lua function raises an error. Use natiny_script_get_error to read the error.

Calling it every frame

natiny_script_call searches for the function by name on every call. For a function called every frame, read it once and keep it.

natiny_script_get returns a borrowed value. It is valid until the next call into the script. natiny_script_hold keeps the value valid until you release it.

C
NatinyScriptValue on_frame =
    natiny_script_hold(script, natiny_script_get(script, "game.on_frame"));

/* inside the frame callback */
if (on_frame.type == NATINY_SCRIPT_FUNCTION) {
    NatinyScriptValue dt_arg = natiny_script_make_number(dt);
    if (!natiny_script_call_value(script, on_frame, &dt_arg, 1, NULL, NULL)) {
        fprintf(stderr, "on_frame: %s\n", natiny_script_get_error(script));
        natiny_script_release(script, on_frame);
        on_frame = natiny_script_make_nil();   /* stop calling a broken hook */
    }
}

/* at shutdown */
natiny_script_release(script, on_frame);

If on_frame does not exist, get returns NATINY_SCRIPT_NIL. The type check makes the callback optional. A held function stays valid even if the Lua code changes the original name.

Pass NULL for results when the return values are not needed.

Keeping a callback the script gives you

A Lua script can pass an unnamed function to C. Use hold when C must call it later:

Lua
host.on_event(function(kind) print("event: " .. kind) end)

Arguments are valid until the C function returns. hold keeps the function after that point:

C
static NatinyScriptValue on_event;

static uint32_t host_on_event(NatinyScript script,
                              const NatinyScriptValue* args, uint32_t count,
                              NatinyScriptValue* results)
{
    (void)results;

    if (count != 1 || args[0].type != NATINY_SCRIPT_FUNCTION) {
        natiny_script_fail(script, "on_event: expected a function");
        return 0;
    }
    natiny_script_release(script, on_event);      /* the previous one, if any */
    on_event = natiny_script_hold(script, args[0]);
    return 0;
}

Call the saved function later with natiny_script_call_value. The function does not need a Lua name.

Handing over a table

Create a Lua table with natiny_script_make_table:

C
NatinyScriptValue config = natiny_script_make_table(script);

natiny_script_table_set(script, config, "title",
                        natiny_script_make_string("Sponza"));
natiny_script_table_set(script, config, "width",
                        natiny_script_make_number(1280));
natiny_script_table_set_item(script, config, 1,
                             natiny_script_make_string("vulkan"));

natiny_script_set(script, "game.config", config);
Lua
print(game.config.title, game.config.width, game.config[1])

The table value is borrowed. It is valid until the next natiny_script_run, natiny_script_call, or natiny_script_call_value on that script. Fill the table and give it a Lua name before the next call. The Lua state then keeps it.

A borrowed table can also be passed directly into a call:

C
NatinyScriptValue event = natiny_script_make_table(script);
natiny_script_table_set(script, event, "kind",
                        natiny_script_make_string("explosion"));

natiny_script_call(script, "game.on_event", &event, 1, NULL, NULL);

A table returned by natiny_script_get or a call is also borrowed:

C
NatinyScriptValue manifest = natiny_script_get(script, "manifest");

if (manifest.type == NATINY_SCRIPT_TABLE) {
    NatinyScriptValue name =
        natiny_script_table_get(script, manifest, "name");
    printf("%.*s\n", (int)name.string.size, name.string.data);
}

Strings use the same rule. Use natiny_script_hold for a string, table, or function that must stay valid after the next call. Release only held values.

Running a script you did not write

Use the safe standard library for code that you did not write. A downloaded level or a mod is an example:

C
NatinyScript script = natiny_script_create();

natiny_script_open_std_safe(script);      /* not open_std */
natiny_script_open_all(script);

natiny_script_open_std_safe does not open io, os, debug, dofile, loadfile, or the native library loader. Its require() reads only from mounted resources.

This does not restrict engine handles. The script can use any handle that you give it. It can also use every module opened with open_all. The safe library does not stop an endless loop. Do not run untrusted code in the same process.

You can run several scripts at the same time. Create a separate NatinyScript for each one. They have separate globals and libraries, but use the same engine.

Common problems

The new script has no print. A new script has no libraries. Call natiny_script_open_std or natiny_script_open_std_safe.

natiny_script_run reports that the resource holds nothing to run. The resource is empty or loading failed. Check that natiny_resource_load returned a valid handle and that its folder is mounted.

An error starts with (script):12: instead of a file name. A resource created with from_data has no path. Load the resource by path to include the file name.

A string from the script is no longer valid. Returned strings are borrowed. Copy the string or keep its value with natiny_script_hold.

A window was destroyed after the script called natiny.backend.init(). This call resets the engine. Let the Lua script control the engine lifetime, or do not open the backend module for it.

The whole thing

C
#include "natiny.h"
#include <stdio.h>

static NatinyScript      script;
static NatinyScriptValue on_frame;

static uint32_t host_log(NatinyScript from,
                         const NatinyScriptValue* args, uint32_t count,
                         NatinyScriptValue* results)
{
    (void)results;

    if (count != 1 || args[0].type != NATINY_SCRIPT_STRING) {
        natiny_script_fail(from, "log: expected a message");
        return 0;
    }
    printf("[script] %.*s\n", (int)args[0].string.size, args[0].string.data);
    return 0;
}

static void frame(float dt, void* userdata)
{
    (void)userdata;

    if (on_frame.type == NATINY_SCRIPT_FUNCTION) {
        NatinyScriptValue dt_arg = natiny_script_make_number(dt);
        if (!natiny_script_call_value(script, on_frame, &dt_arg, 1, NULL, NULL)) {
            fprintf(stderr, "on_frame: %s\n", natiny_script_get_error(script));
            natiny_script_release(script, on_frame);
            on_frame = natiny_script_make_nil();
        }
    }
}

int main(void)
{
    NatinyResource    source;
    NatinyScriptValue config;
    int               status;

    natiny_resource_mount("data", "");

    script = natiny_script_create();
    if (!script) return 1;

    natiny_script_open_std(script);
    natiny_script_open_all(script);
    natiny_script_set(script, "host.log",
                      natiny_script_make_function(script, host_log));

    config = natiny_script_make_table(script);
    natiny_script_table_set(script, config, "title",
                            natiny_script_make_string("Sponza"));
    natiny_script_set(script, "game.config", config);

    source = natiny_resource_load("main.lua");
    if (!source) {
        fprintf(stderr, "no data/main.lua\n");
        natiny_script_destroy(script);
        return 1;
    }

    status = natiny_script_run(script, source);
    natiny_resource_destroy(source);

    if (status != NATINY_SCRIPT_OK) {
        fprintf(stderr, "script: %s\n", natiny_script_get_error(script));
        natiny_script_destroy(script);
        return 1;
    }

    on_frame = natiny_script_hold(script,
                                  natiny_script_get(script, "game.on_frame"));
    if (on_frame.type == NATINY_SCRIPT_FUNCTION) {
        natiny_backend_loop(frame, NULL);
        natiny_script_release(script, on_frame);
    }

    natiny_script_call(script, "game.on_unload", NULL, 0, NULL, NULL);
    natiny_script_destroy(script);
    natiny_backend_shutdown();
    return 0;
}

The script it runs can be as small as this:

Lua
local natiny = require("natiny")

assert(natiny.backend.init())
local window = natiny.window.create(game.config.title, 960, 540)

host.log("started")

-- `game` already exists: the C side put game.config there.
function game.on_frame(dt)
    natiny.window.bind(window, function()
        natiny.render.clear(natiny.color.from_hex("#121721"))
    end)
end

Summary

The main points are:

  • Open only the modules that the script needs.
  • open_std for your own script, open_std_safe for somebody else's.
  • Values returned by a script are borrowed until the next call. natiny_script_hold keeps a value until you release it.

The same API is available in Lua. Switch this page to Lua for that version. For details about each function, start at natiny_script_create.