Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
28e394e
docs: plan the quickjs-ng migration
gkurt Aug 23, 2026
e533964
refactor(quickjs): drop the v8-bridge backend
gkurt Aug 23, 2026
33d8a6c
refactor(quickjs): drop the debug server
gkurt Aug 23, 2026
8dfb96f
refactor(quickjs): remove three uncalled shim declarations
gkurt Aug 23, 2026
ba254e6
refactor(quickjs): delete the dead unity-jsb Unity integration
gkurt Aug 23, 2026
763dfab
refactor(quickjs): delete the dead Unity struct marshalling
gkurt Aug 23, 2026
a452ffc
refactor(quickjs): drop the 18 shim declarations left with no callers
gkurt Aug 23, 2026
8f6f0ce
refactor(quickjs): resolve JSB_UNITYLESS away
gkurt Aug 23, 2026
af05e23
refactor(quickjs): split BindingManager.Bind() out of Generate()
gkurt Aug 23, 2026
94fda65
feat(quickjs): build the engine from quickjs-ng, Windows x64
gkurt Aug 23, 2026
8173d4f
docs(quickjs): record what phase 1 and gate 0 measured
gkurt Aug 23, 2026
1b92640
docs(quickjs): correct two stale claims about the JS_* surface
gkurt Aug 23, 2026
9798d23
refactor(quickjs): finish porting the C shim to quickjs-ng
gkurt Aug 23, 2026
351580f
test(quickjs): check the ported shim against ng at runtime
gkurt Aug 23, 2026
a74ac58
docs(quickjs): record what phase 2 measured and changed
gkurt Aug 23, 2026
05b21bb
test(quickjs): check every P/Invoke declaration against ng's header
gkurt Aug 23, 2026
45dc810
refactor(quickjs): bind the C# declarations to quickjs-ng
gkurt Aug 23, 2026
cd75eaa
chore(quickjs): drop the two atom stubs the C# no longer names
gkurt Aug 23, 2026
16a7244
fix(quickjs): stop ignoring the shipped plugin directories
gkurt Aug 23, 2026
319974b
build(quickjs): install the quickjs-ng build for Windows x64
gkurt Aug 23, 2026
e0cc297
docs(quickjs): record what phase 3 measured and changed
gkurt Aug 23, 2026
ee1fdd0
refactor(quickjs): remove operator overloading
gkurt Aug 23, 2026
af76e42
build(quickjs): re-pin ng for the import.meta hook
gkurt Aug 23, 2026
5e6de37
test(quickjs): check the WebGL jslib against the P/Invoke surface
gkurt Aug 23, 2026
14f8083
feat(quickjs): bind quickjs-ng's asynchronous module loader
gkurt Aug 23, 2026
ab425d2
fix(quickjs): bring the WebGL jslib back into agreement
gkurt Aug 23, 2026
a1e279e
feat(core): load QuickJS module graphs over HTTP
gkurt Aug 23, 2026
6e4be90
docs(quickjs): record what phase 4 measured and changed
gkurt Aug 23, 2026
83e359a
refactor(quickjs): remove the last dead operator stub
gkurt Aug 23, 2026
9ac748b
fix(quickjs): stop marshalling '' back to C# as null
gkurt Aug 23, 2026
336d806
refactor(quickjs): drop the codegen
gkurt Aug 23, 2026
83f49cd
test(quickjs): check exports off Windows and without Unity
gkurt Aug 23, 2026
84a358e
build(quickjs): build every platform artifact in CI
gkurt Aug 23, 2026
e4b69ce
build(unity): take the editor version from the project, not a pin
gkurt Aug 23, 2026
2bb2508
fix(quickjs): stop a module load outliving the runtime
gkurt Aug 23, 2026
12aa523
fix(unity): keep a UNITY_VERSION override from repointing the project
gkurt Aug 23, 2026
b822544
add cli pipeline
gkurt Aug 23, 2026
f9bcb6b
remove bundled dev for quickjs
gkurt Aug 23, 2026
9a30a45
test(unity): probe the JS engines inside an IL2CPP player
gkurt Aug 23, 2026
d0a798a
build(kitchen-sink): keep the CLI pipeline package out of the publish…
gkurt Aug 23, 2026
c64e8a9
use auto engine type
gkurt Aug 23, 2026
76e278c
build(tests): add the CLI pipeline package, and make 6000.5 work
gkurt Aug 24, 2026
2c1ea12
refactor(unity): delete the agent bridge, Unity's CLI covers it
gkurt Aug 24, 2026
027c19c
docs(quickjs): close the IL2CPP exit criterion
gkurt Aug 24, 2026
1a59fd6
docs: changelog entry for the quickjs-ng migration
gkurt Aug 24, 2026
9d2c73a
build(quickjs): install the ng build for Linux x64
gkurt Aug 24, 2026
6826879
build(quickjs): install the ng builds CI could produce, fix the three…
gkurt Aug 24, 2026
8ad5457
build(quickjs): read Mach-O archives, and give the container a python
gkurt Aug 24, 2026
3221dda
build(quickjs): install the ng builds for Windows x86, WSA, iOS and L…
gkurt Aug 24, 2026
934ca65
build(quickjs): finish the plugin set -- x64 from CI, x86_64 in, two …
gkurt Aug 24, 2026
43ff265
build(quickjs): tag the fork commit the build pins
gkurt Aug 24, 2026
8788c47
docs: the migration landed, so stop describing it as pending
gkurt Aug 24, 2026
4f6a5ff
feat(quickjs): ES modules on WebGL, through the same host loader as d…
gkurt Aug 24, 2026
ecbc272
docs: 6000.5 is not green on PlayMode, and two notes said it was
gkurt Aug 24, 2026
b8a5f11
refactor(core): delete the host import hook, which no engine can reach
gkurt Aug 24, 2026
4a26107
fix(quickjs): bind JS_SetMaxStackSize, because ng's default cannot fi…
gkurt Aug 24, 2026
a6bdee2
test(unity): transpile fixture JSX with Sucrase, which fits the stack…
gkurt Aug 24, 2026
969af6d
fix(quickjs): cap the script stack by default, so a runaway script th…
gkurt Aug 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
120 changes: 100 additions & 20 deletions .claude/skills/unity/SKILL.md

