Beyond Lua: Why Janet is the Ultimate Embeddable Language for Modern Systems
Explore the power of Janet, a lightweight, embeddable Lisp-like language designed for modern application scripting. Learn how its built-in PEGs, robust C API, and native data structures make it a compelling alternative to Lua.
The Quest for the Perfect Embeddable Language
For decades, application developers looking to provide scripting capabilities, game engine modding, or runtime configuration have defaulted to Lua. It is fast, lightweight, and incredibly simple to embed in C or C++ applications. However, as software systems grow increasingly complex, Lua's limitations begin to show. The lack of standard data structures (everything is a table), the absence of a built-in macro system, and its idiosyncratic 1-based indexing often leave systems engineers wishing for something more robust.
Enter Janet. First released in late 2018 and steadily gaining traction, Janet is a functional, imperative, and embeddable Lisp-like language. It compiles to bytecode, runs on a lightweight virtual machine, and offers seamless C integration. But unlike traditional Lisps, Janet treats arrays, tuples, tables, and structs as first-class citizens instead of relying on cons cells.
In this technical deep dive, we will explore why Janet is emerging as a premier choice for systems scripting, how its architecture compares to Lua, and how you can embed it into your next project.
What Makes Janet Unique?
To understand Janet, you must look at how it bridges the gap between the expressive power of Lisp and the practical demands of modern systems programming. Janet is not just another Scheme clone; it is designed from the ground up for performance, memory efficiency, and developer ergonomics.
1. Modern, First-Class Data Structures
In traditional Lisps, the fundamental building block is the linked list (cons cells). This is notoriously cache-unfriendly on modern CPU architectures. Janet bypasses this by introducing concrete, flat memory structures:
- Arrays and Tables: Mutable sequences and key-value maps.
- Tuples and Structs: Immutable sequences and key-value maps. Because they are immutable, they can be hashed and used as keys in tables or other structs.
This distinction allows developers to write highly optimized, predictable code without worrying about constant heap allocation and pointer chasing.
2. Built-In Parsing Expression Grammars (PEGs)
One of Janet's killer features is its native support for Parsing Expression Grammars (PEGs). While most languages force you to pull in heavy regular expression engines or write complex parser combinators, Janet includes a fast, bytecode-compiled PEG engine in its standard library. This makes text processing, configuration parsing, and DSL (Domain Specific Language) creation incredibly elegant and fast.
3. Hygienic Macros
As a Lisp, Janet provides a complete macro system. This allows you to extend the language syntax itself, creating compile-time abstractions that generate zero runtime overhead. Unlike Lua, where metaprogramming relies on complex metatables and runtime overhead, Janet macros execute during compilation, resulting in highly optimized bytecode.
Under the Hood: The Janet Virtual Machine
Janet's VM is a register-based virtual machine, similar in design to LuaJIT or the Dalvik VM. This contrasts with stack-based VMs (like the JVM or Python's VM). Register-based VMs generally require fewer instructions to execute the same logic, leading to better instruction cache utilization on modern CPUs.
Memory Management
Janet uses a fast, generational, mark-and-sweep garbage collector. The GC is designed to minimize pause times, which is critical for real-time applications like games or audio synthesis engines. Furthermore, because Janet supports immutable tuples and structs, the GC can optimize memory allocation by reusing identical immutable structures (interning), reducing overall memory pressure.
Compilation and Bytecode
When you load a Janet file, the compiler parses the source code into abstract syntax trees (ASTs), macro-expands it, and compiles it directly into bytecode. This bytecode can be serialized into a standalone image file, allowing for instantaneous startup times in production environments.
Embedding Janet in C: A Practical Tutorial
One of Janet's primary design goals is ease of embedding. The entire Janet runtime can be compiled from a single C source file (janet.c) and a single header (janet.h). This means you don't need to manage complex dynamic library linking; you can simply drop these two files directly into your C/C++ project.
Let's walk through a complete example of embedding the Janet VM in a C application, executing some code, and exposing a custom C function to the Janet runtime.
Step 1: Initializing the VM
First, we include the Janet header and initialize the runtime environment:
#include <stdio.h>
#include "janet.h"
int main(int argc, char *argv[]) {
// Initialize the Janet VM
janet_init();
// Get the core environment (contains standard functions)
JanetTable *env = janet_core_env(NULL);
// Execute a simple script string
const char *code = "(print \"Hello from the Janet VM!\")";
int result = janet_dostring(env, code, "main", NULL);
if (result != 0) {
printf("Execution failed!\n");
}
// Clean up and deallocate the VM
janet_deinit();
return 0;
}
Step 2: Registering a Custom C Function
To make our host application scriptable, we need to expose C functions to the Janet runtime. Janet makes this incredibly simple via the janet_cfuns interface.
Let's create a custom C function that calculates the Fibonacci sequence and expose it to Janet:
#include "janet.h"
// Our custom C function
static Janet c_fib(int32_t argc, Janet *argv) {
// Fix the arity of the function (expects exactly 1 argument)
janet_fixarity(argc, 1);
// Retrieve the argument as an integer
int32_t n = janet_getinteger(argv, 0);
if (n < 0) janet_panic("expected non-negative integer");
int32_t a = 0, b = 1, temp;
for (int32_t i = 0; i < n; i++) {
temp = a + b;
a = b;
b = temp;
}
// Return the result wrapped in a Janet value type
return janet_wrap_integer(a);
}
// Map our C function to a Janet identifier
static const JanetReg cfuns[] = {
{"fib", c_fib, "(fib n)\n\nCalculates the nth Fibonacci number in C."},
{NULL, NULL, NULL}
};
int main() {
janet_init();
JanetTable *env = janet_core_env(NULL);
// Register our functions into the environment
janet_cfuns(env, "custom", cfuns);
// Run a script that calls our C function
const char *script = "(print \"Fibonacci(10) is: \" (custom/fib 10))";
janet_dostring(env, script, "main", NULL);
janet_deinit();
return 0;
}
In this example, janet_getinteger and janet_wrap_integer handle the marshaling of values between C types and Janet's dynamic value type (Janet). Janet values are represented using NaN-boxing, a technique where double-precision floats, pointers, integers, and booleans are packed into a single 64-bit word, making value passing incredibly fast and lightweight.
When to Choose Janet Over Lua?
While Lua is an excellent tool, Janet offers distinct advantages in several scenarios:
| Feature | Lua (5.4) | Janet | | :--- | :--- | :--- | | Syntax | Algol-like (imperative) | Lisp-like (S-expressions) | | Macros | No (requires external preprocessors) | Yes (hygienic, compile-time) | | Data Structures | Tables only | Arrays, Tuples, Tables, Structs | | Text Parsing | Basic pattern matching | Built-in PEG engine | | Concurrency | Coroutines | Fibers, Channels, Thread-pools | | C Integration | Requires stack manipulation | Straightforward value wrapping |
If your project requires extensive configuration parsing, complex DSLs, or highly structured data processing, Janet's built-in PEGs and macro system will save you thousands of lines of code compared to Lua.
Real-World Applications
Where is Janet being used today?
- Game Development: Developers are using Janet as a scripting language for custom game engines. The combination of lightweight fibers (for cooperative multitasking/game scripting) and hot-reloading makes it a joy to write gameplay logic.
- Command Line Tools: Because Janet compiles down to a small, static binary, it is highly suited for building fast CLI utilities that start instantly and consume minimal memory.
- Embedded Systems and IoT: While not as minimal as Lua, Janet's tiny footprint (the entire VM is under 1MB) makes it viable for medium-tier embedded devices running Linux or RTOS environments.
Conclusion: The Modern Scripting Tool
Janet represents a thoughtful evolution of embeddable scripting languages. It preserves the simplicity and lightness of Lua while introducing the expressiveness of Lisp and the memory-efficiency of modern data structures. If you are looking for a scripting engine that offers compile-time safety via macros, powerful text parsing out of the box, and a painless C API, Janet is highly deserving of a place in your engineering toolkit.