One of the basic means for extending Teascript is for the application to register new C functions into Teascript.

When we say that Teascript can call C functions, this does not mean that Teascript can call any C function. As we saw in chapter 10, C functions that Teascript can call must follow a specific protocol for receiving arguments and returning values through the stack. A function with the wrong signature simply cannot be registered. The type tea_CFunction captures this protocol:

typedef void (*tea_CFunction)(tea_State* T);

Every C function callable from Teascript takes a tea_State* and returns nothing. Arguments arrive on the stack and return values are pushed onto it before the function returns. Everything else — type checking, argument counts, error reporting — is the function’s own responsibility.

C Functions

Let’s write a simple C function and make it available to Teascript. We will implement a clamp function that restricts a number to a given range:

static void c_clamp(tea_State* T)
{
    double value = tea_check_number(T, 0);
    double min   = tea_check_number(T, 1);
    double max   = tea_check_number(T, 2);

    if(min > max)
        tea_error(T, "min must be less than or equal to max");

    double result = value < min ? min : value > max ? max : value;
    tea_push_number(T, result);
}

Arguments are at indices 0, 1, 2 and so on. tea_check_number retrieves the value at the given index and raises a descriptive error if the value is not a number, so we never have to handle the wrong-type case manually. We push exactly one return value and return.

To make this function available as a global in Teascript, use tea_push_cfunction followed by tea_set_global:

tea_push_cfunction(T, c_clamp, 3, 0);
tea_set_global(T, "clamp");

The third argument to tea_push_cfunction is the expected argument count, and the fourth is the number of optional arguments. Teascript uses these to check the call site — passing the wrong number of arguments raises an error before your function is even entered. Pass TEA_VARG as the argument count for variadic functions that accept any number of arguments.

From Teascript the function is now indistinguishable from one defined in the language:

print(clamp(15, 0, 10))   # 10
print(clamp(-3, 0, 10))   # 0
print(clamp(7, 0, 10))    # 7

Closures with upvalues work similarly. tea_push_cclosure creates a C function that closes over a set of values currently on the stack:

void tea_push_cclosure(tea_State* T, tea_CFunction fn, int nupvalues, int nargs, int nopts);

Push the upvalues first, then call tea_push_cclosure. Inside the function body, retrieve upvalues by index using the tea_upvalue_index macro:

static void c_adder(tea_State* T)
{
    double addend = tea_get_number(T, tea_upvalue_index(0));
    double value  = tea_check_number(T, 0);
    tea_push_number(T, value + addend);
}

/* Create an adder that adds 10 to its argument */
tea_push_number(T, 10.0);
tea_push_cclosure(T, c_adder, 1, 1, 0);
tea_set_global(T, "add10");
print(add10(5))    # 15
print(add10(32))   # 42

Upvalue indices are negative and count from TEA_UPVALUES_INDEX. Each closure instance gets its own copy of the upvalues, so you can create multiple closures from the same tea_CFunction with different captured state.

C Modules

Registering functions one by one as globals works for small integrations, but for anything substantial you should group related functions into a module. Modules keep the global namespace clean and let scripts import only what they need.

The tea_Reg struct describes an array of functions to register together:

typedef struct tea_Reg
{
    const char* name;
    tea_CFunction fn;
    int nargs;
    int nopts;
} tea_Reg;

The array must be terminated with a sentinel entry where all fields are NULL or zero. tea_create_module takes a name and a tea_Reg array and builds the module in one call:

static void c_clamp(tea_State* T) { /* ... */ }
static void c_lerp(tea_State* T)  { /* ... */ }
static void c_sign(tea_State* T)  { /* ... */ }

static const tea_Reg mathx_module[] = {
    { "clamp", c_clamp, 3, 0 },
    { "lerp",  c_lerp,  3, 0 },
    { "sign",  c_sign,  1, 0 },
    { NULL, NULL, 0, 0 }
};