Large diffs are not rendered by default.

258 changes: 258 additions & 0 deletions .github/workflows/native-quickjs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
# Builds the twelve native `quickjs` artifacts com.reactunity.quickjs P/Invokes into.
#
# Why this exists: the binaries in unity/quickjs/Plugins/QuickJS were unity-jsb
# prebuilts and this repo had no way to reproduce any of them. See
# unity/quickjs/MIGRATION.md, "Gate 0".
#
# It deliberately does NOT commit what it builds. Every artifact is uploaded and
# installing them is a separate, manual step -- the same shape as release-upm.yml,
# and for the same reason: these are tracked binaries in a Unity package, and a
# half-updated set is worse than a stale one. Nothing here needs Unity.
#
# One CMakeLists drives all of it (native/quickjs/CMakeLists.txt); the per-target
# differences are the toolchain arguments below, which is why there is no build
# script per platform. The Android and iOS invocations follow quickjs-ng's own CI.

name: native-quickjs

on:
workflow_dispatch:
inputs:
build-type:
description: CMake build type
type: choice
default: Release
options: [Release, Debug]
pull_request:
paths: ['native/quickjs/**', '.github/workflows/native-quickjs.yml']
push:
branches: [main]
paths: ['native/quickjs/**']

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

env:
BUILD_TYPE: ${{ inputs.build-type || 'Release' }}

