/

Extension API

Playground Docs Architecture Examples Concurrency Home GitHub

Overview

Lattice supports native extensions written in C (or any language that can produce a C-compatible shared library). Extensions are loaded at runtime via dlopen/dlsym and expose functions that Lattice code can call directly.

The extension API is designed around an opaque value type (LatExtValue). Extension code never sees the internal LatValue representation — all interaction happens through accessor and constructor functions declared in lattice_ext.h. This provides ABI stability: as long as the API version matches, extensions compiled against one version of Lattice will work with future releases.

The current API version is LATTICE_EXT_API_VERSION = 1. Extensions register functions via lat_ext_register() inside their lat_ext_init() entry point. The runtime collects these registrations and builds a Map object returned to user code.

// Loading an extension from Lattice code flux db = use("sqlite") db.exec(":memory:", "CREATE TABLE t (id INTEGER)")
ABI stability: Extensions compile against lattice_ext.h only. The opaque LatExtValue pointer hides all internal layout details, so extensions do not need to be recompiled when the Lattice runtime is updated (as long as LATTICE_EXT_API_VERSION remains the same).

Extension Structure

Every extension is a shared library that exports a single entry point: lat_ext_init(LatExtContext *ctx). The runtime calls this function once when the extension is loaded. Inside lat_ext_init, you call lat_ext_register() to expose each function by name.

// Minimal extension template #include "lattice_ext.h" static LatExtValue *my_func(LatExtValue **args, size_t argc) { // implementation return lat_ext_int(42); } void lat_ext_init(LatExtContext *ctx) { lat_ext_register(ctx, "my_func", my_func); }

The registered name ("my_func") becomes the key in the Map returned by use(). From Lattice code, you call it as ext.my_func().

Each registered function has the signature LatExtValue *(*)(LatExtValue **args, size_t argc). It receives an array of opaque argument pointers and returns an opaque result pointer. You use the constructor functions to create return values and the accessor functions to read argument values.

Value Types

All values passed between Lattice and extensions are wrapped in opaque LatExtValue pointers. Use constructors to create values and accessors to read them.

Constructors

lat_ext_int(int64_t v) LatExtValue *

Create an integer value.

lat_ext_float(double v) LatExtValue *

Create a floating-point value.

lat_ext_bool(bool v) LatExtValue *

Create a boolean value.

lat_ext_string(const char *s) LatExtValue *

Create a string value. The data is copied — the caller retains ownership of the original buffer.

lat_ext_nil(void) LatExtValue *

Create a nil value.

lat_ext_array(LatExtValue **elems, size_t len) LatExtValue *

Create an array from an array of element pointers.

lat_ext_map_new(void) LatExtValue *

Create an empty map.

lat_ext_map_set(LatExtValue *map, const char *key, LatExtValue *val)

Set a key-value pair on a map. Combine with lat_ext_map_new() to build maps incrementally.

Type Query

lat_ext_type(const LatExtValue *v) LatExtType

Returns the type of a value as a LatExtType enum. Possible values: LAT_EXT_INT, LAT_EXT_FLOAT, LAT_EXT_BOOL, LAT_EXT_STRING, LAT_EXT_ARRAY, LAT_EXT_MAP, LAT_EXT_NIL, LAT_EXT_OTHER.

Accessors

lat_ext_as_int(const LatExtValue *v) int64_t

Extract the integer value. Behavior is undefined if the value is not an integer.

lat_ext_as_float(const LatExtValue *v) double

Extract the floating-point value.

lat_ext_as_bool(const LatExtValue *v) bool

Extract the boolean value.

lat_ext_as_string(const LatExtValue *v) const char *

Extract the string as a null-terminated C string. The pointer is valid for the lifetime of the LatExtValue.

lat_ext_array_len(const LatExtValue *v) size_t

Get the number of elements in an array value.

lat_ext_array_get(const LatExtValue *v, size_t index) LatExtValue *

Get the element at the given index from an array value.

lat_ext_map_get(const LatExtValue *v, const char *key) LatExtValue *

Look up a value by key in a map. Returns NULL if the key is not found.

Error Handling

Extensions report errors by returning an error value from lat_ext_error(). The runtime treats these as Lattice error values that can be caught with try/catch or propagated with ?.

static LatExtValue *my_divide(LatExtValue **args, size_t argc) { if (argc < 2) return lat_ext_error("expected 2 arguments"); double a = lat_ext_as_float(args[0]); double b = lat_ext_as_float(args[1]); if (b == 0.0) return lat_ext_error("division by zero"); return lat_ext_float(a / b); }
Best practice: Always validate argc before accessing arguments. Check types with lat_ext_type() when the function accepts multiple types. Return a descriptive error message so users can diagnose problems from the Lattice side.

