Runtime Lifecycle
The runtime is the central object in Mikro.js. It wraps a QuickJS JSRuntime and JSContext, owns all timers and registered modules, and provides the event loop that drives JavaScript execution.
MIKRuntime
MIKRuntime (defined in private.h) holds all runtime state. Key fields (simplified; see private.h for the full definition):
struct MIKRuntime {
MIKRunOptions options;
MIKConfig config;
JSRuntime* rt; // QuickJS runtime
JSContext* ctx; // QuickJS context
bool freeing;
bool stop_requested;
const char* fs_base_path; // Module resolution root
const char* fs_root; // Sandbox root for fs operations
size_t fs_limit; // Max bytes for fs_root (0 = unlimited)
MIKTimerRegistry* timers; // Timer scheduling
// Module registries
std::vector<MIKLoopConsumerEntry> loop_consumers;
std::unordered_map<std::string, std::string> virtual_modules;
// Built-in JS references (PromiseRejectionEvent, dispatchEvent)
struct { JSValue promise_event_ctor; JSValue dispatch_event_func; } builtins;
JSValue env_obj; // Frozen import.meta.env
std::vector<std::pair<std::string, std::string>> env_vars;
// Per-module state (16 slots)
void* module_data[MIK_MODULE_DATA_SLOTS];
// Optional hooks (used by Node.js addon)
char* (*preprocess_fn)(...); // Module source preprocessor
void (*error_handler_fn)(...); // Error handler callback
};Creation
Two entry points create a runtime:
MIKRuntime* MIK_NewRuntime(void);
MIKRuntime* MIK_NewRuntimeOptions(MIKRunOptions* options);MIK_NewRuntimeOptions accepts memory limit and stack size. Both call MIK_NewRuntimeInternal() which initializes everything in this order:
- Creates a
JSRuntimewith custom malloc functions for memory tracking - Creates a
JSContext, seeded with the platform's hardware RNG forMath.random() - Sets memory limit and stack size
- Registers the module loader and promise rejection tracker
- Registers core C modules (
native:sys,native:cbor,native:fs, inspect) - Calls init functions for any pre-registered native modules
- Registers
native:stdio(after native modules, so stdin state is ready) - Initializes web standard globals (
TextEncoder,AbortController) - Initializes timers (
setTimeout,setInterval, and their clear functions) - Initializes the
consoleglobal - Builds and freezes
import.meta.env
TIP
MIK_SetPlatform() must be called before creating a runtime. The platform provides the timer clock, RNG seed, and I/O functions used during initialization.
Configuration
Runtime behavior is controlled by MIKConfig:
typedef struct MIKConfig {
int panic_restart_delay_ms; // grace window before the panic action
MIKPanicMode panic_mode; // MIK_PANIC_RESTART | MIK_PANIC_DEEP_SLEEP
int panic_sleep_duration_ms; // deep-sleep length (deep-sleep mode only)
size_t stack_size;
uint32_t mem_reserved;
uint32_t fs_read_max; // 0 = keep runtime default (65536)
char entry_point[128];
char wifi_country[3];
} MIKConfig;Configuration is loaded from the filesystem via MIK_LoadConfig() and applied with MIK_SetConfig(). On ESP32, MIK_Main() handles this automatically.
Environment variables
Environment variables populate import.meta.env in JavaScript:
MIK_SetEnvVar(mik_rt, "WIFI_SSID", "MyNetwork");
MIK_SetEnvVar(mik_rt, "WIFI_PASS", "secret");
MIK_RebuildEnv(mik_rt); // Rebuild the frozen env objectOn ESP32, MIK_LoadEnvFromNVS() reads all NVS entries and calls MIK_SetEnvVar() for each one. The env object is frozen to prevent mutation from JavaScript.
Running the loop
MIK_Loop() executes a single iteration of the event loop. The caller is responsible for calling it repeatedly:
while (MIK_Loop(mik_rt) == 0) {
MIK_GetPlatform()->yield();
}A return value of 0 means "keep going." A non-zero return means the runtime must stop (either an unhandled exception or a stop request). See Event Loop for what happens inside each iteration.
Stopping
The runtime can be stopped in several ways:
- Unhandled promise rejection: The rejection tracker sets
stop_requestedand returns1fromMIK_Loop() - Uncaught exception: Detected at the top of each loop iteration
- Explicit stop:
MIK_Stop(mik_rt)setsstop_requested
After an uncaught exception, MIK_Stop() records a deadline panic_restart_delay_ms in the future on MIKRuntime.restart_at_us. MIK_Loop() keeps the protocol REPL pumping (no more user JS) until the deadline elapses, then takes the configured panic action: platform->restart() (default), or platform->deep_sleep_us() for panic_sleep_duration_ms when panic_mode is MIK_PANIC_DEEP_SLEEP (the timer wake reboots the chip). The host can land deploy / clean / --recover commands during the grace window; deep-sleep forfeits that window once asleep.
Destruction
MIK_FreeRuntime() tears down the runtime in a specific order:
- Sets
freeing = trueto prevent new callbacks from firing - Destroys all loop consumers (calls each consumer's
destroy_fn) - Clears all timers and frees their stored JS values
- Frees internal JS values (
env_obj, event dispatch functions) - Frees the
JSContextandJSRuntime
The freeing flag is checked before delivering promise rejection events or calling error handlers, preventing re-entrant cleanup issues.
Module data slots
Stateful modules need to store persistent data on the runtime (for example WiFi connection state). Sixteen slots are available:
static int my_slot = -1;
if (my_slot < 0) my_slot = MIK_ReserveModuleSlot();
mik_rt->module_data[my_slot] = my_state;
// Later, in a callback:
MyState* state = (MyState*)mik_rt->module_data[my_slot];Reserve the slot once and cache it. Indices are handed out process-wide and never freed, so a slot identifies one module in every runtime, and a module that runs in several runtimes keeps the same index in all of them.
Do not reserve per runtime. A runtime-local index would restart from zero in the next runtime, so a module that cached its index would end up reading whichever module claimed that index there, through the wrong type.