qjs 0.1.0
qjs: ^0.1.0 copied to clipboard
QuickJS bindings for Dart.
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');
}