Embed a JavaScript engine inside your Elixir process using MQuickJS.
MquickjsEx provides an ergonomic interface to MQuickJS, a minimal JavaScript engine targeting embedded systems. It runs JavaScript code in as little as 10KB of RAM while enabling bidirectional function calls between Elixir and JavaScript.
No external runtime required. Unlike solutions that shell out to Node.js or Bun, MquickjsEx embeds JavaScript execution directly in your Elixir process via NIFs. No separate runtime to install, no process spawning, no IPC overhead.
Perfect for LLM tool calling. When an LLM generates JavaScript code (for data transformation, calculations, or custom logic), MquickjsEx can execute it safely in a sandboxed environment with controlled access to your Elixir functions. The LLM writes JavaScript; you control what it can actually do.
# LLM generates this code
js_code = """
var data = fetch_records("users");
var active = data.filter(function(u) { return u.status === "active"; });
save_result(active.length);
"""
# You control what functions are available
ctx = MquickjsEx.new!()
|> MquickjsEx.set!(:fetch_records, fn [table] -> MyApp.Repo.all(table) end)
|> MquickjsEx.set!(:save_result, fn [val] -> send(self(), {:result, val}) end)
MquickjsEx.eval!(ctx, js_code)- No dependencies - No Node.js, Bun, or Deno installation required
- In-process - Runs in the BEAM, no subprocess spawning or IPC
- Lightweight - MQuickJS runs in a fixed memory buffer (default 64KB)
- Safe - JavaScript runs in a sandboxed environment with no file system or network access
- Bidirectional - Call JavaScript from Elixir and Elixir from JavaScript
- Type conversion - Automatic conversion between Elixir and JavaScript types
- API modules - Define reusable function sets with the
MquickjsEx.APIbehaviour - Private storage - Store Elixir data associated with a context without exposing it to JavaScript
Add mquickjs_ex to your list of dependencies in mix.exs:
def deps do
[
{:mquickjs_ex, "~> 0.1.0"}
]
end# Create a new JavaScript context
{:ok, ctx} = MquickjsEx.new()
# Evaluate JavaScript code
{:ok, result} = MquickjsEx.eval(ctx, "1 + 2")
# => {:ok, 3}
# Set values in JavaScript
ctx = MquickjsEx.set!(ctx, :message, "Hello from Elixir")
{:ok, msg} = MquickjsEx.eval(ctx, "message")
# => {:ok, "Hello from Elixir"}
# Call Elixir functions from JavaScript
ctx = MquickjsEx.set!(ctx, :add, fn [a, b] -> a + b end)
{result, _ctx} = MquickjsEx.eval!(ctx, "add(40, 2)")
# => 42| Elixir | JavaScript | Notes |
|---|---|---|
nil |
null |
|
true / false |
true / false |
|
| integers | number | 31-bit signed integers |
| floats | number | 64-bit floating point |
| binaries | string | UTF-8 encoded |
| atoms | string | Converted via to_string/1 |
| lists | Array | |
| maps | Object | |
| functions | callable | Via trampoline (see below) |
When you register an Elixir function with set/3, it becomes callable from JavaScript. Under the hood, this uses a trampoline pattern with re-execution:
- JavaScript code calls a registered function (e.g.,
add(1, 2)) - The call throws a special
__yield__exception, halting JavaScript execution - Control returns to Elixir, which executes the callback with the provided arguments
- The result is cached and JavaScript code is re-executed from the beginning
- On replay, the cached result is returned instead of yielding
- This repeats until all callbacks complete and JavaScript finishes
Run 1: JS executes → calls add(1,2) → yields to Elixir → Elixir computes 3
Run 2: JS executes → add(1,2) returns 3 (cached) → JS completes
For multiple callback calls, each run replays from the start with accumulated cached results:
Run 1: code → fetch("a") → yields [cache: ]
Run 2: code → fetch("a")=cached → fetch("b") → yields [cache: result_a]
Run 3: code → fetch("a")=cached → fetch("b")=cached → completes [cache: result_a, result_b]
- Idempotent code: JavaScript code should be idempotent (no side effects that accumulate on replay)
- Performance: Multiple callbacks mean multiple re-executions; keep callback-heavy code efficient
- Determinism: Code must execute the same way each time to hit cached results in order
For reusable function sets, use the MquickjsEx.API behaviour:
defmodule MathAPI do
use MquickjsEx.API, scope: "math"
defjs add(a, b), do: a + b
defjs multiply(a, b), do: a * b
end
{:ok, ctx} = MquickjsEx.new()
{:ok, ctx} = MquickjsEx.load_api(ctx, MathAPI)
{:ok, 5} = MquickjsEx.eval(ctx, "math.add(2, 3)")
{:ok, 6} = MquickjsEx.eval(ctx, "math.multiply(2, 3)")use MquickjsEx.API, scope: "utils.math"
# Functions available as: utils.math.add(1, 2)Use the three-argument form of defjs to access or modify the JavaScript context:
defmodule ConfigAPI do
use MquickjsEx.API, scope: "config"
# Read-only access to state
defjs get(key), state do
MquickjsEx.get!(state, key)
end
# Modify state by returning {result, new_state}
defjs set(key, value), state do
new_state = MquickjsEx.set!(state, key, value)
{nil, new_state}
end
endFor functions accepting any number of arguments:
@variadic true
defjs sum(args), do: Enum.sum(args)
# Called as: sum(1, 2, 3, 4, 5) => 15Run setup code when the API is loaded:
@impl MquickjsEx.API
def install(ctx, _scope, _data) do
MquickjsEx.set!(ctx, :api_loaded, true)
end
# Or return JavaScript code to evaluate:
@impl MquickjsEx.API
def install(_ctx, _scope, _data) do
"var API_VERSION = 1;"
endStore Elixir data associated with a context without exposing it to JavaScript:
{:ok, ctx} = MquickjsEx.new()
ctx = MquickjsEx.put_private(ctx, :user_id, 123)
ctx = MquickjsEx.put_private(ctx, :session, %{role: :admin})
{:ok, 123} = MquickjsEx.get_private(ctx, :user_id)
123 = MquickjsEx.get_private!(ctx, :user_id)
ctx = MquickjsEx.delete_private(ctx, :user_id)
:error = MquickjsEx.get_private(ctx, :user_id)Private storage is useful for passing context to API callbacks:
defmodule UserAPI do
use MquickjsEx.API, scope: "user"
defjs current_id(), state do
MquickjsEx.get_private!(state, :user_id)
end
endConfigure the JavaScript heap size when creating a context:
# Default: 64KB
{:ok, ctx} = MquickjsEx.new()
# Custom size: 128KB
{:ok, ctx} = MquickjsEx.new(memory: 131072)
# Minimal: 10KB (MQuickJS can run in as little as 10KB!)
{:ok, ctx} = MquickjsEx.new(memory: 10240)MQuickJS implements a subset of JavaScript close to ES5 in a stricter mode:
- No
withkeyword - Global variables must be declared with
var
- Arrays cannot have holes:
a[10] = 1throwsTypeErrorifa.length < 10 - Array literals with holes are syntax errors:
[1, , 3]is invalid - Use objects for sparse array-like structures
eval('1 + 2'); // Forbidden
(1, eval)('1 + 2'); // OK (indirect/global eval)new Number(1), new String("x"), new Boolean(true) are not supported.
Date: OnlyDate.now()is supportedString.toLowerCase()/toUpperCase(): ASCII onlyRegExp: Case folding is ASCII only; matching is unicode-only
for...of(arrays only, no custom iterators)- Typed arrays
\u{hex}unicode escapes in strings- Math:
imul,clz32,fround,trunc,log2,log10 - Exponentiation operator (
**) - RegExp:
s,y,uflags - String:
codePointAt,replaceAll,trimStart,trimEnd globalThis
For the complete reference, see the MQuickJS documentation.
| Function | Description |
|---|---|
new/1 |
Create a new JavaScript context |
eval/2 |
Evaluate JavaScript code, returns {:ok, result} or {:error, reason} |
eval!/2 |
Evaluate JavaScript code, raises on error, returns {result, ctx} |
get/2 |
Get a global variable |
get!/2 |
Get a global variable, raises on error |
set/3 |
Set a global variable or function |
set!/3 |
Set a global variable or function, raises on error |
gc/1 |
Trigger garbage collection |
| Function | Description |
|---|---|
load_api/3 |
Load an API module, returns {:ok, ctx} or {:error, reason} |
load_api!/3 |
Load an API module, raises on error, returns ctx |
| Function | Description |
|---|---|
put_private/3 |
Store a key-value pair in private storage |
get_private/2 |
Retrieve a value, returns {:ok, value} or :error |
get_private!/2 |
Retrieve a value, raises if not found |
delete_private/2 |
Remove a key from private storage |
This project builds on MQuickJS by Fabrice Bellard and Charlie Gordon - a remarkable minimal JavaScript engine that makes embedding JS in resource-constrained environments possible.
Inspiration:
- tv-labs/lua - Ergonomic Elixir API for Luerl; influenced our public API design
- livebook-dev/pythonx - Demonstrated embedding another language runtime directly in the BEAM
MIT
This library includes vendored code from MQuickJS (Micro QuickJS JavaScript Engine) by Fabrice Bellard and Charlie Gordon, licensed under the MIT License. See c_src/vendor/LICENSE for details.