Embedding Guide
This guide covers the concepts you need to embed mino in a host application. It picks up where Get Started leaves off and prepares you for the patterns in the Embedding Cookbook. For the full function-by-function listing, see the C API Reference.
Runtime state
Every mino session begins with a runtime state. The state owns the garbage collector, intern tables, module cache, and every object allocated within it.
mino_state_t *S = mino_state_new();Multiple states can coexist in the same process. They share nothing. This is the foundation of mino's isolation model: each state is a self-contained runtime that can be created, used, and destroyed independently.
Environments
An environment holds name-to-value bindings. The quickest start installs the sandbox preset in one call:
mino_env_t *env = mino_env_new_default(S); /* sandbox preset */Or build the env explicitly and pick the capabilities you want with a bitmask:
mino_env_t *env = mino_env_new(S);
mino_install(S, env, MINO_CAP_DEFAULT | MINO_CAP_IO);Tear down in reverse order when done:
mino_env_free(S, env);
mino_state_free(S);Capability-gated install
Capabilities are addressable as bits. Hand mino_install a bitmask and it installs exactly the subset you asked for. The runtime consults the bits at symbol-resolution time; a name from a disabled capability raises an MNS002 capability-disabled diagnostic instead of a bare unbound-symbol error.
Three named presets cover the common shapes one call away:
| Preset | Call | What you get |
|---|---|---|
| Minimal | mino_install_minimal(S, env) | Floor only — reader, evaluator, GC, persistent collections, numeric ops, foundational macros. No core.clj evaluation. Smallest cold start. |
| Sandbox | mino_install_sandbox(S, env) | Floor + multimethods, protocols, transducers, regex, bignum + the bundled clojure.* libs that carry no I/O surface. Evaluates core.clj. Excludes IO, FS, PROC, STM, AGENT, HOST, ASYNC. Equivalent to mino_install(S, env, MINO_CAP_DEFAULT). |
| Everything | mino_install_all(S, env) | Every capability and every bundled stdlib namespace the standalone binary ships with. Equivalent to mino_install(S, env, MINO_CAP_ALL). |
Pick exactly the bits you want for a custom tier:
mino_install(S, env, MINO_CAP_DEFAULT | MINO_CAP_IO | MINO_CAP_REGEX);Available bits: MINO_CAP_FLOOR, MINO_CAP_REGEX, MINO_CAP_BIGNUM, MINO_CAP_MULTIMETHODS, MINO_CAP_PROTOCOLS, MINO_CAP_TRANSDUCERS, MINO_CAP_IO, MINO_CAP_FS, MINO_CAP_PROC, MINO_CAP_STM, MINO_CAP_AGENT, MINO_CAP_HOST, MINO_CAP_ASYNC, MINO_CAP_STRING_LIB, MINO_CAP_SET_LIB, MINO_CAP_WALK, MINO_CAP_EDN, MINO_CAP_PPRINT, MINO_CAP_ZIP, MINO_CAP_DATA, MINO_CAP_TEST, MINO_CAP_REPL_LIB, MINO_CAP_DATAFY, MINO_CAP_INSTANT, MINO_CAP_SPEC, MINO_CAP_TOOLING. MINO_CAP_FLOOR is always installed implicitly. mino_install is idempotent: calling it again with additional bits adds the missing capabilities and does not re-evaluate core.clj.
Query what is installed at any time:
unsigned int caps = mino_capabilities(S);
int has_regex = mino_capability_installed(S, MINO_CAP_REGEX);
/* Iterate the registry, printing what is on / off */
for (const mino_capability_info_t *p = mino_capability_list();
p->name != NULL; p++) {
printf(" %s: %s\n", p->name,
mino_capability_installed(S, p->bit) ? "on" : "off");
}At the REPL, :capabilities (alias :caps) prints the table interactively. The banner shows (embedded, N of M capabilities installed) whenever a runtime is in a partial-install state.
Evaluating code
The simplest way to run mino code is mino_eval_string. It reads and evaluates all forms in a string and returns the value of the last one:
mino_val_t *result = mino_eval_string(S, "(+ 1 2)", env);
/* result is a mino integer with value 3 */If evaluation fails (parse error, runtime error, undefined name), the return value is NULL. The error message is available via mino_last_error(S):
mino_val_t *result = mino_eval_string(S, "(undefined-fn 1)", env);
if (result == NULL) {
fprintf(stderr, "error: %s\n", mino_last_error(S));
}Protected eval variants
When calling a function that might throw, use mino_pcall to catch the error without unwinding your C stack:
mino_val_t *out = NULL;
mino_val_t *ex = NULL;
if (mino_pcall(S, fn, args, env, &out, &ex) != 0) {
/* ex carries the raw value the user passed to (throw ...) */
}Three _ex variants extend the same shape to the eval-family entry points so embedders can distinguish "real nil result" from "caught throw" without consulting mino_last_error:
| Function | Same shape as | Use when |
|---|---|---|
mino_eval_ex | mino_eval | evaluating a parsed form |
mino_eval_string_ex | mino_eval_string | evaluating source text |
mino_load_file_ex | mino_load_file | evaluating a file |
mino_pcall | (none; original entry) | calling a known function value with prepared args |
Each _ex call returns 0 on success (writing the result through out) or -1 on a caught throw / OOM / parse failure. When out_ex is non-NULL on error, it receives the raw payload — useful for handlers that want to surface the user's ex-info unchanged. See cookbook/error_handling.c for the canonical real-nil-vs-caught-throw pattern.
Structured error access
After a call returns NULL, inspect the classified diagnostic without parsing the human-readable message:
const char *kind = mino_error_kind(S); /* e.g. "eval/type" */
const char *code = mino_error_code(S); /* e.g. "MTY001" */
const char *msg = mino_last_error(S);
/* ... handle ... */
mino_clear_error(S); /* reset for next call */Kind strings group similar failures (eval/type, eval/arity, eval/bounds, eval/contract, name, syntax, io). Codes are stable across releases so handlers can switch on them without parsing the message text.
Value ownership
This is the most important concept for correct embedding. Every value returned by mino is borrowed: it survives until the next garbage collection cycle. Allocation pressure triggers collection, so any mino call that allocates may invalidate previously returned values. In practice, use a value or extract its data promptly, and ref anything that must survive across many mino calls.
mino_val_t *v = mino_int(S, 42);
/* v is valid here */
mino_val_t *w = mino_int(S, 99);
/* v might have been collected -- do not use it */Retaining values with refs
To keep a value alive across multiple mino calls, root it with a ref:
mino_ref_t *r = mino_ref(S, val); /* root val */
/* ... any number of allocations / evals ... */
mino_val_t *v = mino_deref(r); /* get the value back */
mino_unref(S, r); /* release the root */Refs are owned by the state. Forgetting to unref is not a leak in the traditional sense (the ref is freed when the state is freed), but holding refs longer than needed prevents the collector from reclaiming objects.
Environments are roots
Any value bound in a live environment survives collection automatically. You do not need to ref values that you have bound with mino_env_set:
mino_env_set(S, env, "my-val", mino_int(S, 42));
/* The integer 42 is now rooted through env -- no ref needed */Building collections from C
When the host produces a value element-by-element — parsing incremental input, copying a C array, gathering rows out of a database row iterator — use a builder. Each builder wraps a transient and exposes an _add step plus a _persistent! finaliser. The persistent result is a mino value the embedder can return; the builder itself must not be reused after _persistent! runs.
/* Vector: positional accumulation. */
mino_vector_builder_t *vb = mino_vector_builder_new(S);
for (size_t i = 0; i < n; i++) {
mino_vector_builder_push(vb, mino_int(S, items[i]));
}
mino_val_t *v = mino_vector_builder_persistent(vb);
/* Map: insertion-ordered key-value pairs. */
mino_map_builder_t *mb = mino_map_builder_new(S);
mino_map_builder_put(mb, mino_keyword(S, "a"), mino_int(S, 1));
mino_map_builder_put(mb, mino_keyword(S, "b"), mino_int(S, 2));
mino_val_t *m = mino_map_builder_persistent(mb);
/* Set: deduplicated values. */
mino_set_builder_t *sb = mino_set_builder_new(S);
mino_set_builder_add(sb, mino_int(S, 1));
mino_set_builder_add(sb, mino_int(S, 1)); /* dropped, already present */
mino_val_t *s = mino_set_builder_persistent(sb);When every element is already sitting in a C array, the fixed-arity constructors are simpler and shorter: mino_vector(S, elems, n), mino_map(S, keys, vals, n), mino_set(S, elems, n). Builders pay off when the host is doing the accumulation loop itself, or when it does not know the length up front.
Builders root their staged values for the GC, so an allocation inside the loop will not reclaim the partial result. See cookbook/build_collections.c for a complete worked example.
Iterating collections from C
One iterator type walks every sequential and associative collection mino exposes: vectors, maps (hashed and sorted), sets, lists, the empty-list singleton, lazy seqs, and chunked seqs. The host owns the iterator storage; allocate mino_iter_sizeof() bytes (typically on the C stack via alloca) and drive it with the lifecycle:
mino_iter_t *it = alloca(mino_iter_sizeof());
mino_iter_init(S, it, coll);
mino_val_t *k, *v;
while (mino_iter_next(it, &k, &v)) {
/* vectors / sets / lists: k is the element, v is NULL.
* maps: k is the key, v is the value. */
}
mino_iter_done(it);mino_iter_init pins the collection so a GC fired mid-walk cannot reclaim the cells the iterator borrows pointers into. mino_iter_done releases that root and must always be called once, even if the walk exits early. Calling mino_iter_next after it returned 0 keeps returning 0, and mino_iter_done on a NULL iterator is harmless.
Map iteration follows insertion order for hashed maps and key order for sorted maps. See cookbook/iterate.c for a worked example that covers vectors, maps, lazy seqs, and cons lists through the same surface.
Host functions
Register C functions as mino primitives with mino_register_fn:
static mino_val_t *my_greet(mino_state_t *S, mino_val_t *args,
mino_env_t *env)
{
const char *name;
size_t len;
(void)env;
if (!mino_is_cons(args) ||
!mino_to_string(mino_car(args), &name, &len))
return mino_nil(S);
char buf[256];
snprintf(buf, sizeof(buf), "hello, %s!", name);
return mino_string(S, buf);
}
mino_register_fn(S, env, "greet", my_greet);The runtime passes the active mino_state_t *S as the first argument to every primitive callback. Use it for all value construction and API calls within the function.
Structured host interop
For richer host integration, mino provides a type-oriented capability registry. The host registers constructors, methods, static methods, and getters per type, and mino code calls them through familiar dot-syntax:
mino_host_enable(S);
/* Register a Counter type with constructor and methods */
mino_host_register_ctor(S, "Counter", 0, counter_new, NULL);
mino_host_register_method(S, "Counter", "inc", 0, counter_inc, NULL);
mino_host_register_method(S, "Counter", "get", 0, counter_get, NULL);
mino_host_register_getter(S, "Counter", "value", counter_value, NULL);mino code can then use dot-syntax:
(def c (new Counter))
(.inc c)
(.-value c) ;=> 1Interop is disabled by default. The host must call mino_host_enable(S) to activate it. Unregistered types and methods produce clear error messages. mino_register_fn remains available for simpler one-off host functions.
Handles
Handles wrap opaque host pointers so mino code can pass them around without knowing what they contain:
FILE *fp = fopen("data.txt", "r");
mino_val_t *h = mino_handle(S, fp, "file");Retrieve the pointer later with mino_handle_ptr(h) and check the type with mino_handle_tag(h).
Finalizers
Attach a cleanup function that fires when the handle is collected by the GC or when the state is freed:
void close_file(void *ptr, const char *tag) {
fclose((FILE *)ptr);
}
mino_val_t *h = mino_handle_ex(S, fp, "file", close_file);Finalizers must not call back into the mino API. They run during GC sweep when the runtime is not in a safe state for re-entry.
Sandboxing
A fresh environment created with mino_env_new has no bindings at all. mino_install_sandbox adds the canonical Clojure-core surface (map, filter, reduce, regex, bignum, the safe clojure.* libs) without granting I/O, processes, host interop, or shared-state primitives. The host controls exactly what untrusted code can do:
mino_env_t *sandbox = mino_env_new(S);
mino_install_sandbox(S, sandbox);
/* sandbox has map, filter, reduce, etc. but no slurp, spit, sh,
refs, agents, or host-interop. */
/* Grant specific capabilities */
mino_register_fn(S, sandbox, "query", my_safe_query_fn);Execution limits
Cap eval steps and heap usage to prevent runaway scripts:
mino_set_limit(S, MINO_LIMIT_STEPS, 100000);
mino_set_limit(S, MINO_LIMIT_HEAP, 8 * 1024 * 1024); /* 8 MB */When a limit is exceeded, the current eval returns NULL and mino_last_error reports the cause. Pass 0 to disable a limit.
Modules
Register a resolver to let mino code load files by name:
const char *my_resolver(const char *name, void *ctx) {
static char path[256];
snprintf(path, sizeof(path), "scripts/%s.clj", name);
return path;
}
mino_set_resolver(S, my_resolver, NULL);When mino code calls (require "utils"), the resolver maps the name to a file path. The file is loaded once; subsequent requires return the cached value.
Sessions
Multiple independent evaluation contexts can share a single state by cloning an environment:
mino_env_t *base = mino_env_new_default(S); /* sandbox preset */
mino_env_t *session1 = mino_env_clone(S, base);
mino_env_t *session2 = mino_env_clone(S, base);Each clone starts with the same bindings but evolves independently. Defining a name in one session does not affect the other. This is the building block for nREPL-style session management and multi-user environments.
Interruption
Stop a running eval from another thread:
/* From any thread: */
mino_interrupt(S);The eval loop checks the interrupt flag on every step. The running eval returns NULL with mino_last_error reporting "interrupted". The flag is cleared at the start of the next eval call.
mino_interrupt is the only mino API function safe to call from a different thread.
The REPL handle
The in-process REPL lets a host drive read-eval-print one line at a time without managing a read buffer:
mino_repl_t *repl = mino_repl_new(S, env);
mino_val_t *result;
int rc = mino_repl_feed(repl, "(+ 1 2)", &result);
switch (rc) {
case MINO_REPL_OK: /* result is ready */ break;
case MINO_REPL_MORE: /* need more input */ break;
case MINO_REPL_ERROR: /* see mino_last_error */ break;
}
mino_repl_free(repl);This is useful for building interactive consoles, debuggers, and live inspection tools inside running applications.
Threading rules
A mino_state_t is not thread-safe. The host must not call into a state from multiple threads at the same time. Different states can be used from different threads simultaneously since they share nothing: each state owns its own GC heap, scheduler, intern tables, module cache, PRNG, and error reporting buffer. No mutable process-global state lives inside the runtime.
The one call safe to make from a non-owning thread is mino_interrupt(S). It only writes a volatile flag that the owning thread observes at the next eval step.
Running many states across threads
Host a fleet of isolated runtimes by giving each OS thread its own state. The orchestrator lives in the host application; mino itself starts no threads and holds no shared data.
/* One pthread per state. */
void *worker(void *arg) {
mino_state_t *S = mino_state_new();
mino_env_t *env = mino_env_new_default(S);
mino_load_file(S, "bot.clj", env);
mino_state_free(S);
return NULL;
}Because the two intern tables and the PRNG live on the state, two workers running at the same instant never touch a shared memory location inside the runtime.
Cross-state values
To move values between states (which may live on different threads), use mino_clone:
mino_val_t *copy = mino_clone(dst_state, src_state, val);Only data values (numbers, strings, collections) can cross state boundaries. Functions, environments, atoms, and handles are not transferable. Clone is safe only when both states are quiescent on their owning threads: call from the thread that owns the destination, after synchronising with the thread that owns the source.
Delivering messages from a non-owning thread
For ongoing cross-thread delivery (a network thread pushing messages into a mino worker, for example), let the host own a lock-free or mutex-protected queue per state. The owning thread drains the queue from inside mino code by calling a host-registered primitive that translates messages into mino values and hands them to a channel:
/* Per-thread pointer: each thread runs exactly one state. */
static _Thread_local host_inbox_t *current_inbox;
static mino_val_t *prim_inbox_drain(mino_state_t *S,
mino_val_t *args,
mino_env_t *env) {
host_msg_t m;
(void)args; (void)env;
while (host_inbox_try_pop(current_inbox, &m)) {
mino_val_t *v = host_msg_to_mino_val(S, &m);
/* push v onto a channel held on the mino side */
(void)v;
}
return mino_nil(S);
}
mino_register_fn(S, env, "host-inbox-drain", prim_inbox_drain);The contract is unchanged: the mino side is touched only on the owning thread. The host's inbox is a thread-safe handoff surface sitting outside the runtime.
Resolvers must be reentrant
The CLI binary ships a CWD-relative module resolver that uses file-scope static buffers. That resolver is not safe for use from multiple threads. Hosts should register their own resolver via mino_set_resolver and, in a multi-threaded context, write results into a caller-provided or per-state buffer rather than a function-scope static.
Garbage collection
The collector is a non-moving generational tracing collector with an incremental old-gen mark phase. Short-lived allocations live in a young-generation nursery; values that survive a minor collection are promoted to old-gen, which is marked in paced slices between mutator allocations. A write barrier tracks old-to-young pointers so minor collections stay proportional to young reachability. Any mino function that allocates may advance the collector, which is why borrowed values can become invalid after the next call.
Objects survive collection if they are reachable from a root: registered environments, host refs, the intern tables, the module cache, or the C stack (via conservative scanning).
See the Garbage Collection reference for collector phases, tuning knobs with accepted ranges, the full (gc-stats) field list, and the MINO_GC_* environment variables.
Next steps
- Embedding Cookbook: six worked examples showing patterns for configuration, rules engines, plugins, data pipelines, and interactive consoles.
- C API Reference: every public function, type, and enum in
mino.h. - About: design philosophy, trade-offs, and related projects.