Building "hello"

This walkthrough builds a complete extension from scratch. The "hello" extension exposes a single function, hello_greet, that takes a name and returns a greeting string.

Step 1: Create the source file

Create extensions/hello/hello.c with the following content:

#include "lattice_ext.h" #include <stdio.h> #include <string.h> #include <stdlib.h> static LatExtValue *hello_greet(LatExtValue **args, size_t argc) { if (argc < 1) return lat_ext_error("expected at least 1 argument"); if (lat_ext_type(args[0]) != LAT_EXT_STRING) return lat_ext_error("argument must be a string"); const char *name = lat_ext_as_string(args[0]); // Build greeting string size_t len = strlen("Hello, !") + strlen(name) + 1; char *buf = malloc(len); snprintf(buf, len, "Hello, %s!", name); LatExtValue *result = lat_ext_string(buf); free(buf); // lat_ext_string copies the data return result; } void lat_ext_init(LatExtContext *ctx) { lat_ext_register(ctx, "hello_greet", hello_greet); }

Step 2: Create the Makefile

Create extensions/hello/Makefile:

CC = cc CFLAGS = -std=c11 -Wall -Wextra -Werror -fPIC -I../../include # Platform-specific shared library settings UNAME_S := $(shell uname -s) ifeq ($(UNAME_S),Darwin) SHARED_EXT = .dylib SHARED_FLAGS = -dynamiclib -undefined dynamic_lookup else SHARED_EXT = .so SHARED_FLAGS = -shared endif TARGET = hello$(SHARED_EXT) .PHONY: all clean all: $(TARGET) $(TARGET): hello.c $(CC) $(CFLAGS) $(SHARED_FLAGS) -o $@ $< clean: rm -f $(TARGET)

Step 3: Build the extension

$ cd extensions/hello $ make cc -std=c11 -Wall -Wextra -Werror -fPIC -I../../include -dynamiclib -undefined dynamic_lookup -o hello.dylib hello.c

Step 4: Use from Lattice

Create a file test_hello.lat in the project root:

flux ext = use("hello") let greeting = ext.hello_greet("World") print(greeting)

Step 5: Run it

$ ./clat test_hello.lat Hello, World!
Tip: The use() function searches for the shared library in several locations. See the Search Paths section for the full resolution order.

Existing Extensions

Lattice ships with several built-in extensions in the extensions/ directory. Each is a standalone shared library with its own Makefile.

sqlite

SQLite database access. Functions: exec(path, sql) for DDL/DML, query(path, sql) for SELECT (returns array of maps), last_insert_rowid(path) for the last inserted row ID. Databases are opened lazily and cached by path.

flux db = use("sqlite") db.exec(":memory:", "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)") db.exec(":memory:", "INSERT INTO users (name) VALUES ('Alice')") let rows = db.query(":memory:", "SELECT * FROM users") print(rows) // [{id: 1, name: "Alice"}]
redis

Redis client. Functions: connect(host, port), get(key), set(key, value), del(key), keys(pattern), publish(channel, message), subscribe(channel). Provides basic key-value operations and pub/sub messaging.

flux r = use("redis") r.connect("127.0.0.1", 6379) r.set("greeting", "hello") print(r.get("greeting")) // "hello"
ffi

Foreign Function Interface. Call arbitrary C functions from shared libraries at runtime. Load a library, look up symbols, and invoke them with typed arguments.

flux ffi = use("ffi") let lib = ffi.open("libm.dylib") let result = ffi.call(lib, "sqrt", "double", [16.0]) print(result) // 4.0
image

Image processing. Functions: load(path) to read an image file, resize(img, width, height) to scale, save(img, path) to write output. Supports PNG and JPEG formats.

flux img = use("image") let photo = img.load("input.png") let thumb = img.resize(photo, 128, 128) img.save(thumb, "thumb.png")
websocket

WebSocket client. Functions: connect(url) to establish a connection, send(ws, message) to send a text frame, recv(ws) to receive the next message. Useful for real-time communication.

flux ws = use("websocket") let conn = ws.connect("ws://localhost:8080") ws.send(conn, "ping") let reply = ws.recv(conn) print(reply)

Build Instructions

Extensions are compiled as position-independent shared libraries. The following Makefile template works on both macOS and Linux and can be copied as a starting point for any new extension.

CC = cc CFLAGS = -std=c11 -Wall -Wextra -Werror -fPIC -I../../include # Platform-specific shared library settings UNAME_S := $(shell uname -s) ifeq ($(UNAME_S),Darwin) SHARED_EXT = .dylib SHARED_FLAGS = -dynamiclib -undefined dynamic_lookup else SHARED_EXT = .so SHARED_FLAGS = -shared endif TARGET = myext$(SHARED_EXT) .PHONY: all clean install all: $(TARGET) $(TARGET): myext.c $(CC) $(CFLAGS) $(SHARED_FLAGS) -o $@ $< clean: rm -f $(TARGET) # Install to user-level extension directory install: $(TARGET) @mkdir -p $(HOME)/.lattice/ext cp $(TARGET) $(HOME)/.lattice/ext/

