flutter_qjs_next 1.1.1
flutter_qjs_next: ^1.1.1 copied to clipboard
QuickJS bindings for Flutter and Dart through dart:ffi (flutter_qjs_next), with native support for Android, iOS, macOS, Linux, and Windows.
flutter_qjs_next #
Flutter / Dart bindings for QuickJS via dart:ffi.
- Embedded QuickJS 2026-06-04
- Platforms: Android, iOS, macOS, Linux, Windows (no Web — native FFI only)
- API style compatible with flutter_js (
JavascriptRuntime,getJavascriptRuntime())
Install #
flutter pub add flutter_qjs_next
import 'package:flutter_qjs_next/flutter_qjs.dart';
Quick start #
import 'package:flutter_qjs_next/flutter_qjs.dart';
void main() {
final js = getJavascriptRuntime(
timeout: 5000, // recommended for untrusted scripts; null/0 = off
// memoryLimit defaults to 64 MiB; use 0 for unlimited
);
final r = js.evaluate('Math.trunc(Math.random() * 100).toString()');
print(r.stringResult);
// Promise / setTimeout need the host event loop:
// either call js.dispatch() after async work, or use evaluateAsync + handlePromises.
js.dispose(); // always dispose — free native heap + channel maps
}
Limits #
| Parameter | Meaning | Default |
|---|---|---|
stackSize |
JS stack size in bytes | 1 MiB |
timeout |
Interrupt after this many ms of wall-clock JS work (null/0 = off) |
off |
memoryLimit |
Heap limit in bytes (0 = unlimited) |
64 MiB |
Each getJavascriptRuntime() is a separate QuickJS engine (own heap, channels, ReceivePort). Create many only when you need isolation; otherwise reuse one runtime or use a pool.
Multi-engine pool #
final pool = JsEnginePool(
maxSize: 4,
config: JsEnginePoolConfig(
timeout: 3000,
// default: resetOnRelease true → reinitialize() between tenants
),
);
final out = await pool.withEngine((js) async {
return js.evaluate('1 + 1').stringResult;
});
pool.dispose();
Engine ids include the isolate hash (qjs-<isolate>-<serial>-<us>) so parallel isolates do not collide in channel maps.
forceJavascriptCoreOnAndroid and xhr are accepted for API compatibility with flutter_js but are not implemented (always QuickJS; no built-in XHR/fetch polyfill).
Dart ↔ JS bridge #
js.onMessage('log', (args) {
print(args); // typically a List from JSON
});
sendMessage('log', JSON.stringify([1, 2, 3]));
Prefer setupBridge / newer channel APIs when available; channel names and payloads should be treated as untrusted if they come from user scripts.
TypedArray / binary #
TypedData (e.g. Uint8List) maps to JS TypedArrays via a bulk buffer path.
ByteBuffer maps to ArrayBuffer.
Use evaluateJson when you only need a Dart JSON-like tree (often faster for large objects).
Event loop / Promises #
QuickJS jobs and setTimeout are drained through the runtime’s ReceivePort. After scheduling async JS, call dispatch() (or rely on paths that already pump the port, e.g. promise helpers in handle_promises.dart).
// Drain Promise microtasks / jobs in a tight loop:
js.executePendingJobs(); // or executePendingJob() once per job
// QuickJsRuntime2 only:
// (js as QuickJsRuntime2).hasPendingJobs
autoExecutePendingJobs (QuickJsRuntime2)
Default is true: after evaluate, evaluateJson, evaluateBytecode, and callFunction, the runtime automatically drains the QuickJS job queue via executePendingJobs() (Promise reactions / microtasks).
Implications:
- Synchronous scripts that schedule Promises often settle without an extra manual drain.
- Call order / timing can differ from engines that only run jobs when you call
dispatch()/executePendingJobs()yourself. - There is a small cost after each of those entry points when jobs are pending.
Disable when you need explicit control or minimal post-call work:
final js = QuickJsRuntime2(autoExecutePendingJobs: false);
// or:
(js as QuickJsRuntime2).autoExecutePendingJobs = false;
With autoExecutePendingJobs: false, drain jobs yourself (executePendingJobs(), dispatch(), or handlePromises) after async JS.
Memory / GC #
js.runGC();
final m = js.getMemoryUsage(); // JsMemoryUsage? (malloc / JS heap sizes)
Default heap limit is 64 MiB (memoryLimit: 0 for unlimited). Always call dispose() so native JSRuntime / JSContext, ReceivePort, and channel maps are released.
Logging #
FlutterQjsLogger.level = FlutterQjsLogLevel.debug;
FlutterQjsLogger.handler = (level, message, error) { /* ... */ };
Documentation #
Full package wiki: docs/wiki/
| Start here | Getting started · Concepts |
| API | API reference · Runtime · Bridge |
| Guides | Security · Performance · Migration |
| Recipes | Hello evaluate · Async bridge · Pool |
| Ops | FAQ · Troubleshooting · Testing |
Example app #
See example/ for a full Flutter demo (AJV, typed arrays, etc.).
cd example && flutter run
Benchmark #
Use Flutter’s test harness (loads the plugin native library correctly).
Do not use bare dart run on this package: on recent Dart SDKs, standalone
compilation of dart:ffi callbacks (Pointer.fromFunction) can crash the
compiler before any code runs.
cd example
# default: 8 RNG seeds → mean ± σ (us/op; buffers also MiB/s)
flutter test test/benchmark_test.dart
# adjust repeats (1 = single seed, faster smoke)
flutter test test/benchmark_test.dart --dart-define=BENCH_RUNS=3
flutter test test/benchmark_test.dart --dart-define=BENCH_RUNS=1
# base seed (used when BENCH_RUNS=1, or to extend seeds when runs>8)
flutter test test/benchmark_test.dart --dart-define=BENCH_SEED=464037
Shared logic: example/lib/benchmark_runner.dart
(runFlutterQjsBenchmarkSuite / runFlutterQjsBenchmarks).
Coverage:
- Hot path: tiny
evaluateand cachedJSInvokable.invoke - String / small
MapDart↔JS identity round-trips - Buffer size ladder (1 KiB / 64 KiB / 1 MiB / 16 MiB) for owned TypedArray paths
- TypedArray variants and non-zero-offset views in both directions
evaluateJsonvs fullevaluate(deep jsToDart) on array and object payloads- Multi-seed mean ± σ (
kBenchmarkDefaultSeeds, default 8 runs) plus OS / CPU / executable banner for cross-commit comparison
Interactive option:
cd example && flutter run
# tap "Run Benchmarks" (same 8-run suite as the test default)
benchmark/flutter_qjs_benchmark.dart only prints these instructions if invoked
with dart run by mistake.
Performance vs pre-optimization baseline #
Compared the current revision to git commit 3261eb4 (pre bulk-buffer /
marshalling work) on the same machine and seeds with BENCH_RUNS=32.
The old commit uses a compatibility runner: its 16 MiB JS→Dart case is omitted
because it cannot complete a practical multi-seed run, and its 1 MiB JS→Dart
steady cases use two iterations instead of twenty. Large-path ratios are
therefore directional, not exact apples-to-apples results.
| Scenario | 3261eb4 (us/op) | current (us/op) | Speedup |
|---|---|---|---|
evaluate tiny (1+1) |
4.1 | 3.5 | 1.2× |
host invoke (a,b)=>a+b |
2.4 | 2.2 | 1.1× |
| string Dart→JS→Dart | 3.2 | 3.1 | 1.0× |
| small Map Dart→JS→Dart | 21.1 | 18.1 | 1.2× |
Dart Uint8List→JS (1 MiB) |
141.6 | 49.3 | 2.9× |
Dart Float64List→JS (10k) |
1863.5 | 4.6 | 405× |
| evaluate large array (full jsToDart) | 3004.1 | 1877.5 | 1.6× |
| evaluate large object (full jsToDart) | 5934.5 | 4916.3 | 1.2× |
JS Uint8Array→Dart (1 KiB) |
602.7 | 5.9 | 102× |
JS Uint8Array→Dart (64 KiB) |
47392.2 | 21.9 | 2164× |
JS Uint8Array→Dart (1 MiB) |
975550.7 | 421.5 | 2314× |
JS Float64Array→Dart (10k) |
7844.7 | 623.1 | 12.6× |
JS Uint8ClampedArray→Dart (offset) |
22206.8 | 9.3 | 2388× |
What improved most
- TypedArray / buffer paths — bulk copy instead of per-element marshalling (largest wins on JS→Dart and non-byte TypedArrays Dart→JS).
- String / host invoke — leaner FFI and value conversion.
- Large array
evaluate— faster recursive jsToDart; useevaluateJsonwhen you only need a JSON-like tree (avoids deep object graph conversion).
Raw logs (full suite output + aggregated means):
benchmark_results/3261eb4/3261eb4_BENCH_RUNS32.txt,
benchmark_results/1c9561d/optimized_BENCH_RUNS32_clamped.txt.
Numbers are single-host microbenchmarks (Linux flutter_tester); treat them as
relative, not absolute product SLOs.
Soak / stress (long-haul) #
Separate from micro-benchmarks: high-concurrency burn-in, RSS/metrics, fail-fast
dump (not us/op). Shared logic: example/lib/soak_stress_runner.dart.
cd example
# short smoke (default ~30s)
flutter test test/soak_stress_test.dart
# ≥1h burn-in example
flutter test test/soak_stress_test.dart \
--dart-define=SOAK_DURATION_SEC=3600 \
--dart-define=SOAK_POOL_SIZE=8 \
--dart-define=SOAK_WORKERS=32
On failure, a dump is written under soak_dumps/ (config, pool stats, RSS,
recent ops, sample getMemoryUsage). Native core dump is optional/external
(gcore / ulimit -c).
Architecture (short) #
lib/quickjs/*— Dart FFI bindings and marshallingcxx/ffi.cpp— stable C ABI around QuickJS (JSValue*on the heap)cxx/quickjs/— embedded engine (same tree used on Windows viacxx-windows/)
Limitations / security #
- Scripts run with full engine capability; do not eval untrusted code without your own sandbox policy.
- Prefer
timeout(wall-clock interrupt) and the defaultmemoryLimit(64 MiB) for untrusted scripts. - No Web platform; no shipping XHR implementation in this package.
- Dispose runtimes you create (
dispose()) to free native resources. - Module sources from
moduleHandlerare copied into QuickJS thenfree’d in native (no Dart microtask free race). JSValue*returned across the FFI boundary is heap-allocated; callers must free via the package APIs (jsFreeValue/ Dart wrappers) — double-free of the same handle after dispose is undefined.