jobs:
build:
name: ${{ matrix.name }}
runs-on: ${{ matrix.os }}
# Empty for every leg but linux-x64, which runs the job inside the image named there.
container: ${{ matrix.container || '' }}
strategy:
fail-fast: false
matrix:
include:
# ---- desktop. `native` means the runner can execute what it built,
# which is what lets shim-test run rather than only link.
- name: windows-x64
os: windows-latest
artifact: x64/quickjs.dll
built: build/${{ github.event.inputs.build-type || 'Release' }}/quickjs.dll
configure: -A x64
native: true
- name: windows-x86
os: windows-latest
artifact: x86/quickjs.dll
built: build/${{ github.event.inputs.build-type || 'Release' }}/quickjs.dll
configure: -A Win32
- name: linux-x64
os: ubuntu-latest
# Built in an old container on purpose: glibc is backward compatible, never
# forward, so the floor a binary records is the oldest system it will load on.
# ubuntu-latest is 24.04 (2.39) and the Unity test container is older, so a
# runner-native build produced a .so Unity could not load at all --
# DllNotFoundException, which reads like a missing file rather than a too-new
# one. 22.04 gives 2.35. The binary this replaces asked for 2.17, so this is a
# narrower reach than users had; an older base is the fix if that matters.
container: ubuntu:22.04
artifact: x64/libquickjs.so
built: build/libquickjs.so
configure: -DCMAKE_BUILD_TYPE=${{ github.event.inputs.build-type || 'Release' }}
native: true
- name: macos-universal
os: macos-latest
artifact: quickjs.bundle/Contents/MacOS/quickjs
built: build/quickjs.bundle/Contents/MacOS/quickjs
# One bundle for both Macs; Unity ships OSXUniversal, not per-arch.
configure: >-
-DCMAKE_BUILD_TYPE=${{ github.event.inputs.build-type || 'Release' }}
"-DCMAKE_OSX_ARCHITECTURES=arm64;x86_64"
native: true

# ---- WSA. UWP compiles with /sdl, which promotes two warnings quickjs.c
# trips deliberately; the CMakeLists suppresses them. The static-library
# probe is needed because CMake's default try-compile builds and signs an
# appx, which fails on ARM64.
# WSA ARM (32-bit) is absent on purpose: the Windows SDK dropped 32-bit ARM
# (MSB8087 from 10.0.26100 on) and Unity no longer targets it.
- name: wsa-x64
os: windows-latest
artifact: WSA/x64/quickjs.dll
built: build/${{ github.event.inputs.build-type || 'Release' }}/quickjs.dll
configure: >-
-A x64
-DCMAKE_SYSTEM_NAME=WindowsStore -DCMAKE_SYSTEM_VERSION=10.0
-DCMAKE_TRY_COMPILE_TARGET_TYPE=STATIC_LIBRARY
- name: wsa-x86
os: windows-latest
artifact: WSA/x86/quickjs.dll
built: build/${{ github.event.inputs.build-type || 'Release' }}/quickjs.dll
configure: >-
-A Win32
-DCMAKE_SYSTEM_NAME=WindowsStore -DCMAKE_SYSTEM_VERSION=10.0
-DCMAKE_TRY_COMPILE_TARGET_TYPE=STATIC_LIBRARY
- name: wsa-arm64
os: windows-latest
artifact: WSA/ARM64/quickjs.dll
built: build/${{ github.event.inputs.build-type || 'Release' }}/quickjs.dll
configure: >-
-A ARM64
-DCMAKE_SYSTEM_NAME=WindowsStore -DCMAKE_SYSTEM_VERSION=10.0
-DCMAKE_TRY_COMPILE_TARGET_TYPE=STATIC_LIBRARY

# ---- Android. android-24 matches Unity 6's minimum API level.
# armeabi-v7a and x86_64 are Unity 6's other two ABIs; 32-bit x86 is not
# one of them, which is why the shipped set's `x86` is not rebuilt here.
- name: android-arm64-v8a
os: ubuntu-latest
artifact: Android/libs/arm64-v8a/libquickjs.so
built: build/libquickjs.so
android-abi: arm64-v8a
- name: android-armeabi-v7a
os: ubuntu-latest
artifact: Android/libs/armeabi-v7a/libquickjs.so
built: build/libquickjs.so
android-abi: armeabi-v7a
- name: android-x86_64
os: ubuntu-latest
artifact: Android/libs/x86_64/libquickjs.so
built: build/libquickjs.so
android-abi: x86_64

