Language comparison
Status: Implemented for the model: three languages, all sandboxed, the same API. Rows that mention planned parts say so.
All three languages call the same API, generated from one clight.idl. This page puts the differences side by side. For “which one should I pick”, see Choosing a language and runtime.
At a glance
| C | C++ | Lua | |
|---|---|---|---|
| Binding | clight.h (the ABI) + clight_easy.h |
clight.hpp, header-only |
Generated host-side binding + prelude |
| Runtime | WebAssembly (Wasmtime, AOT) | WebAssembly (Wasmtime, AOT) | Lua 5.4 VM |
| Build step | clight build (wasi-sdk) |
clight build (wasi-sdk) |
none |
| One file for all OSes | yes | yes | yes |
| Sandboxed | yes | yes | yes |
| Hot reload | rebuild: the module reloads in dev mode, CLIGHT_MOD_HOT_STATE keeps state |
rebuild: the module reloads in dev mode, CLIGHT_MOD_HOT_STATE keeps state |
on save, keeps mod.persist |
| Files | WASI: / read-only, /data read/write |
the same | none yet |
The same task in three languages
Give a player 5 emeralds when they kill a zombie.
static void on_death(void* payload, void* user) {
cl_ev_entity_death* e = CL_EVENT(cl_ev_entity_death, payload);
(void)user;
if (!CL_IS_NULL(e->attacker) && cl_entity_is_player(e->attacker))
cl_easy_give(e->attacker, "minecraft:emerald", 5);
}
/* mod_main: cl_subscribe_opts o = CL_SUBSCRIBE_OPTS_INIT;
o.filter = cl_ident_intern(CL_STR("minecraft:zombie"));
return cl_event_subscribe(CL_EV_ENTITY_DEATH, &o, on_death, NULL, NULL); */
clight::on<clight::ev::EntityDeath>([](cl_ev_entity_death& e) {
clight::Entity killer = e.attacker;
if (killer && killer.is_player()) killer.give("minecraft:emerald", 5);
}, {.filter = "minecraft:zombie"}).detach();
cl.on("entity.death", function(event)
local killer = event.attacker
if killer and killer:is_player() then killer:give("minecraft:emerald", 5) end
end, { filter = "minecraft:zombie" })
Concepts side by side
| Concept | C | C++ | Lua |
|---|---|---|---|
| Errors | cl_result codes, cl_last_error() |
clight::Expected<T> (no exceptions) |
raised cl.Error, cl.try.* |
| Owned values | cl_*_release by hand |
RAII (ItemStack, Text, Buf) |
GC, <close> |
| Handles | struct { uint64_t h; } |
value classes | userdata |
| Event payload | borrowed struct pointer | the payload struct by reference | a view with fields and easy fields |
| Unsubscribing | cl_event_unsubscribe(sub) |
destroy the Subscription (or .detach() to keep it) |
sub:cancel() |
| Later / repeating | cl_sched_run_server, cl_sched_repeat_server |
clight::after, clight::every (lambdas) |
cl.sched.run_server, cl.spawn + cl.sleep_ticks |
| Strings in | cl_str (CL_STR("...")), or C strings with clight_easy.h |
std::string_view |
string |
| Strings out | caller buffer, two-call pattern | std::string |
string |
| Logging | CL_LOG(fmt, ...) |
clight::log::info("{}", x) |
cl.log(...), cl.log.info("%d", x) |
| Worker jobs, parallel loops, profiler zones | planned | planned | planned |
Performance profile
| C/C++ (WebAssembly) | Lua | |
|---|---|---|
| Heavy maths | close to native (AOT) | much slower; move it to a WASM mod |
| Event handlers and glue | fast | fast enough for handlers, plugins and UI |
| API calls | cross the sandbox boundary | cross the binding |
| Runs one mod’s handlers concurrently | no (per-mod lock) | no (per-mod lock) |
Safety profile
| Risk | C/C++ (WebAssembly) | Lua |
|---|---|---|
| Can crash the game | no (traps are contained) | no (errors are contained) |
| Can reach files outside its own folder, or the network | no | no |
| Capabilities enforced | yes | yes |
| Time limit | a deadline per call (epoch interruption) | 50 ms per callback (instruction hook) |
See Security, the sandbox and capabilities.
Mixing languages
A mod has one runtime, but a project can contain several mods: a C++ core for heavy logic and a Lua mod for commands and UI that lists the core in depends. They share events, registries and payload channels. Typed services between mods (.clidl) are planned (Services and mod interop).