Key flags explained:

-fPIC

Generate position-independent code, required for shared libraries.

-I../../include

Add the Lattice include directory so lattice_ext.h is found. Adjust the path if your extension lives elsewhere.

-dynamiclib -undefined dynamic_lookup

macOS-specific: produce a dynamic library and allow symbols to be resolved at load time (since the extension links against the host process).

-shared

Linux-specific: produce a shared object (.so) file.

Linking external libraries: If your extension depends on an external library (e.g., -lsqlite3, -lhiredis), add it to the link command after $<. The runtime loads the extension via dlopen, so transitive dependencies must be resolvable at load time.

Search Paths

When you call use("name"), the Lattice runtime searches for the shared library in the following order. The first match wins.

1. ./extensions/<name>.{dylib|so}

Flat file in the extensions/ directory next to the running script or working directory.

2. ./extensions/<name>/<name>.{dylib|so}

Nested directory structure. This is the conventional layout for extensions with their own Makefile and source files.

3. ~/.lattice/ext/<name>.{dylib|so}

User-level installation directory. Use make install to copy the built library here for system-wide access.

4. $LATTICE_EXT_PATH/<name>.{dylib|so}

Custom path specified by the LATTICE_EXT_PATH environment variable. Useful for CI/CD pipelines or non-standard layouts.

Platform detection: On macOS the runtime looks for .dylib files; on Linux it looks for .so files. This is automatic — extension authors only need to ensure their Makefile produces the correct suffix for the build platform.

API Reference

Complete listing of all functions declared in lattice_ext.h. Extensions compile against this header only.

Registration

Function Signature Description
lat_ext_register void lat_ext_register(LatExtContext *ctx, const char *name, LatExtFn fn) Register a named function in the extension context. Called inside lat_ext_init().

Constructors

Function Signature Description
lat_ext_int LatExtValue *lat_ext_int(int64_t v) Create an integer value.
lat_ext_float LatExtValue *lat_ext_float(double v) Create a floating-point value.
lat_ext_bool LatExtValue *lat_ext_bool(bool v) Create a boolean value.
lat_ext_string LatExtValue *lat_ext_string(const char *s) Create a string value. Copies the input data.
lat_ext_nil LatExtValue *lat_ext_nil(void) Create a nil value.
lat_ext_array LatExtValue *lat_ext_array(LatExtValue **elems, size_t len) Create an array from element pointers.
lat_ext_map_new LatExtValue *lat_ext_map_new(void) Create an empty map.
lat_ext_map_set void lat_ext_map_set(LatExtValue *map, const char *key, LatExtValue *val) Set a key-value pair on a map.
lat_ext_error LatExtValue *lat_ext_error(const char *msg) Create an error value with the given message.

Type Query

Function Signature Description
lat_ext_type LatExtType lat_ext_type(const LatExtValue *v) Returns the type of a value. One of: LAT_EXT_INT, LAT_EXT_FLOAT, LAT_EXT_BOOL, LAT_EXT_STRING, LAT_EXT_ARRAY, LAT_EXT_MAP, LAT_EXT_NIL, LAT_EXT_OTHER.

Accessors

Function Signature Description
lat_ext_as_int int64_t lat_ext_as_int(const LatExtValue *v) Extract integer value.
lat_ext_as_float double lat_ext_as_float(const LatExtValue *v) Extract floating-point value.
lat_ext_as_bool bool lat_ext_as_bool(const LatExtValue *v) Extract boolean value.
lat_ext_as_string const char *lat_ext_as_string(const LatExtValue *v) Extract null-terminated string pointer.
lat_ext_array_len size_t lat_ext_array_len(const LatExtValue *v) Get the number of elements in an array.
lat_ext_array_get LatExtValue *lat_ext_array_get(const LatExtValue *v, size_t index) Get array element at index.
lat_ext_map_get LatExtValue *lat_ext_map_get(const LatExtValue *v, const char *key) Look up a map value by key. Returns NULL if not found.

Cleanup

Function Signature Description
lat_ext_free void lat_ext_free(LatExtValue *v) Free a heap-allocated LatExtValue. Call this on values you created but are not returning to the runtime.
Memory ownership: Values returned from an extension function are owned by the runtime — do not free them. Values you create internally (e.g., temporary computations) that are not returned should be freed with lat_ext_free() to avoid leaks.