# ---- iOS. A static archive, per the IOS branch in the CMakeLists:
# Unity links it into the generated Xcode project.
- name: ios-arm64
os: macos-latest
artifact: iOS/libquickjs.a
built: build/${{ github.event.inputs.build-type || 'Release' }}-iphoneos/libquickjs.a
configure: >-
-G Xcode -DCMAKE_SYSTEM_NAME=iOS
-DCMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED=NO
-DCMAKE_OSX_ARCHITECTURES=arm64

steps:
# A bare distro image has none of this. GitHub mounts its own node in, so the
# actions themselves run, but git and a toolchain are ours -- and this has to
# precede checkout, which shells out to git.
- name: Container prerequisites
if: matrix.container
run: |
apt-get update
# python-is-python3 because the shared check step calls `python`, which a
# bare 22.04 does not provide even with python3 installed.
apt-get install -y --no-install-recommends git cmake build-essential ca-certificates python3 python-is-python3

- uses: actions/checkout@v7

- name: Configure
working-directory: native/quickjs
shell: bash
run: |
if [ -n "${{ matrix.android-abi }}" ]; then
cmake -B build \
-DCMAKE_TOOLCHAIN_FILE="$ANDROID_NDK_ROOT/build/cmake/android.toolchain.cmake" \
-DANDROID_ABI=${{ matrix.android-abi }} \
-DANDROID_PLATFORM=android-24 \
-DCMAKE_BUILD_TYPE=$BUILD_TYPE
else
cmake -B build ${{ matrix.configure }}
fi

# Always name the target: quickjs-ng registers run-test262, api-test and
# friends unconditionally, and a default build compiles all of them.
- name: Build
working-directory: native/quickjs
run: cmake --build build --config ${{ env.BUILD_TYPE }} --target quickjs

- name: Build and run shim-test
if: matrix.native
working-directory: native/quickjs
shell: bash
run: |
cmake --build build --config "$BUILD_TYPE" --target shim-test
# Multi-config generators put it in a per-config directory, single-config not.
for candidate in "build/$BUILD_TYPE/shim-test.exe" "build/$BUILD_TYPE/shim-test" "build/shim-test"; do
if [ -x "$candidate" ]; then "$candidate"; exit $?; fi
done
echo "shim-test binary not found" && exit 1

# Reads PE, ELF, Mach-O and archives, and takes the wanted set from the
# committed pinvoke-native.txt -- the .csproj it is otherwise derived from are
# generated by Unity and gitignored, so no job here could produce them.
- name: Check exports
working-directory: native/quickjs
shell: bash
run: |
# ubuntu images ship python3 without a `python` alias
PY=python3; command -v python3 >/dev/null || PY=python
"$PY" check-exports.py "${{ matrix.built }}"

- name: Stage artifact
working-directory: native/quickjs
shell: bash
run: |
mkdir -p "staged/$(dirname '${{ matrix.artifact }}')"
cp "${{ matrix.built }}" "staged/${{ matrix.artifact }}"

- uses: actions/upload-artifact@v7
with:
name: quickjs-${{ matrix.name }}
path: native/quickjs/staged/
if-no-files-found: error

# The WebGL backend has no native library, so its check is about the jslib
# rather than a binary. It belongs here because it asks the same question.
jslib:
name: webgl-jslib
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Check the jslib against the P/Invoke surface
run: python3 native/quickjs/check-jslib.py

- uses: actions/setup-node@v6
with:
node-version-file: .node-version

# The jslib is generated from .source, and nothing else checks that the committed
# one is what .source produces -- a hand-edit or a forgotten rebuild would ship.
- name: Rebuild the jslib and check it matches
working-directory: unity/quickjs/Plugins/QuickJS/WebGL/.source
run: |
npx -y -p typescript@5 tsc
node postbuild.mjs
git diff --exit-code -- ../jsbplugin.jslib

