qjs
QuickJS bindings for Dart.
Quick Start
import 'package:qjs/qjs.dart';
void main() {
final rt = JSRuntime.create();
// Evaluate expressions
final result = rt.evaluateJavascript('2 + 2');
print(result.asNumber); // 4.0
// Pass variables via the global object
rt.global['name'] = JSValue.string(rt, 'Dart');
final greeting = rt.evaluateJavascript(r'`Hello, ${name}!`');
print(greeting.asDartString); // Hello, Dart!
// Call JavaScript functions
final add = rt.evaluateJavascript('(a, b) => a + b').asObject.asFunction;
final sum = add([JSValue.number(rt, 10), JSValue.number(rt, 25)]);
print(sum.asNumber); // 35.0
rt.release();
}
Runtime & Configuration
JSRuntime
JSRuntime manages an isolated JavaScript execution context and its native memory.
// Default configuration
final rt = JSRuntime.create();
// Custom configuration (e.g. sandbox memory limit of 64 MB and 512 KB stack)
final customRt = JSRuntime.create(const JSRuntimeConfig(
memoryLimit: 64 * 1024 * 1024,
maxStackSize: 512 * 1024,
microtaskQueue: true,
));
// Hardened sandbox profile (disables eval/Proxy, limits memory to 64 MB)
final sandboxRt = JSRuntime.create(const JSRuntimeConfig.hardened());
customRt.release();
sandboxRt.release();
JSRuntimeConfig
| Option | Type | Default | Description |
|---|---|---|---|
enableEval |
bool |
true |
Allows dynamic code evaluation via eval() and new Function() |
es6Proxy |
bool |
true |
Enables ES6 Proxy support in global scope |
microtaskQueue |
bool |
false |
Manages an explicit microtask queue (for Promise.then resolution) |
memoryLimit |
int |
0 |
Maximum heap memory in bytes allocated by runtime (0 = unlimited) |
maxStackSize |
int |
0 |
Maximum system stack size in bytes allocated for execution (0 = default) |
gcThreshold |
int |
0 |
Memory allocation threshold in bytes before automatic GC triggers (0 = default) |
JSRuntimeConfig.hardened() disables eval and Proxy, and restricts memoryLimit to 64 MB and maxStackSize to 512 KB.
Code Evaluation
evaluateJavascript
Executes JavaScript source code. Provide sourceUrl to get meaningful stack traces when exceptions occur:
final result = rt.evaluateJavascript(
'''
function fib(n) {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
}
fib(10);
''',
sourceUrl: 'math.js',
);
print(result.asNumber); // 55.0
Prepared JavaScript (Bytecode Preparation)
Pre-compiles source code into bytecode to execute repeatedly without re-parsing overhead:
final script = rt.prepareJavaScript(
'globalThis.counter = (globalThis.counter || 0) + 1;',
sourceUrl: 'counter.js',
);
rt.evaluatePreparedJavaScript(script);
rt.evaluatePreparedJavaScript(script);
print(rt.global['counter'].asNumber); // 2.0
// Explicitly free compiled bytecode
script.release();
JavaScript Values
JSValue is a handle to a native JavaScript value.
Type Checks and Conversions
| Type Check | Getter | Returns |
|---|---|---|
val.isNumber |
val.asNumber |
double |
val.isString |
val.asDartString |
String |
val.isBoolean |
val.asBoolean |
bool |
val.isNull |
— | val.isNull == true |
val.isUndefined |
— | val.isUndefined == true |
val.isObject |
val.asObject |
JSObject |
val.isArray |
val.asArray |
JSArray |
val.isArrayBuffer |
val.asArrayBuffer |
JSArrayBuffer |
val.isBigInt |
val.asBigInt |
JSBigInt |
val.isSymbol |
val.asSymbol |
JSSymbol |
val.asObject.isFunction |
val.asObject.asFunction |
JSFunction |
final numVal = JSValue.number(rt, 42);
final strVal = JSValue.string(rt, 'foo');
final boolVal = JSValue.boolean(rt, true);
final nullVal = JSValue.null_(rt);
final undefVal = JSValue.undefined(rt);
// Strict equality (===)
print(numVal.strictEquals(JSValue.number(rt, 42))); // true
print(numVal.strictEquals(strVal)); // false
Objects & Properties
JSObject
Create and manipulate JavaScript objects:
final user = JSObject.create(rt);
// Index operator read/write
user['id'] = JSValue.number(rt, 1001);
user['name'] = JSValue.string(rt, 'Alice');
// Check properties
if (user.hasProperty('name')) {
print(user['name'].asDartString); // Alice
}
// Enumerate property names
final names = user.getPropertyNames(); // JSArray
for (var i = 0; i < names.length; i++) {
print(names[i].asDartString);
}
Property Descriptors & Accessors
final obj = JSObject.create(rt);
// Read-only property
obj.defineProperty(
'version',
value: JSValue.string(rt, '1.0.0'),
writable: false,
enumerable: true,
configurable: false,
);
// Getter & Setter backed by Dart
var internalState = 0;
obj.defineProperty(
'count',
get: JSFunction.createFromHostFunction(rt, (rt, thisValue, args) {
return JSValue.number(rt, internalState.toDouble());
}),
set: JSFunction.createFromHostFunction(rt, (rt, thisValue, args) {
internalState = args[0].asNumber.toInt();
return JSValue.undefined(rt);
}),
);
Object Freezing & Integrity
final config = JSObject.create(rt)..['env'] = JSValue.string(rt, 'production');
config.preventExtensions(); // Disallow adding new properties
config.seal(); // Disallow adding/deleting properties
config.freeze(); // Make completely immutable
Custom Host Objects
Implement JSHostObject to handle property accesses dynamically in Dart:
class StorageHostObject extends JSHostObject {
final Map<String, JSValue> _storage = {};
@override
JSValue get(JSRuntime rt, JSPropNameId name) {
return _storage[name.string] ?? JSValue.undefined(rt);
}
@override
void set(JSRuntime rt, JSPropNameId name, JSValue value) {
_storage[name.string] = value.retain();
}
@override
List<JSPropNameId> getPropertyNames(JSRuntime rt) {
return _storage.keys.map((k) => JSPropNameId.fromString(rt, k)).toList();
}
}
final hostObj = JSObject.createFromHostObject(rt, StorageHostObject());
rt.global['storage'] = hostObj.asValue;
Arrays & Buffers
JSArray
final array = JSArray.create(rt)
..add(JSValue.number(rt, 10))
..add(JSValue.number(rt, 20));
print(array.length); // 2
print(array[0].asNumber); // 10.0
// Zero-copy live Dart List view (modifications sync both ways)
final List<JSValue> list = array.asList;
list.add(JSValue.number(rt, 30));
print(array.length); // 3
JSArrayBuffer
JSArrayBuffer provides direct, zero-copy access to binary memory via Uint8List:
// Create a new 1 KB buffer
final buffer = JSArrayBuffer.create(rt, 1024);
buffer.data[0] = 0xAA;
buffer.data[1] = 0xBB;
// Create from existing Uint8List
final data = Uint8List.fromList([1, 2, 3, 4]);
final fromBytes = JSArrayBuffer.fromBytes(rt, data);
// Zero-copy read/write from JavaScript
rt.global['buf'] = buffer.asValue;
rt.evaluateJavascript('''
const view = new Uint8Array(buf);
view[2] = 0xCC;
''');
print(buffer.data[2]); // 204 (0xCC)
Functions & Host Interop
Calling JavaScript Functions from Dart
final multiply = rt.evaluateJavascript('(a, b) => a * b').asObject.asFunction;
// Direct invocation
final product = multiply([
JSValue.number(rt, 6),
JSValue.number(rt, 7),
]);
print(product.asNumber); // 42.0
// Constructor invocation (new Point(x, y))
final pointCtor = rt.evaluateJavascript(
'function Point(x, y) { this.x = x; this.y = y; } Point;'
).asObject.asFunction;
final point = pointCtor.callAsConstructor([
JSValue.number(rt, 5),
JSValue.number(rt, 10),
]).asObject;
print(point['x'].asNumber); // 5.0
Registering Dart Functions in JavaScript
final hostLog = JSFunction.createFromHostFunction(
rt,
(rt, thisValue, args) {
final message = args.map((a) => a.isString ? a.asDartString : a.asNumber).join(' ');
print('[Host] $message');
return JSValue.undefined(rt);
},
name: 'nativeLog',
length: 1,
);
rt.global['nativeLog'] = hostLog.asValue;
rt.evaluateJavascript('nativeLog("Item count:", 42);'); // [Host] Item count: 42
Async & Promises
JSPromise.fromAsyncFunction
Bridge Dart Future calls directly into JavaScript Promise:
final fetchData = JSFunction.createFromHostFunction(rt, (rt, thisValue, args) {
return JSPromise.fromAsyncFunction(rt, () async {
// Perform async Dart operation
await Future<void>.delayed(const Duration(milliseconds: 100));
return JSValue.string(rt, 'Response payload');
});
});
rt.global['fetchData'] = fetchData.asValue;
rt.evaluateJavascript('''
async function run() {
const data = await fetchData();
globalThis.result = data;
}
run();
''');
JSPromise.withResolvers
Creates a promise along with separate resolve and reject functions (Promise.withResolvers()):
final (promise, resolve, reject) = JSPromise.withResolvers(rt);
rt.global['pendingPromise'] = promise;
// Resolve manually from Dart
resolve([JSValue.string(rt, 'Resolved from Dart!')]);
Microtask Queue
When microtaskQueue: true is configured, drain pending promises explicitly:
final rt = JSRuntime.create(const JSRuntimeConfig(microtaskQueue: true));
rt.evaluateJavascript('''
Promise.resolve().then(() => { globalThis.resolved = true; });
''');
// Process pending microtasks
rt.drainMicrotasks();
print(rt.global['resolved'].asBoolean); // true
rt.release();
Extensions
Console Logging (JSConsole)
Bridges JavaScript console.log, info, warn, error, and debug to package:logging:
import 'package:logging/logging.dart';
import 'package:qjs/qjs.dart';
final logger = Logger('QuickJS');
logger.onRecord.listen((rec) => print('[${rec.level.name}] ${rec.message}'));
final rt = JSRuntime.create();
JSConsole.install(rt, logger: logger);
rt.evaluateJavascript('console.warn("Watch out:", 404);');
// Output: [WARNING] Watch out: 404
Timers (JSTimers)
Installs standard timer APIs (setTimeout, setInterval, clearTimeout, clearInterval, setImmediate):
final rt = JSRuntime.create();
final timers = JSTimers.install(rt);
rt.evaluateJavascript('''
setTimeout(() => {
globalThis.executed = true;
}, 50);
''');
// Await until all scheduled timers resolve
await timers.resolved;
print(rt.global['executed'].asBoolean); // true
timers.release();
rt.release();
Error Handling
Catching Errors from JavaScript
When JavaScript code throws, Dart receives a JSException:
try {
rt.evaluateJavascript('throw new TypeError("Invalid configuration");');
} on JSException catch (e) {
print('Caught JS error: ${e.message}');
print('Stack trace:\n${e.stack}');
print('Error value: ${e.value}');
} on JSNativeException catch (e) {
print('Engine internal exception: ${e.message}');
}
Creating Standard Errors in Dart
final typeError = JSError.typeError(rt, 'Expected a string');
final rangeError = JSError.rangeError(rt, 'Index out of bounds');
final syntaxError = JSError.syntaxError(rt, 'Unexpected token');
// Throw into JS from host functions
throw JSException(typeError);
Memory Management
Finalizers & Reference Counting
- Automatic Cleanup: Every
JSValue,JSObject,JSArray, andJSPreparedJavaScriptregisters with Dart'sNativeFinalizer. When a Dart handle is garbage collected, the underlying native reference count is decremented automatically. - Explicit Release: Call
rt.release()when finished with a runtime to free all associated memory, global objects, and native contexts deterministically. - Retaining Handles: Use
value.retain()to increment the native reference count when sharing handles across asynchronous boundaries.
Weak References (JSWeakObject)
Hold references to JavaScript objects without preventing garbage collection:
final obj = JSObject.create(rt);
final weakRef = JSWeakObject.create(rt, obj);
// Lock weak reference to obtain strong handle
final strongVal = weakRef.lock();
if (!strongVal.isUndefined) {
print('Object is still alive');
}