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:
#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:
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:
natiny_script_set(script, "game.step_agents",
natiny_script_make_function(script, step_agents));
-- 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:
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.
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:
host.on_event(function(kind) print("event: " .. kind) end)
Arguments are valid until the C function returns. hold keeps the function
after that point:
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:
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);
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:
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:
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:
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
#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:
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_stdfor your own script,open_std_safefor somebody else's.- Values returned by a script are borrowed until the next call.
natiny_script_holdkeeps 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.