# The module machinery is the one part of the jslib that runs outside a browser, and
# the part with a scanner in it. It is tested against the generated file, not .source.
- name: Run the jslib module tests
working-directory: unity/quickjs/Plugins/QuickJS/WebGL/.source
run: node --test jsbplugin.test.mjs

# One place to see whether the set is complete, and the only job worth making
# a required check.
collect:
name: all artifacts
needs: [build, jslib]
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v8
with:
path: artifacts
merge-multiple: true
- name: List what was built
run: find artifacts -type f | sort
21 changes: 21 additions & 0 deletions .tegami/2026-08-24-4e91cb.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
packages:
"upm:com.reactunity.quickjs": minor
"upm:com.reactunity.core": minor
---

### QuickJS is now quickjs-ng, and modules load without blocking a frame

`com.reactunity.quickjs` bound unity-jsb's fork of Bellard-era QuickJS. It now binds [quickjs-ng](https://github.com/quickjs-ng/quickjs). These are two different engines rather than two versions of one, so the native binary, the C shim over it, and every P/Invoke declaration were rebuilt rather than upgraded — the sources are in `native/quickjs` in the repo, which is new: the binaries used to arrive prebuilt from upstream with no way to rebuild them here.

**What you get for it: `import` actually works.** ng has an asynchronous module loader, and the binding now uses it. A module graph is fetched over `UnityWebRequest` while the engine keeps running, so an `import` of an `http` URL — and therefore a dynamic `import()` — resolves without a blocking frame, and a graph whose second hop is only discoverable once the first arrives still completes. `import.meta.url` is populated on a fetched module, which is what the relative imports inside it resolve against. `EngineCapabilities.ModuleResolution` is how you ask whether the engine in front of you can do this.

**WebGL too, which it never could before.** ES module syntax simply did not work on that backend: it has no QuickJS in it, and the JavaScript reimplementation standing in for one evaluates through `eval`, which cannot run `import` or `export` at all. It now fetches a graph through the same loader every other platform uses, then hands the linking to the browser it is already running in — so live bindings, top-level `await` and the module cache come from there. Two things to know. Circular imports are refused rather than resolved, with an error naming the cycle; and the page needs `unsafe-eval` and `blob:` scripts allowed, which is the default, and `unsafe-eval` was already required by `eval`.

**A runaway script now throws instead of killing the process.** quickjs-ng caps how much C stack a script may use, but its 1 MB default is measured against the thread that created the engine — Unity's main thread, which has less than that left — so the guard never fired. Infinite recursion took out the player or the Editor with a `StackOverflowException` that no `catch` could see. The limit now defaults to 768 KB, so the same script raises an ordinary `RangeError` you can catch and log. `ScriptRuntime.MaxStackSize` changes it: raise it for deliberately deep code, lower it on a platform whose main thread has a smaller stack, and note that zero does not mean "no limit" but "use ng's default", which is the unusable one.

**One behaviour change worth checking your code for.** QuickJS used to marshal an empty JavaScript string back to C# as `null`, which made it the one engine that could not represent `''`. It now returns `string.Empty`. If you wrote `?? ""` or a null check around a value that arrives from script, that workaround is no longer needed — and if you branched on `null` to mean "empty", that branch has changed meaning.

**Two features are gone, and neither was reachable.** Operator overloading is not something ng implements, and the machinery for registering it had sat behind a permanently false guard. The editor-side code generator is gone too: it emitted C# bindings to compile at runtime, which the `.NET Standard` profile Unity compiles against excluded anyway, so it could not run in a Unity project at all. Reflection binding, which is what actually binds your types, is untouched.

Verified against an IL2CPP player on Windows x64, not only in the Editor. The Editor is always Mono, so it says nothing about a P/Invoke stub the AOT compiler had to generate or a type the managed stripper deleted — which is most of what this binding is made of.
Loading
Loading