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)")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
Create an integer value.
Create a floating-point value.
Create a boolean value.
Create a string value. The data is copied — the caller retains ownership of the original buffer.
Create a nil value.
Create an array from an array of element pointers.
Create an empty map.
Set a key-value pair on a map. Combine with lat_ext_map_new() to build maps incrementally.
Type Query
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
Extract the integer value. Behavior is undefined if the value is not an integer.
Extract the floating-point value.
Extract the boolean value.
Extract the string as a null-terminated C string. The pointer is valid for the lifetime of the LatExtValue.
Get the number of elements in an array value.
Get the element at the given index from an array value.
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);
}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.cStep 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!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 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 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"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.0Image 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 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:
Generate position-independent code, required for shared libraries.
Add the Lattice include directory so lattice_ext.h is found. Adjust the path if your extension lives elsewhere.
macOS-specific: produce a dynamic library and allow symbols to be resolved at load time (since the extension links against the host process).
Linux-specific: produce a shared object (.so) file.
-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.
Flat file in the extensions/ directory next to the running script or working directory.
Nested directory structure. This is the conventional layout for extensions with their own Makefile and source files.
User-level installation directory. Use make install to copy the built library here for system-wide access.
Custom path specified by the LATTICE_EXT_PATH environment variable. Useful for CI/CD pipelines or non-standard layouts.
.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. |
lat_ext_free() to avoid leaks.
Lattice