Skip to content

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).