dart_qjs
dart_qjs is a QuickJS binding for Dart built with dart:ffi.
This project was rebuilt from the ideas and API shape of
ekibun/flutter_qjs, but its biggest
difference is the packaging model: it uses Dart native assets instead of a
Flutter plugin layout. Native code is built through a Dart build hook, bundled
as a native asset, and loaded automatically by the Dart toolchain.
Another important difference is the JavaScript engine source: the original
project used upstream QuickJS, while this project builds against
quickjs-ng.
Why this package
- Run JavaScript with QuickJS from Dart.
- Keep an API close to the original
flutter_qjsproject. - Use
quickjs-nginstead of upstream QuickJS. - Use Dart native assets instead of maintaining platform plugin glue.
- Build the native library with CMake through
native_toolchain_cmake.
Native assets
The native-assets-based workflow is the main reason this package exists.
At build time, the hook in hook/build.dart:
- clones
quickjs-ng - builds the native library with CMake
- registers the resulting dynamic library as a Dart native asset
For consumers, that means:
- no manual
DynamicLibrary.open(...) - no hand-written platform loader code
- no Flutter plugin scaffolding just to ship a native library
Prerequisites
Before using the package, make sure the following tools are available in your environment:
- Dart SDK
^3.11.4 git- CMake
- a working native toolchain for your target platform
The build hook currently clones quickjs-ng during the build, so network access
is also required unless you adapt the hook to use a vendored source tree.
Installation
Add the package to your pubspec.yaml:
dependencies:
dart_qjs: ^1.0.0
Then fetch dependencies:
dart pub get
The native library is built as part of the native assets workflow when the package is compiled or tested.
Quick start
The main engine class is currently named FlutterQjs for API compatibility with
the upstream project.
import 'package:dart_qjs/dart_qjs.dart';
void main() {
final engine = FlutterQjs();
try {
final result = engine.evaluate(r'''
(() => {
const name = 'QuickJS';
return `hello ${name}`;
})()
''');
print(result);
} finally {
engine.close();
}
}
Main-thread engine
FlutterQjs executes JavaScript synchronously and exposes a small event loop for
pending QuickJS jobs.
final engine = FlutterQjs(
stackSize: 1024 * 1024,
timeout: 1000,
memoryLimit: 16 * 1024 * 1024,
);
engine.dispatch();
final result = engine.evaluate('1 + 2');
print(result); // 3
engine.port.close();
engine.close();
Notes:
evaluate()is synchronous on the main-thread engine.dispatch()processes pending QuickJS jobs from the receive port.- Close
portbefore shutdown if you starteddispatch().
Isolate engine
If you want asynchronous evaluation or async module loading, use IsolateQjs.
final engine = IsolateQjs(
moduleHandler: (String module) async {
if (module == 'hello') {
return 'export default (name) => `hello ${name}!`;';
}
throw Exception('Module not found: $module');
},
);
final result = await engine.evaluate(r'''
import('hello').then(({ default: greet }) => greet('world'))
''');
print(result);
await engine.close();
Notes:
evaluate()returns aFutureonIsolateQjs.moduleHandlercan be asynchronous.- Functions used across isolate boundaries should be top-level or static.
Modules
ES modules can be resolved from Dart with moduleHandler.
final engine = FlutterQjs(
moduleHandler: (String module) {
if (module == 'math') {
return 'export const add = (a, b) => a + b;';
}
throw Exception('Module not found: $module');
},
);
final add = engine.evaluate(r'''
import('math').then(({ add }) => add(2, 3))
''');
Module results are cached by QuickJS. To reset module state, close the engine and create it again.
Dart and JavaScript interop
The library converts common Dart values to JavaScript values and back.
| Dart | JavaScript |
|---|---|
bool |
boolean |
int |
number |
double |
number |
String |
string |
Uint8List |
ArrayBuffer |
List |
Array |
Map |
Object |
Future |
Promise |
JSError |
Error |
| other objects | wrapped as DartObject |
JavaScript functions returned into Dart implement JSInvokable.
final engine = FlutterQjs();
final fn = engine.evaluate('(name) => `hello ${name}`') as JSInvokable;
print(fn.invoke(['dart']));
fn.free();
engine.close();
Important:
- Objects implementing
JSRefshould be released when you keep them around. - Use
free()orJSRef.freeRecursive(...)to release JS-backed references. - Use
dup()if you need to retain a reference past an invocation boundary.
Error handling
JavaScript exceptions are surfaced as JSError.
try {
engine.evaluate('throw new Error("boom")');
} on JSError catch (error) {
print(error);
}
Unhandled promise rejections can also be observed with
hostPromiseRejectionHandler.
Development
Useful commands while working on the package:
dart pub get
dart test
Because native assets are involved, the first build on a machine may take longer than a pure Dart package.
Credits
This project is derived from flutter_qjs by ekibun and contributors, with a
reworked package structure and native-assets-based build pipeline.
License
This project is licensed under the MIT License. See LICENSE for the full text.