# Duktape
Duktape JavaScript engine for Erlang.
This library embeds the [Duktape](https://duktape.org/) JavaScript engine (v2.7.0) as an Erlang NIF, allowing you to evaluate JavaScript code directly from Erlang.
## Features
- Execute JavaScript code from Erlang
- Bidirectional type conversion between Erlang and JavaScript
- Multiple isolated JavaScript contexts
- CommonJS module support
- Thread-safe with automatic resource cleanup
- No external dependencies - Duktape is embedded
## Requirements
- Erlang/OTP 24 or later
- CMake 3.10 or later
- C compiler (gcc, clang, or MSVC)
## Installation
Add to your `rebar.config`:
```erlang
{deps, [
{duktape, {git, "https://github.com/benoitc/erlang-duktape.git", {branch, "main"}}}
]}.
```
Then run:
```bash
rebar3 compile
```
## Quick Start
```erlang
%% Create a JavaScript context
{ok, Ctx} = duktape:new_context().
%% Evaluate JavaScript code
{ok, 42} = duktape:eval(Ctx, <<"21 * 2">>).
{ok, <<"hello">>} = duktape:eval(Ctx, <<"'hello'">>).
%% Evaluate with variable bindings
{ok, 30} = duktape:eval(Ctx, <<"x * y">>, #{x => 5, y => 6}).
%% Define and call functions
{ok, _} = duktape:eval(Ctx, <<"function add(a, b) { return a + b; }">>).
{ok, 7} = duktape:call(Ctx, add, [3, 4]).
%% CommonJS modules
ok = duktape:register_module(Ctx, <<"utils">>, <<"
exports.greet = function(name) {
return 'Hello, ' + name + '!';
};
">>).
{ok, <<"Hello, World!">>} = duktape:eval(Ctx, <<"require('utils').greet('World')">>).
```
## API Reference
### Context Management
#### `new_context() -> {ok, context()} | {error, term()}`
Create a new JavaScript context. Contexts are isolated - variables and functions defined in one context are not visible in others.
Contexts are automatically cleaned up when garbage collected, but you can also explicitly destroy them with `destroy_context/1`.
#### `destroy_context(Ctx) -> ok | {error, term()}`
Explicitly destroy a JavaScript context. This is optional - contexts are automatically cleaned up on garbage collection. Calling destroy on an already-destroyed context is safe (idempotent).
### Evaluation
#### `eval(Ctx, Code) -> {ok, Value} | {error, term()}`
Evaluate JavaScript code and return the result of the last expression.
```erlang
{ok, 3} = duktape:eval(Ctx, <<"1 + 2">>).
{ok, <<"hello">>} = duktape:eval(Ctx, <<"'hello'">>).
{error, {js_error, _}} = duktape:eval(Ctx, <<"throw 'oops'">>).
```
#### `eval(Ctx, Code, Bindings) -> {ok, Value} | {error, term()}`
Evaluate JavaScript code with variable bindings. Bindings are set as global variables before evaluation.
```erlang
{ok, 30} = duktape:eval(Ctx, <<"x * y">>, #{x => 5, y => 6}).
{ok, <<"hello world">>} = duktape:eval(Ctx, <<"greeting + ' ' + name">>,
#{greeting => <<"hello">>, name => <<"world">>}).
```
### Function Calls
#### `call(Ctx, FunctionName) -> {ok, Value} | {error, term()}`
Call a global JavaScript function with no arguments.
```erlang
{ok, _} = duktape:eval(Ctx, <<"function getTime() { return Date.now(); }">>).
{ok, Timestamp} = duktape:call(Ctx, <<"getTime">>).
```
#### `call(Ctx, FunctionName, Args) -> {ok, Value} | {error, term()}`
Call a global JavaScript function with arguments. Function names can be binaries or atoms.
```erlang
{ok, _} = duktape:eval(Ctx, <<"function add(a, b) { return a + b; }">>).
{ok, 7} = duktape:call(Ctx, <<"add">>, [3, 4]).
{ok, 7} = duktape:call(Ctx, add, [3, 4]).
```
### CommonJS Modules
#### `register_module(Ctx, ModuleId, Source) -> ok | {error, term()}`
Register a CommonJS module with source code. The module can then be loaded with `require/2` or via `require()` in JavaScript.
```erlang
ok = duktape:register_module(Ctx, <<"math">>, <<"
exports.add = function(a, b) { return a + b; };
exports.multiply = function(a, b) { return a * b; };
">>).
```
#### `require(Ctx, ModuleId) -> {ok, Exports} | {error, term()}`
Load a CommonJS module and return its exports. Modules are cached - subsequent requires return the same exports object.
```erlang
{ok, Exports} = duktape:require(Ctx, <<"math">>).
```
### Utility
#### `info() -> {ok, string()}`
Get NIF information. Used to verify the NIF is loaded correctly.
## Type Conversions
### Erlang to JavaScript
| Erlang | JavaScript |
|--------|------------|
| `integer()` | number |
| `float()` | number |
| `binary()` | string |
| `true` | true |
| `false` | false |
| `null` | null |
| `undefined` | undefined |
| other atoms | string |
| `list()` | array (or string if iolist) |
| `map()` | object |
| `tuple()` | array |
### JavaScript to Erlang
| JavaScript | Erlang |
|------------|--------|
| number (integer) | `integer()` |
| number (float) | `float()` |
| NaN | `nan` (atom) |
| Infinity | `infinity` (atom) |
| -Infinity | `neg_infinity` (atom) |
| string | `binary()` |
| true | `true` |
| false | `false` |
| null | `null` |
| undefined | `undefined` |
| array | `list()` |
| object | `map()` |
## Error Handling
JavaScript errors are returned as `{error, {js_error, Message}}` where `Message` is a binary containing the error message and stack trace.
```erlang
{error, {js_error, <<"ReferenceError: x is not defined", _/binary>>}} =
duktape:eval(Ctx, <<"x + 1">>).
```
Contexts remain usable after errors - you can continue to evaluate code in the same context.
## Thread Safety
All context operations are thread-safe. Multiple Erlang processes can share a context, though operations are serialized via a mutex. For maximum parallelism, create separate contexts for concurrent workloads.
## Resource Management
Contexts are managed as Erlang NIF resources with automatic cleanup:
- Contexts are garbage collected when no Erlang process holds a reference
- Multiple processes can share a context safely
- Explicit `destroy_context/1` is optional but can be used for immediate cleanup
- Reference counting ensures contexts are not destroyed while in use
## Benchmarks
Performance benchmarks on Apple M4 Pro, Erlang/OTP 28:
| Benchmark | Ops/sec | Mean (ms) | P95 (ms) | P99 (ms) |
|-----------|--------:|----------:|---------:|---------:|
| eval_simple | 2,633 | 0.380 | 0.418 | 0.458 |
| eval_complex | 2,355 | 0.425 | 0.469 | 0.513 |
| eval_bindings_small (5 vars) | 2,632 | 0.380 | 0.418 | 0.457 |
| eval_bindings_large (50 vars) | 1,688 | 0.593 | 0.715 | 0.773 |
| call_no_args | 2,513 | 0.398 | 0.488 | 0.532 |
| call_with_args (5 args) | 2,478 | 0.404 | 0.492 | 0.533 |
| call_many_args (20 args) | 2,176 | 0.460 | 0.540 | 0.604 |
| type_convert_simple | 2,548 | 0.392 | 0.461 | 0.524 |
| type_convert_array (1000 elem) | 2,121 | 0.472 | 0.555 | 0.594 |
| type_convert_nested | 2,489 | 0.402 | 0.485 | 0.520 |
| context_create | 2,702 | 0.370 | 0.430 | 0.485 |
| module_require_cached | 2,230 | 0.448 | 0.525 | 0.577 |
| concurrent_same_context | 120,617 | 0.829 | 1.070 | 1.199 |
| concurrent_many_contexts | 33,276 | 3.005 | 3.380 | 3.873 |
Run benchmarks yourself:
```bash
./run_bench.sh # Run all benchmarks
./run_bench.sh eval_simple # Run specific benchmark
./run_bench.sh --smoke # Quick validation
```
## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
Duktape is also licensed under the MIT License.