TEA_API void tea_import_mathx(tea_State* T)
{
    tea_create_module(T, "mathx", mathx_module);
}

Once registered, the module is importable from Teascript like any other:

import mathx

print(mathx.clamp(15, 0, 10))
print(mathx.lerp(0, 100, 0.25))
print(mathx.sign(-3))

Or with selective imports:

import mathx { clamp, lerp }

print(clamp(15, 0, 10))

For larger modules it is often natural to split functionality into submodules. tea_create_submodule works identically to tea_create_module but nests the result under the module currently on top of the stack:

static const tea_Reg mathx_trig[] = {
    { "sin", c_sin, 1, 0 },
    { "cos", c_cos, 1, 0 },
    { "tan", c_tan, 1, 0 },
    { NULL, NULL, 0, 0 }
};

TEA_API void tea_import_mathx(tea_State* T)
{
    tea_create_module(T, "mathx", mathx_module);
    tea_create_submodule(T, "trig", mathx_trig);
}
from mathx import trig

print(trig.sin(0))

If your module functions need shared state — a cached resource, a configuration value, a counter — use upvalues. Push the shared state before calling tea_set_funcs, which registers a tea_Reg array and distributes the upvalues on the stack to every function in the array:

void tea_set_funcs(tea_State* T, const tea_Reg* reg, int nup);

All functions registered in the same tea_set_funcs call share the same upvalue slots. This is the standard way to give a module its own private state without resorting to C globals.

C Classes

C modules expose collections of free functions. When you need to associate behavior with a specific kind of value — as we did with RingBuffer in chapter 14 — the right abstraction is a C class.

The tea_Methods struct extends tea_Reg with a type tag that controls how a method participates in Teascript’s dispatch:

typedef struct tea_Methods
{
    const char* name;
    const char* type;
    tea_CFunction fn;
    int nargs;
    int nopts;
} tea_Methods;

The type field can be:

TagMeaning
"method"A regular callable method
"get"A property getter, invoked on obj.name
"set"A property setter, invoked on obj.name = value
"static"A static method, callable on the class itself

tea_create_class takes a class name and a tea_Methods array and registers the class in the current module:

static void point_tostring(tea_State* T)
{
    /* ... */
}

static void point_get_x(tea_State* T)
{
    /* ... */
}

static void point_translate(tea_State* T)
{
    /* ... */
}

static void point_origin(tea_State* T)
{
    tea_push_number(T, 0);
    tea_push_number(T, 0);
    /* construct and push a Point at (0, 0) */
}

static const tea_Methods point_methods[] = {
    { "__string",  "method", point_tostring,  1, 0 },
    { "x",         "get",    point_get_x,     1, 0 },
    { "translate", "method", point_translate, 3, 0 },
    { "origin",    "static", point_origin,    0, 0 },
    { NULL, NULL, NULL, 0, 0 }
};

"static" methods receive no implicit self — the class itself is not passed either. They are called directly on the class:

var p = Point.origin()

Classes and modules compose naturally. A module can export both free functions and classes:

TEA_API void tea_import_geometry(tea_State* T)
{
    tea_create_class(T, "Point", point_methods);
    tea_create_class(T, "Rect",  rect_methods);
    tea_create_module(T, "geometry", geometry_funcs);
}
import geometry { Point, Rect }

var p = Point(3, 4)
var r = Rect(p, 100, 200)

The class is just another value inside the module. From a script’s perspective there is no observable difference between a class written in Teascript and one registered from C. Both support construction, method calls, property access, operator overloading, and inheritance — all through the same mechanisms.

There is one important thing a C class cannot do that a Teascript class can: define init through the methods table in a way that participates in inheritance chains. The constructor is the function registered in the module’s tea_Reg — it is a plain C function that calls tea_new_udata and sets up the block. If you need a C class to be subclassable from Teascript, you will need to expose the initialization logic in a way that a subclass init can invoke explicitly.