A binding to the quickjs-ng runtime for Pharo: evaluate JavaScript inside the image and get Pharo values back.
QJSLibrary libraryPath: '/path/to/libqjsshim.dylib'.
QJSRuntime evaluate: '[1, 2, 3].map(x => x * x)'. "#(1 4 9)"
QJSRuntime during: [ :context |
context runtime timeoutMilliseconds: 1000.
context eval: 'function sq(x) { return x * x }'.
context eval: 'sq(12)' ] "144"evaluate: and during: create a fresh runtime and context and release both afterwards, also on error. A fresh runtime with its context costs some 40 µs (quickjs-ng) to 70 µs (Bellard with Intl); an eval: in an existing context some 2 µs.
QJSRuntime during: [ :context |
context at: 'grid' put: aGrid exposing: #(name rowAt:).
context eval: 'grid.name() + " has " + grid.rowAt_(1)' ]Only the selectors you list exist in JS, named PharoJS-style: #rowAt: becomes rowAt_, and #at:put: becomes at_put_. To decide what JS may do with objects that come back from those calls, give a table of classes instead:
context at: 'grid' put: aGrid exposing: {
AGGrid -> #(name rows).
AGRow -> #(id name) }.
context eval: 'grid.rows().map(r => r.name())'The table applies to everything reachable through the binding. An object gets the selectors of the nearest class in its hierarchy that has an entry. Without an entry it reaches JS opaque: JS can hold it and pass it back, but call nothing on it. Values cross as values in both directions: nil, Booleans, numbers, strings, sequenceable collections and dictionaries. A binding that comes back from JS is the Pharo object itself. An error in Pharo becomes a JS Error that JS can catch. context at: 'n' put: 42 sets a plain value.
A call from JS into Pharo costs some 13 µs. The runtime keeps the objects handed to JS until JS lets go of them; it forgets them after the next evaluation.
You can also hold a runtime and contexts yourself. free releases them at once and may be called any number of times, in any order; releasing a runtime releases its contexts too. Without free, finalization releases them when they become garbage. After an image restart they are stale and signal QJSError instead of reaching into freed memory.
shim/holds a small C library,libqjsshim, built against an installed QuickJS. This repository does not contain QuickJS itself. QuickJS passesJSValueby value and keeps many helpersstatic inline, so UFFI cannot call it directly. The shim gives Pharo a flat API of boxes for runtimes, contexts and values; all other values are integers, doubles or UTF-8 bytes. The runtime box keeps track of every context and value box, so they can be released and destroyed in any order (seeqjs_shim.h). Time and memory limits are checked in C.src/holds the Pharo packages (Tonel).QuickJS-CorecontainsQJSLibrary(UFFI),QJSRuntime,QJSContext,QJSBindingandQJSError;QuickJS-Testscontains the tests.- QuickJS comes from its own repository, built with its own build. Two variants work: quickjs-ng (the default, tested at
v0.17.0) and Bellard's QuickJS, whosemastercarries experimentalIntlandTemporalsupport since 2026-10-10.
Results are converted as follows:
undefinedandnullbecomenil.- Numbers become
IntegerorFloat; integral values up to 2^53 come back asInteger. - Strings become
String. - Arrays and plain objects come back as
ArrayandDictionary, element by element. - A binding comes back as the Pharo object it stands for.
- A JS exception is signalled as
QJSError, carrying the JS message and stack.
Not there yet: promises (the job queue).
First build QuickJS with its own build, as a static library compiled position independent. Then build the shim against it; the static QuickJS is linked into libqjsshim, so that one library is all an image needs.
quickjs-ng with its cmake:
git clone --branch v0.17.0 https://github.com/quickjs-ng/quickjs.git
cmake -S quickjs -B quickjs-build -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
-DCMAKE_INSTALL_PREFIX=$HOME/quickjs-ng -DCMAKE_INSTALL_LIBDIR=lib
cmake --build quickjs-build && cmake --install quickjs-build
make -C shim QUICKJS_PREFIX=$HOME/quickjs-ng test # writes build/ng/libqjsshim.(dylib|so)Bellard's QuickJS with its make (QJSC_CC keeps the flag out of the compiler name it records):
git clone https://github.com/bellard/quickjs.git quickjs-bellard
make -C quickjs-bellard CC="cc -fPIC" QJSC_CC=cc install PREFIX=$HOME/quickjs-bellard-install
make -C shim VARIANT=bellard QUICKJS_PREFIX=$HOME/quickjs-bellard-install test # writes build/bellard/On a Mac without a static QuickJS, brew install quickjs-ng and a plain make -C shim test work too; the shim then links Homebrew's shared libqjs.
make test also builds and runs test_shim, which exercises the shim from C without Pharo. make asan runs the same test under AddressSanitizer. The CI builds both variants on Debian 12 and runs the C and Pharo tests with Pharo 12 and 13.
Metacello new
baseline: 'QuickJS';
repository: 'github://ApptiveGrid/QuickJS-Pharo:main/src';
load.
QJSLibrary libraryPath: '/path/to/libqjsshim.dylib'.To run the tests headless from a checkout:
QJS_LIB=$PWD/build/ng/libqjsshim.dylib QJS_REPO=$PWD pharo --headless Pharo.image st scripts/run-tests.stTested with Pharo 13.1 on macOS arm64.