Commit 08193cf
authored
Migrate com.reactunity.quickjs to quickjs-ng (#137)
* docs: plan the quickjs-ng migration
com.reactunity.quickjs binds unity-jsb's fork of Bellard-era QuickJS. The
shipped DLL exports JS_NewBigDecimal and JSB_ATOM_BigFloatEnv and has no
JS_SetModuleLoaderFunc2; quickjs-ng deleted BigDecimal and BigFloat outright.
They are different engines, not two versions of one, and both of our upstreams
are dormant.
The driver is asynchronous module loading - the spec's HostLoadImportedModule,
which lets a host fetch modules over a network without blocking a frame. It is
implemented on gkurt/quickjs and cannot reach Unity until this migration lands.
MIGRATION.md records the measured surface rather than an estimate: which of the
137 P/Invoke names survive as-is, which changed signature in ways that compile
clean and return garbage, and which subsystems can simply go. Also records the
one question grep could not settle, and how to settle it with a single build.
* refactor(quickjs): drop the v8-bridge backend
unity-jsb could build against either QuickJS or its own v8-bridge, selected by
JSB_WITH_V8_BACKEND. No v8-bridge binary has ever shipped in this package -
Plugins/QuickJS holds only libquickjs - so every one of those branches
referenced a DLL that does not exist. ClearScript is how ReactUnity offers V8,
and it is a maintained upstream rather than a fork we would have to carry.
Resolving the symbol to false throughout, which also removes the engine picker
in ScriptEngineStatsWindow: it had one option left.
* refactor(quickjs): drop the debug server
The five JS_*Debugger* entry points and JS_SetLogFunc only ever existed in the
v8-bridge build; the QuickJS build got no-op stubs. Two further layers made them
unreachable even in principle: ScriptRuntime.Initialize hardcoded
args.withDebugServer = false before anything read it, so the "waiting for
debugger" branch could not be taken, and OnDebuggerConnected had no subscribers.
So this removes no behaviour. It does remove the impression that QuickJS can be
debugged - ScriptRuntimeArgs advertised withDebugServer, waitingForDebugger and
debugServerPort, and QuickJSEngine dutifully set all three. Debugging is what
ClearScript is for.
RaiseDebuggerConnectedEvent is renamed RaiseInitialized, which is what it
actually did: fire OnInitializing and OnInitialized. QuickJSEngine still accepts
debug and awaitDebugger, since IJavaScriptEngineFactory.Create passes them for
every engine, and now ignores them.
* refactor(quickjs): remove three uncalled shim declarations
JSB_GetBridgeClassID, jsb_get_int_4 and jsb_set_int_4 have no callers -
JSB_GetBridgeClassID appears only inside a commented-out line. Three fewer
functions to port to quickjs-ng.
Also corrects two claims in MIGRATION.md. JSB_Init and the two *PropertyInternal
wrappers looked dead and are not: the search that found them excluded
Runtime/Source/Native, which is where their callers live. JSB_Init is the DLL
version handshake, and phase 3 has to map *PropertyInternal onto ng'"'"'s
JS_GetProperty/JS_SetProperty rather than deleting them. Noted in the doc so the
next pass does not repeat the mistake.
* refactor(quickjs): delete the dead unity-jsb Unity integration
`Runtime/Source/Unity/` was compiled out in its entirety. Every file in it
opens with `#if !JSB_UNITYLESS`, and `JSB_UNITYLESS` is defined for all five
jsb assemblies by a `versionDefines` entry with an empty expression, on every
platform. Verified by injecting a syntax error inside one of the guards and
watching the compile pass.
That takes two whole assemblies with it -- jsb.editor.unity (25 files) and
jsb.editor.hotfix (1 file, plus 468 KB of bundled Unity.Cecil DLLs) -- neither
of which is referenced by anything outside the tree being removed. ReactUnity
supplies its own Unity layer, so jsb's JSBehaviour, inspectors, editor windows
and Prefs GUI were never reachable.
The hard type references from live files (DefaultAsyncManager, ScriptContext,
ScriptRuntime) all sit inside dead blocks; those blocks go in a later commit.
* refactor(quickjs): delete the dead Unity struct marshalling
Settles the open question in MIGRATION.md, and more cheaply than the experiment
it proposed. Fifteen of the sixteen `Binding/ValueTypes/Values_*.cs` files are
whole-file `#if !JSB_UNITYLESS`, so the Vector/Color/Matrix fast paths that
account for most of the shim call-sites were never compiled. No experiment
needed -- the preprocessor had already answered it.
`Values_DateTime.cs` stays: System.DateTime needs no Unity, and it is the only
live file in the directory. `Values_inject.cs` goes with them, being dead for
the same reason and the only other consumer of JSBehaviour.
This is what shrinks phase 2. jsb_get/set_float_4, _floats, _int_1..3,
_byte_4 and jsb_get_payload_header lose every call-site here.
* refactor(quickjs): drop the 18 shim declarations left with no callers
With the Unity struct marshalling gone, the JSB_*/jsb_* surface measures 59
declarations, 41 with live callers and 18 with none. The 18 are the struct fast
paths (jsb_get/set_float_2..4, _int_1..3, _byte_4, _floats) plus JSB_FreePayload
and jsb_construct_bridge_object, which had no C# caller even before this branch.
That sizes phase 2 exactly: 25 shim functions plus 16 JSB_ATOM_* to port, not
the "18 to 41" the plan guessed from grep.
The jslib still implements all 18 -- Plugins/QuickJS/WebGL/.source/jsbplugin.ts
is the source of truth there and regenerating it belongs with the rest of the
phase 4 WebGL work. Unused exports are harmless in the meantime.
* refactor(quickjs): resolve JSB_UNITYLESS away
The symbol was supplied unconditionally by a `versionDefines` entry in each
asmdef, so `#if !JSB_UNITYLESS` was permanently false and `#if JSB_UNITYLESS`
permanently true. Substituting it and constant-folding removes 407 lines and,
more usefully, 22 conditionals wrapped around the P/Invoke calling-convention
attributes and delegate rooting in JSApi.cs -- exactly the declarations phase 3
has to rewrite one by one against the ng header.
Compound conditions were simplified rather than resolved where the symbol was
not decisive: `JSB_UNITYLESS && !UNITY_2019_1_OR_NEWER` becomes
`!UNITY_2019_1_OR_NEWER`. Conditions not mentioning the symbol are left byte-for
-byte alone, so the diff carries no incidental reformatting.
Also drops the now-inert versionDefines entries. JSB_RUNTIME_REFLECT_BINDING
stays: the asmdef defineConstraints still test it.
Two mentions survive, both in comments, one of them inside a commented-out
block that also names a declaration deleted earlier on this branch.
* refactor(quickjs): split BindingManager.Bind() out of Generate()
`QuickJSEngine.InvokeReflectBinding` called `bm.Generate(TypeBindingFlags.None)`,
which constructed a `CodeGenerator`, called `cg.Begin()`, and ran `cg.Generate()`
per exported type into buffers nothing ever read -- no `codegenCallback` is
passed, so every write was guarded off. The reflect binding needs none of it.
`Bind()` is that path with the codegen removed: the IBindingCallback hooks,
OnPre/OnPostGenerateType, the delegates, and the static modules. The log tail both
paths shared is now `SubmitLog()`.
`Generate(TypeBindingFlags)` is untouched, so this is not the codegen deletion --
see "The codegen is not separable" in MIGRATION.md for why that one is a product
decision rather than a subtraction.
Suite is byte-identical to the run before the change: EditMode 340/348 passed,
PlayMode 689/701, 0 failed either side.
* feat(quickjs): build the engine from quickjs-ng, Windows x64
Gate 0. The repo had no way to reproduce any of the twelve shipped binaries --
they are unity-jsb prebuilts. This builds one of them from source.
CMake fetches quickjs-ng pinned to a commit (a fork, since the async module
loader is not upstream), builds it static with BUILDING_QJS_SHARED so JS_EXTERN
becomes dllexport, and links it into a single `quickjs` shared library that
re-exports ng's whole JS_* API alongside the JSB_*/jsb_* shim.
The shim is vendored from unity-jsb (MIT) and ported in four changes -- the
JS_BOOL alias, JS_NewClassID gaining a JSRuntime*, JS_FreeRuntime becoming void,
and JSB_Init losing the runtime it needed -- plus two additions. All of them, and
the one deliberate behaviour regression (the "gc object leaks" diagnostic, which
ng reports by asserting instead of by return value), are written up in the README.
Measured against the C# layer rather than assumed: of the 105 P/Invoke names
live in `Runtime/Source/Native`, this DLL satisfies 99. The 6 gaps are all
C#-side work for phase 3 and all enumerated in MIGRATION.md. It exports the four
async-loader entry points and does not export JS_NewBigDecimal -- the exact
inverse of the fingerprint that identified the old binary as Bellard-era.
Windows x64 / MSVC only. unity-jsb built Windows with MinGW; both follow the
Win64 ABI for the 16-byte JSValue return, but the other eleven artifacts are
untested and there is no CI matrix yet.
* docs(quickjs): record what phase 1 and gate 0 measured
Most of this replaces guesses with numbers, and two sections replace claims that
were wrong.
The plan said `Binding/ValueTypes/` was an open question needing a build to
settle, and that the shim was somewhere between 18 and 41 functions. Both were
answered by noticing that JSB_UNITYLESS is defined unconditionally: 28.9% of the
package was never compiled, the struct fast paths among it, and the real number
is 25 functions plus 16 atoms.
It also called `Binding/Editor/` pure codegen with no native dependency and
treated deleting it as subtraction. It is reachable at runtime from
BindingManager._EmitDelegateMethod, so that is now written up as a feature
decision rather than a pending chore.
New: the four atoms ng does not have and what each costs, the six names phase 3
must fix in C#, and where gate 0 stands. Plus check-exports.py, so the gate 0
claim is a command rather than a paragraph.
* docs(quickjs): correct two stale claims about the JS_* surface
The debugger paragraph still described the five JS_*Debugger* entry points and
JS_SetLogFunc as living behind JSB_WITH_V8_BACKEND -- they were deleted earlier
on this branch.
And JS_NewFloat64 was listed as needing a shim alongside JS_NewString. It already
has one: the C# declaration carries EntryPoint = "JSB_NewFloat64". Only
JS_NewString is unshimmed. Noted, because an audit of this surface that ignores
EntryPoint reports gaps that are not there.
* refactor(quickjs): finish porting the C shim to quickjs-ng
Gate 0 got the shim compiling against ng. Compiling is not porting: two of
the differences between ng and Bellard-era QuickJS change behaviour rather
than break the build, and the vendored source handled neither.
JS_SetOpaque is the important one. Bellard's wrote the pointer into whatever
object it was handed; ng returns -1 for anything that is not an object of a
registered class. Left unchecked, a failure leaks the payload and hands C# a
bridge object whose id reads back as 0, with nothing reported anywhere. Both
live constructors now go through one helper that checks it.
js_malloc's return was never checked either, so an allocation failure
dereferenced NULL instead of propagating the exception ng had already thrown.
Same for the runtime payload in JSB_NewRuntime, where registering the class
before allocating it leaves neither failure path anything to unwind.
Two smaller ones: JS_NewClass's return was ignored, so a failed registration
produced a runtime whose bridge objects silently had no class; and the atom
accessors were declared K&R `()` rather than `(void)`, which C23 redefines.
Then subtraction. 26 shim functions nothing names are gone -- JSB_Eval,
JSB_FreePayload, JSB_GetClassID, JSB_GetBridgeClassID,
jsb_construct_bridge_object, jsb_get_payload, and the 20 struct accessors
phase 1 orphaned when it deleted the Unity value-type marshalling. The
UNITY_WEBGL and EMSCRIPTEN guards go too: on WebGL JSApi.JSBDLL is
"__Internal" and this library is never loaded, so they guarded a
configuration that cannot occur -- and incoherently, skipping quickjs.h and
then using JSAtom.
JS_NewString is deleted rather than shimmed. ng made it `static inline`, so
the plan was to give it a JSB_ shim the way JS_NewFloat64 already has one;
grepping first showed the C# declaration has no callers at all.
Suite unchanged: 340/348 EditMode, 689/701 PlayMode, 0 failed.
* test(quickjs): check the ported shim against ng at runtime
A successful link proves nothing about the atom table, which is the shim's
riskiest part. The accessors are generated from ng's own quickjs-atom.h with
the same DEF trick quickjs.c uses, so the numbering is right by construction
-- but that is an argument, and ng went from 224 atoms to 241. Bad numbering
would not crash; every atom-keyed property lookup would silently address a
different name.
shim-test links the shim against ng in-process, so it runs before anything is
copied into Unity, and asserts all 241 ids and their strings, that ng still
lacks the four atoms it is missing, and that the bridge payloads round-trip
with the class finalizer firing. It is registered with CTest too.
Worth running in Debug as well: ng reports a non-empty GC object list by
asserting in JS_FreeRuntime rather than returning a value the way unity-jsb's
patched Bellard did, so a Debug pass is what is left of the leak diagnostic
this port gave up.
check-exports.py now diffs both directions. It only reported names C# calls
that we do not export, which let the 26 uncalled shim functions sit there
unnoticed; a stale export is cheap but it is also how the surface grows back.
* docs(quickjs): record what phase 2 measured and changed
The shim is 27 functions, not the 25 this plan claimed. The old count went by
C# member name, so it missed the three entry points reached through EntryPoint
aliases on JS_*-named declarations -- JSB_DupValue, JSB_FreeValue and
JSB_NewFloat64. The plan warned that any audit of this surface has to honour
EntryPoint and then got it wrong anyway, which is the argument for re-running
check-exports.py over reading the tables.
Re-measured the whole surface while correcting it: 104 live entry points, 59
JS_* and 27 shim and 16 atoms and 2 allocator, against 137 declarations before
phase 1 started deleting.
Phase 3's list drops to five names, and its two property remaps are now
verified rather than proposed: ng's JS_GetProperty and JS_SetProperty are
exact identities of the wrapper bodies in JSApi.cs. Its JS_AddIntrinsicOperators
entry is restated -- making the no-bignum branch permanent is the fix, and it
also retires the two operator-overloading atom stubs in unity_qjs.c.
* test(quickjs): check every P/Invoke declaration against ng's header
check-exports.py answers "does the symbol exist", which is the failure
that throws on first call. It cannot see the failure that doesn't: a
signature ng changed still links, and just returns the wrong answer.
check-signatures.py parses every prototype in quickjs.h, parses every
live P/Invoke, and diffs return width, arity, discarded returns and bool
parameter width. Run against the tree as it stands it reports eleven
mismatches -- the five MIGRATION.md predicted, plus three it did not
have. The next commit fixes them.
Sabotaged before being trusted, the same way shim-test was: four
doctored copies of quickjs.h, one per check, each of which it caught.
Its blind spot is deliberate and recorded in the docstring -- it reads
DllImport declarations, so callback delegates, enums and struct layouts
stay hand-checked.
Both scripts now read the live P/Invoke set through pinvoke.py rather
than each carrying its own copy of the C# preprocessor evaluator. They
need the same two things, and duplicating them is how the two checks
would come to disagree about what "live" means.
* refactor(quickjs): bind the C# declarations to quickjs-ng
Every live P/Invoke now matches its prototype in quickjs.h, and
check-signatures.py reports 0 mismatches across all 60. The five names
MIGRATION.md predicted are resolved:
- JS_GetProperty/JS_SetProperty bind straight to ng's exports. Their
bodies are exact identities of the wrapper methods they replace
(quickjs.c:9266, :10807), and the *Internal functions they called are
no longer public.
- IsOperatorOverloadingSupported is permanently false, since ng removed
the engine feature. That takes JS_AddIntrinsicOperators and both
Operators atoms off the surface. The cost is bounded: the only
consumers are two checks in TypeBindingInfo, so what goes is codegen
emitting operator overloads, on a path ReactUnity does not use.
jsb.isOperatorOverloadingSupported still answers correctly.
- fileName/lineNumber are gone from ng's Error objects, which keeps only
the Function.prototype getters. FormatException drops both reads: ng's
stack already opens with " at <file>:<line>:<col>" in exactly the
shape the deleted code assembled by hand (quickjs.c:8256). Verified on
a real parse error from the suite, which now reports the same location
plus a column and the frames above it.
Three the plan had wrong or missing, all found by running the check:
- JS_IsJobPending carried an `out JSContext pctx` that no QuickJS header
has ever declared, Bellard's included. The callee ignored the second
register; the caller read back an uninitialised local.
- JS_SetConstructor returns int in ng and was declared void, discarding
a failure.
- JS_ToCStringLen2's cesu8 was marshalled as UnmanagedType.Bool, the
four-byte Win32 BOOL, against a one-byte C bool.
Two more from auditing what the check cannot see:
- JSHostPromiseRejectionTracker's is_handled narrowed to a C bool. This
is a reverse P/Invoke, so the managed signature decides how many bytes
come off the register, and reading four where ng wrote one takes three
undefined bytes with it. It guards the "Unhandled promise rejection"
log, so the symptom would have been handled rejections reported as
unhandled. Correct before ng: Bellard's really did pass JS_BOOL.
- JS_EVAL_FLAG_STRIP is JS_EVAL_FLAG_ASYNC_LOAD in ng -- same bit,
unrelated meaning. Nothing passed it, but it is bit 4 of the flag word
phase 4's async loader has to set, so the old name was a trap laid in
the next phase's path.
Constants and layouts were re-derived rather than carried forward, all
of them and not just the tags. SHORT_BIG_INT occupies 7 in ng, the slot
FLOAT64 used to hold, so a stale tag block reads every double as a
bigint. JSPropFlags, JSGPNFlags, JSCFunctionEnum, JSMemoryUsage and ten
of the eleven delegates already agreed; JSEvalFlags and
JSPropertyEnum.is_enumerable did not.
The one bug here that no signature check would have caught:
JSValue.IsString() tested only JS_TAG_STRING. ng represents `a + b` as
an unflattened rope with its own tag and hands it out as an ordinary
value -- which is why ng's own JS_IsString accepts both tags -- so every
concatenated string reaching a binding was classified as a non-string.
Reachable from any script, with no error to notice.
Call sites follow from the declarations: 25 dropped JSContext arguments,
42 comparisons against 1/0 turned into boolean expressions, and 18
unreachable `isArray == -1` blocks removed, ng's bool having no room for
the tri-state Bellard's returned for the proxy case.
* chore(quickjs): drop the two atom stubs the C# no longer names
Phase 2 hand-wrote JSB_ATOM_Operators and JSB_ATOM_Symbol_operatorSet to
return JS_ATOM_NULL, because ng removed operator overloading and the atom
table has no entry to generate an accessor from. It also predicted they
would go once the C# stopped asking, and that check-exports.py would say
so. It did, reporting both as stale.
Every atom accessor the shim exports now comes from the one macro over
quickjs-atom.h. shim-test keeps asserting ng defines none of the four
atoms unity-jsb named, so the reasoning written against their absence
fails loudly if a future ng brings one back -- the two CHECKs it loses
here covered the stubs specifically, which no longer exist.
shim-test still passes in Release and Debug; Debug is the one that runs
ng's assert on a non-empty GC object list.
* fix(quickjs): stop ignoring the shipped plugin directories
unity/quickjs/.gitignore is a Visual Studio template inherited from the
merged quickjs repo, and two of its build-output rules, [Xx]64/ and
[Xx]86/, also match Plugins/QuickJS/x64 and x86 -- the directories the
native binaries actually ship from.
The four files already in x64 and the two in x86 are tracked, so they
survive the rule and everything looked fine. What does not work is
adding a new one: it is silently untracked, and `git add` on it fails
outright, which takes lint-staged and the pre-commit hook down with it.
That is how this surfaced -- installing the quickjs-ng build could not
be committed at all.
Negates the two paths rather than deleting the rules, so a genuine
Debug/Release/x64 build directory elsewhere in the package is still
ignored. Verified both ways: a new file under Plugins/QuickJS/x64 and
x86 now shows up, and unity/quickjs/x64/junk.obj is still matched by
line 18.
Gate 0 has eleven more binaries to install, two of them in these two
directories.
* build(quickjs): install the quickjs-ng build for Windows x64
Replaces the unity-jsb prebuilt with what native/quickjs produces: ng
plus the ported shim. First time the engine this migration is about has
actually been loaded by Unity.
A passing suite would not on its own prove ng is what ran, so the
evidence is structural: ng exports JS_GetProperty and JS_SetProperty,
which are static inline in Bellard's and absent from the old DLL's
exports, and the C# commit binds both directly. The old binary would
have thrown EntryPointNotFoundException on the first property read. The
suite is unchanged from the pre-migration baseline -- EditMode 340/348,
PlayMode 689/701, 0 failed in both.
This pairs with the C# commit and only works with it: either half alone
is a broken tree, because ng and Bellard do not export the same names.
That is inherent to the change rather than sloppy sequencing.
The other eleven artifacts are still unity-jsb prebuilts of Bellard-era
QuickJS, so every platform other than Windows x64 is now inconsistent
with this C#. Gate 0 already tracks them. Nothing regressed, but nothing
outside Windows x64 works on this branch either.
Copied by hand, as gate 0 does for now.
* docs(quickjs): record what phase 3 measured and changed
Phase 3 marked done, with its exit criterion split honestly: the Unity
suite passes against the ng DLL on Windows, and the IL2CPP player build
the criterion also asks for cannot run here at all -- no installed
editor has the IL2CPP player variation, so it needs a Hub module and a
batch-mode build entry point scripts/unity does not have. Recorded as
open rather than waved through, along with what it would have covered
that the Editor run does not.
The measured surface is re-counted at 99 live entry points, down from
104 by subtraction: seven declarations deleted, two added. Both checks
now exit 0.
"What fails silently" is rewritten around the seven rows phase 3 fixed,
three of which are corrections to what this document claimed. The
JS_IsJobPending row said width-only; the parameter it actually carried
was in no QuickJS header ever. That is the second time an audit of this
surface has been wrong in a way running the check would have caught, so
the table now says where it comes from.
New sections cover what phase 3 found -- the string-rope IsString bug,
the is_handled callback width, the JS_EVAL_FLAG_STRIP bit that now means
ASYNC_LOAD -- and what check-signatures.py does and cannot see.
Also notes the gitignore trap in gate 0, since two of the eleven
remaining artifacts land in the directories it silently swallowed, and
records that operator overloading is now permanently unsupported, with
the cost bounded to codegen emitting operator overloads.
* refactor(quickjs): remove operator overloading
quickjs-ng has no operator overloading -- the engine feature is gone, not
disabled -- so phase 3 pinned IsOperatorOverloadingSupported to false and
left the machinery behind the guard. This deletes it.
Gone from the runtime: OperatorDecl and its two defs, TypeRegister's six
RegisterOperator overloads with SubmitOperators and GetOperatorDecl,
ClassDecl's Add{Self,Left,Right}Operator, ScriptContext's Operators.create
lookup and CreateDefaultOperators, and JS_AddIntrinsicOperators with both
atoms. Gone from the binding generator: Prefs.enableOperatorOverloading and
alwaysEmitOperatorMethod, TypeTransform.EnableOperatorOverloading,
OperatorBindingInfo, the op_* switch in AddMethod, CodeGenHelper_Operator,
and the two places codegen emitted an AddSelfOperator call.
`op_*` methods still bind as ordinary static methods under those names,
which is what they already did with the guard false -- IsSupportedOperators
survives as IsOperatorMethod, whose one remaining caller is the
special-name filter in CollectMethods and has nothing to do with
overloading.
Three consequences worth naming:
- `jsb.isOperatorOverloadingSupported` is no longer defined. It read false,
and reads undefined now, so anything gating on it still takes the same
branch.
- TypeBindingInfo.preload was `operators.Count != 0` and nothing else ever
set it, so both call sites now pass false.
ScriptRuntime.AddTypeReference keeps the parameter: generated bindings
pass it and eager binding is not an operator concept.
- JSB_ATOM_Function went with _functionConstructor, whose only reader was
TypeRegister.GetConstructor(typeof(JSFunction)) on the operator path.
check-exports.py is 98 of 98 now, still 0 stale.
The WebGL jslib loses the same three entry points. Its build instructions
now pin typescript@5: TS 7 removed every option this tsconfig needs and has
no ES5 emit at all, so `npx -p typescript tsc` fails on the config rather
than emitting anything.
Compiles clean and the suite is unmoved -- EditMode 340/348, PlayMode
689/701, 0 failed.
* build(quickjs): re-pin ng for the import.meta hook
gkurt/quickjs 30ceffe adds JS_SetModuleMetaFunc -- ECMA-262's
HostGetImportMetaProperties -- and implements the JS_LoadModuleAsync that
quickjs.h already named twice without declaring.
The async loader is handed source text and compiles it itself, so unlike the
synchronous loaders it never gives the host a JSModuleDef, and populating
import.meta has always been host policy. Without the hook every module
loaded asynchronously reports `import.meta.url` as undefined, which two
tests assert on for every engine. Measured, not assumed: with the hook's
call removed the fork's own suite reports 'undefined|undefined'.
shim-test passes 241/241 in both configurations, ctest passes, and the DLL
exports all seven module entry points.
* test(quickjs): check the WebGL jslib against the P/Invoke surface
The jslib reimplements the whole JSBDLL surface on the browser's engine, and
nothing kept it in agreement with the C# that calls it. check-exports.py
answers "does the library export this" for the native backend; this asks the
same question of the other one, with dumpbin replaced by reading the
generated jslib, and reports both directions.
Both directions had something on the first run:
- JS_GetProperty and JS_SetProperty are missing. Phase 3 deleted the two
*Internal wrappers and bound ng's exports directly; the jslib never grew
them, so on WebGL this branch does not link -- an undefined symbol on the
first property read, which is every property read.
- 26 entries are named by nothing: the 18 jsb_get_*/jsb_set_* declarations
phase 1 removed, the two *Internal functions, JS_NewString,
JSB_ATOM_fileName and lineNumber, and three bridge functions.
Liveness is re-evaluated with a WebGL define set rather than the Editor one,
which is not cosmetic: JS_SetBaseUrl is a real P/Invoke exactly where the
jslib is, and an Editor-define reading would report it as an unused entry.
It checks names only. The jslib has no header to compare signatures against,
so arity and tag values stay a reading exercise -- which is how it came to
hold Bellard's tag numbers and the pctx argument on JS_IsJobPending that no
QuickJS header has ever declared. Fixed in the next commit; this one is the
check, and it exits 1 as it stands.
* feat(quickjs): bind quickjs-ng's asynchronous module loader
The synchronous loader has to return a JSModuleDef there and then, so a host
that fetches over a network cannot satisfy it without blocking - which is the
whole reason for this migration. This binds the async one.
JSApi+AsyncModules.cs declares the six entry points and the two callbacks;
AsyncModuleLoader owns the trampolines, the GCHandle the engine carries as its
opaque pointer, and a path-style normalizer for anything a subclass declines to
resolve. ModuleLoadCompletion is deliberately the same shape as Jint's, so the
two loaders read alike. ScriptContext.EvalModuleAsync starts a graph and
returns; the six declarations were checked against quickjs.h by
check-signatures.py along with the other 60.
Four things in it are load-bearing:
- Nothing has evaluated when EvalModuleAsync returns. Draining the job queue
afterwards finishes a graph that needs nothing from the loader, which is
every bundle; one waiting on a request finishes over later updates.
- A rejected graph is reported once. Attaching the handler also marks the
rejection handled, so this replaces the tracker's "unhandled promise
rejection" rather than adding a second line. The handler is created once per
context: JSB_NewCFunction roots its delegate for good, so a fresh one per
graph would leak a GCHandle on every hot reload.
- No exception may cross back into C. Unwinding through the engine's frames
would leave the load handle unsettled and hang the graph for good.
- The delegates stay rooted and the GCHandle is freed only after the runtime,
which still holds the pointer it backs.
Two fixes came out of writing it. JSModuleNormalizeFunc and JSModuleLoaderFunc
declared their module names as [MarshalAs(UnmanagedType.LPStr)] string - the
ANSI code page against a const char* the engine encodes as UTF-8, so any module
path outside ASCII arrived mangled. Both now take IntPtr and decode through a
new JSApi.GetString(IntPtr). And import.meta no longer needs the
compile-then-set-then-eval dance: the engine calls the host back instead.
* fix(quickjs): bring the WebGL jslib back into agreement
check-jslib.py is green: 105 of 105 with nothing unused. The two directions it
reported are both fixed.
JS_GetProperty and JS_SetProperty are implemented, which is what stopped this
branch linking for WebGL at all. JS_SetProperty takes ng's shape: no flags
argument, since JS_PROP_THROW is the only combination the C# ever passed, and an
int return rather than a bool. And 24 entries nothing declares are gone - the 18
jsb_get_*/jsb_set_* struct accessors phase 1 deleted, JS_NewString,
JSB_ATOM_fileName, JSB_ATOM_lineNumber and three bridge functions - with the two
*Internal property functions renamed rather than dropped.
The signature work the check cannot do was read off the C#: the tag block
re-derived from ng's enum, the JS_WRITE_OBJ/JS_READ_OBJ flags, JSEvalFlags with
bit 4 renamed to ASYNC_LOAD, JSPropFlags, and the arities of JS_IsArray,
JS_IsError, JS_IsJobPending and JS_SetConstructor.
Widths do not matter on this side - wasm passes a C bool as an i32, so nothing
like phase 3's is_handled exists here - but arity does, and silently:
JS_IsArray(ctx, val) against a caller passing one argument reads val out of the
ctx slot. ng dropped the JSContext from JS_IsArray and JS_IsError while
references are kept per runtime, so there is nothing left to resolve a JSValue
against; getAnyValue searches the live runtimes, and two of them would be
ambiguous, which needs JSWorker, which needs threads WebGL does not have.
The six async module entry points are here because the Emscripten link needs
them, not because they work. This backend has no module scope at all:
context.evaluate is an eval inside the sandbox iframe, wrapped in with(globals)
so a bundle sees ReactUnity's globals, and eval cannot run import or export. ES
module syntax has never worked on WebGL and this does not change that - each
entry point reports a clear failure instead of pretending. Giving the iframe a
module realm and reconciling it with a globals proxy a module cannot see is its
own piece of work.
The build instructions now pin typescript@5: TS 7 removed every option this
tsconfig needs and has no ES5 emit at all, which Emscripten still requires, so
`npx -p typescript tsc` fails on the config. That the pipeline was otherwise
intact was checked first - rebuilding the untouched source reproduced the
committed jslib byte for byte.
* feat(core): load QuickJS module graphs over HTTP
QuickJSModuleLoader resolves an import specifier against the module importing it
and fetches each one with UnityWebRequest over Dispatcher.StartDeferred, the way
JintModuleLoader does - the two are deliberately the same shape, because they
answer the same question and a divergence between them should be visible by
reading them side by side.
QuickJSEngine installs it before anything executes, and now claims
EngineCapabilities.ModuleResolution, which retires ModuleCompat's dynamic-import
rewrite and the host import hook for this engine. Not on WebGL: that backend has
no QuickJS and evaluates through eval, which cannot run module syntax at all, so
it keeps the hook and RewriteDynamicImports stays alive for it.
AStaticImportGraphLoadsAsynchronously no longer skips QuickJS, which is the
evidence that this works and that it works asynchronously: the test asserts
nothing has evaluated when ExecuteScript returns, and the synchronous loader
would have resolved and run the whole two-hop graph inline. It also asserts a
fetched module keeps a whole url in import.meta.url, which is what the relative
import below it resolves against. The suite is 690/701 in PlayMode against a
689/701 baseline - one more test, not one fewer skip.
OnlyQuickJSCannotResolveSpecifiersItself becomes
EveryEngineResolvesSpecifiersItself, since that is now the invariant the import
hook's existence is measured against.
One unrelated quirk had to be worked around rather than fixed: QuickJS marshals
'' back as null, JSApi.GetString returning null for any zero-length string, so
the test's probe coalesces. Naming it here because the probe now hides it.
* docs(quickjs): record what phase 4 measured and changed
The measured surface is 104 live P/Invokes on the native backend and 105 on
WebGL, both fully satisfied, with all 66 JS_* declarations matching quickjs.h.
Four corrections to what this document claimed:
- It said the async loader would be *easier* on WebGL, the browser having real
promises and real import(). That backend has no module scope to put one in -
context.evaluate is an eval in a sandbox iframe wrapped in with(globals) - so
ES module syntax has never worked there at all.
- "Two implementations drifting" is half closed rather than open: check-jslib.py
holds the names in agreement both ways. Signatures are still unenforced.
- Gate 0's export list and the fork's assertion count were both stale.
- The four-atoms section described operator overloading as guarded, not gone.
Plus what phase 4 changed: the module loader and the four things about it that
are load-bearing, the two marshalling fixes writing it turned up, the jslib
reconciliation and what its check can and cannot see, the operator-overloading
removal and the exact capability it costs, and the jslib build failing under
TypeScript 7.
* refactor(quickjs): remove the last dead operator stub
TypeDB._DynamicOperatorInvoke was a [MonoPInvokeCallback] whose entire body
was `throw new NotImplementedException()`, referenced by nothing. It is the
only thing the operator-overloading removal left behind, and dropping it takes
one more reverse-P/Invoke off the IL2CPP surface.
Two operator-named things stay, and both are load-bearing rather than residue.
The op_* switch in CodeGenHelper_Method emits `a + b` for a bound operator
method, which is why one compiles at all -- C# refuses to call op_Addition by
name -- and IsOperatorMethod is the filter that lets it past the special-name
check. main did the same with phase 3's guard already false, so keeping them
preserves that behaviour rather than adding to it.
* fix(quickjs): stop marshalling '' back to C# as null
JSApi.GetString(ctx, ptr, len) returned null for len == 0, so QuickJS was the
one engine that handed C# null for a JS empty string -- and since that is the
marshaller every JS-to-C# string goes through, including the object path in
Values.cs, it made '' unrepresentable. Not an ng regression: main has the same
line. A zero length is now the empty string, and null means only what it should,
that there is no buffer.
Nothing depended on the old behaviour. The two callers that read that null as a
signal both get more correct: JSContext.ToStringSafe used it to detect a
toString that threw and would fire its take-and-drop on a legitimate '', and
js_get_classvalue(out Type) fed it to TypeDB.GetType, which answers the same for
null and "".
Characterised before fixing, which narrowed it: JS null and undefined already
arrived as null despite js_get_primitive carrying a bare `// no check`, and
non-ASCII already round-tripped, Marshal.PtrToStringAnsi resolving to UTF-8
under Mono and IL2CPP. The empty string was the whole divergence, and the
C#-to-script direction was already correct -- so that half of the new test is a
regression guard rather than a fix.
One behavioural consequence worth knowing: InlineStyles.SaveValue treats null as
"remove this property", so `style.x = ''` from script used to clear a
declaration on QuickJS and now sets it to empty. That is convergence -- Jint and
ClearScript always delivered "" there -- and the suite covers it.
StringMarshallingTests pins both directions for all three engines; reverting the
fix fails exactly one of them on exactly one engine. ModuleSyntaxTests drops the
`?? ""` this quirk forced on its probe.
EditMode 346/354 (was 340/348, all six new tests passing), PlayMode 690/701,
0 failed.
* refactor(quickjs): drop the codegen
The last phase 1 item, held back because it was a feature decision rather than
subtraction. ReactUnity never reaches it: nothing under unity/core names a
codegen type, and QuickJSEngine binds through BindingManager.Bind(), the split
phase 1 made for exactly this.
It also could not work here. BindingManager._EmitDelegateMethod built C# with
CodeGenerator and compiled it at runtime as the fallback for a delegate
signature no hand-written template covers -- which is why Codegen/ was reachable
from the reflect-binding path at all -- but the two guards disagreed.
IsCodeEmitSupported() returned true unless NETCOREAPP while Compile was also
compiled out under NET_STANDARD*, and both test projects run
apiCompatibilityLevel 6. So the path ran, built the source, got null from
Compile, dereferenced it, and swallowed the NullReferenceException.
Gone: Binding/Editor/Codegen/ (17 files), the two codegen binding callbacks and
ICodeGenCallback, Generate(TypeBindingFlags) with _WriteCSharp/_WriteTSD and the
codeGenCallback plumbing, _EmitDelegateMethod and the AddAssemblies pair only it
used, the orphaned DocResolver, and UnitylessReflectBind -- the sole caller of
Generate, itself called by nothing. Prefs loses the 23 members left with no
reader and no longer claims to load from js-bridge.json, which nothing has read
for some time; newLineStyle stays because the live newline property is built on
it.
Kept, relocated: CodeGenUtils mixed the runtime compiler in with naming and type
helpers TSTypeNaming and TypeBindingInfo need, so those four are now
BindingUtils. RemoveAt, ToLiteral and ConcatAsLiteral had no readers left.
TextGenerator stays -- it is also BindingManager's log writer.
The cost: a delegate whose signature no template covers can no longer be bound,
in practice ref/out beyond the template set. No behavioural change under Unity,
where that path already returned null; the difference is that
GenerateReflectedDelegateMethod now warns and names the signature instead of
logging a NullReferenceException. Bindings a user already generated keep working
-- they reference runtime types in Binding/, not the generator.
EditMode 346/354, PlayMode 690/701, 0 failed.
* test(quickjs): check exports off Windows and without Unity
Two things stopped the export checks covering anything but the Windows build,
both of which a CI matrix runs straight into.
check-exports.py shelled out to dumpbin, so seven of the twelve shipped
artifacts could not be checked at all. It now reads PE, ELF, Mach-O and static
archives, sniffing the format from the file's magic rather than the host OS, so a
cross-compiled artifact is checkable from whichever runner built it. PE still
prefers dumpbin and falls back to llvm-readobj; the rest use llvm-nm or nm. The
archive reader is not theoretical: iOS ships a .a.
The wanted set came from the generated .csproj in tests/, which are gitignored,
exist only after the project has been opened in Unity, and hold absolute paths --
so no CI job without Unity could derive it. It is now committed as
pinvoke-native.txt (104) and pinvoke-webgl.txt (105). Both checks use the live
declarations when the .csproj are present and the committed file otherwise, and
compare the two when both exist, failing with the added and removed names so the
file cannot quietly rot. --write regenerates. A change to the native surface now
also shows up in a diff, which it did not before.
Verified: unchanged on the Windows x64 DLL (104/104, PE), and the same 104/104 on
a Linux libquickjs.so read as ELF from a checkout with no .csproj at all. The
drift guard was sabotaged -- an invented symbol in the committed file exits 1 and
names it.
* build(quickjs): build every platform artifact in CI
Gate 0's plan was to vendor unity-jsb's jsb_build scripts. That was the wrong
plan and none of it is needed: our own CMakeLists already branches for every
target shape -- IOS static, APPLE module-and-bundle, shared elsewhere -- so the
per-platform difference is a configure line. Nothing to port, nothing to keep in
step with upstream.
native-quickjs.yml is the matrix: build each artifact, run shim-test wherever the
runner can execute what it built, check the exports, upload. Installing into
Plugins/QuickJS/ stays a manual step, like release-upm.yml, because these are
tracked binaries and a half-updated set is worse than a stale one. Nothing in the
workflow needs Unity.
Five of twelve are now proven, four measured rather than argued: Windows x64
(already installed), Windows x86, Linux x64, WSA x64 and WSA ARM64, each 104/104
exports -- plus the shim's 241 atoms passing on Linux, the first time that test
has run off Windows. macOS, iOS and the Android ABIs need a Mac and an NDK, so CI
is their first run.
Four findings, all documented in native/quickjs/README.md:
- WSA was never the hard part. It compiles all of quickjs.c and trips only on
/sdl, which UWP sets by default and which promotes C4146 and C4703 to errors --
two warnings quickjs.c and dtoa.c trip deliberately, already suppressed on our
target but not on ng's. Separately, CMake's compiler probe builds and signs an
appx, which fails on ARM64; a static-library probe skips it.
- WSA ARM (32-bit) is dropped, not pending: the Windows SDK stopped supporting
32-bit ARM (MSB8087, 10.0.26100 on) and Unity no longer targets it.
- The Android ABI list is a unity-jsb relic -- arm64-v8a, armeabi-v7a and 32-bit
x86, with no x86_64, which is the ABI Unity 6 actually targets while 32-bit x86
is not one it offers. The workflow builds x86_64 and not x86.
- iOS would have shipped an empty archive. A static library does not absorb a
static library it links, so the .a would have held the shim alone with every
JS_* symbol missing, and nothing would fail until Unity linked the Xcode
project. The shipped prebuilt is a merged archive; this merges too, and
check-exports.py reads archives so it catches a regression.
The two /sdl suppressions land on ng's qjs target, which desktop shares, so the
Windows x64 build was re-checked: builds, 241 atoms, 104/104 exports.
* build(unity): take the editor version from the project, not a pin
scripts/unity hard-coded an Editor version per project. That goes stale the first
time anyone upgrades and then silently drives the wrong Editor -- which is what
just happened: kitchen-sink was opened with 6000.5.9f1 while the script still
said 6000.5.5f1. getProject now reads ProjectSettings/ProjectVersion.txt, so
whichever Editor actually opened the project is the one these commands use.
UNITY_VERSION= still overrides per run.
The reason the pin existed is kept as a note rather than a constraint: tests/ on
the 6000.5 line resolves com.unity.inputsystem and test-framework.performance
versions that still use TreeView, which 6000.5 made obsolete-as-error, giving 306
compile errors before a single test runs. That is a thing to know when you see
it, not a version to encode.
Also commits kitchen-sink's own version bump from that Editor session:
6000.5.5f1 -> 6000.5.9f1, with inputsystem 1.19.0 -> 1.20.0 and the two
transitive bumps its lock file picked up.
Resolves to tests -> 6000.1.4f1 (unchanged) and kitchen-sink -> 6000.5.9f1.
* fix(quickjs): stop a module load outliving the runtime
Reported as a crash on the second play-mode run after loading a Vite graph, and
the crash log has the detail that makes it lethal: "Entering Playmode with Reload
Domain disabled", twice. With domain reload off, managed closures survive
play-mode exit, so a callback from the previous run can still fire.
ModuleLoadCompletion captured a raw JSContext and JSModuleLoadHandle and was
handed to a UnityWebRequest callback, with nothing checking whether the runtime
still existed. Two failure modes, both real:
- Use-after-free. A response arriving after teardown calls JS_FulfillModuleLoad
on a freed context. That matches the crash: native, inside
JS_ExecutePendingJob.
- A leak. An in-flight load never settles, so its handle and the graph's promise
are still live when JSB_FreeRuntime runs -- the "gc object leaks" path.
The completion is now a ticket: the engine state lives in the loader, keyed by
id, so settling is idempotent and a ticket held past the runtime's lifetime
settles nothing. Teardown is two phases and the order is load-bearing both ways:
Close() runs before Runtime.Shutdown() and rejects what is still in flight while
the context is alive, Dispose() runs after and frees the GCHandle the engine was
holding, as before.
Also corrects a comment the log disproved. It claimed attaching a rejection
handler replaces the tracker's "unhandled promise rejection"; a rejected Vite
graph logs both, because the promises the graph rejects inside itself are
separate from the one attached to.
Compile-verified only. The suite cannot run on this machine: tests/ needs the
6000.1 line and no 6000.1 editor is installed, while the 6000.5 line fails
obsolete-as-error inside com.unity.inputsystem and test-framework.performance
before any test starts. Every error in that run was in a Unity package, none in
this code.
* fix(unity): keep a UNITY_VERSION override from repointing the project
Consequence of reading the editor version from ProjectVersion.txt: Unity rewrites
that file on open and the churn list deliberately left it alone, so one
`UNITY_VERSION=6000.5.9f1` run to check something would silently make 6000.5 the
project's version from then on -- and for tests/ that is an editor which cannot
run its suite at all. The file is now snapshotted and restored, but only when the
override is what asked for a different editor; an ordinary run still leaves the
stamp tracking whatever actually ran, which is what keeps the GUI off the modal
"Project Upgrade Required" dialog.
CLAUDE.md gets the sharper version of the 6000.5 trap, including the part that
misled me: `compile` passes there because it only builds the project's own
assemblies, while `test` pulls in the package editor assemblies that fail.
* add cli pipeline
* remove bundled dev for quickjs
* test(unity): probe the JS engines inside an IL2CPP player
The Editor is always Mono, so nothing `compile` or `test` reports says anything
about the backend that ships. A P/Invoke stub the AOT compiler had to generate
from a signature alone, a reverse callback it never saw, a type the managed
stripper deleted -- that is most of what the QuickJS binding is made of, and a
player is the only place any of it appears.
`pnpm unity player tests --backend il2cpp` builds a development standalone
player and runs EngineProbe in it. The probe creates every engine in the build
and runs seven checks each: evaluate, strings (including '', which QuickJS got
wrong until 9ac748b3), a Func and an Action called from JS, a type reference
through the reflect binder, a global round trip, and a module. Both halves are
gated on REACT_UNITY_DEVELOPER, so none of it ships.
Measured on 6000.5.9f1, tests/:
il2cpp 309 s build QuickJS 7/7, Jint 7/7
mono 15 s build QuickJS 7/7, Jint 7/7, ClearScript 7/7
Two engines against three is the gating working: ClearScript is compiled out
under IL2CPP by design, and the probe is what shows you that rather than
assuming it.
Both guards were checked by making them fail. Expecting 3 from `1 + 1` reported
`evaluate: expected 3, got 2` on all three engines and exited 1. Pointing a
`--backend mono` run at the IL2CPP player reported the mismatch and exited 1 --
the probe prints the backend the *player* was compiled with, not the one the run
asked for, because a run that quietly fell back to Mono is not a pass.
Not on CI: it needs a C++ toolchain and the IL2CPP module on the runner.
Snapshot asserts now go Inconclusive when the .snapshots folder is not there
instead of laying down a fresh baseline beside the executable and passing --
a player has no project folder, so that path was a false green waiting to
happen. UnityConnectSettings.asset joins the restore list; a player build
reserialises it into a 6000.5-only shape.
* build(kitchen-sink): keep the CLI pipeline package out of the published sample
`com.unity.pipeline` is agent tooling of this checkout -- it is what lets
Unity`s CLI drive an Editor we have open -- and b8225447 added it to the sample
that ships to users, along with an EditorPipelineManager asset under
Assets/Settings/Pipeline.
Dropping the package while copying the asset would have been worse than either:
Unity reports a dangling script reference as a broken component, not as a
missing package. So both go, and verify() asserts both, since the whole point of
prepare.mts is that the export cannot break in a way nothing here catches.
Also unpins biome.jsonc`s $schema from 2.5.5, which the 2.5.6 pin in
package.json made an error rather than a warning -- `pnpm check` is what CI runs.
* use auto engine type
* build(tests): add the CLI pipeline package, and make 6000.5 work
Two things, and the first is what exposed the second.
`com.unity.pipeline` in tests/ too, so Unity`s CLI can drive an Editor open on
it the way it already can for kitchen-sink. Verified end to end: the Editor comes
up ready on port 7801, exposes 142 commands, and
`unity command run_tests -- --mode EditMode --filter <name>` returned 6/6 across
all three engines as structured results, no results file involved.
Adding it also gave the resolver a reason to re-resolve from scratch, which
dropped every package back to the manifest`s minimums -- and `com.unity.inputsystem`
1.14.2 does not compile on 6000.5: nine CS0619s in its editor assemblies from the
EntityId migration (`GetInstanceID`, `GetAssetPath(int)`, `InstanceIDToObject(int)`),
which report zero tests rather than a red suite.
That is the whole of the "tests/ cannot run on 6000.5" story. 1.14.2 was only ever
a floor, and 6000.5 was stepping over it to 1.20.0 whenever the lockfile let it, so
whether a run worked depended on whether anything had invalidated the lock. The
floors are now raised past it -- inputsystem 1.20.0, test-framework.performance
3.5.0, testtools.codecoverage 1.3.0 -- each still declaring unity 6000.0 or older,
so CI can resolve them. test-framework, ugui and ext.nunit are left alone: they are
builtin, the editor supplies its own, and pinning the local answer is exactly how
you get the 6000-only manifest that will not resolve on 6000.1.
Green on 6000.5.9f1 with no UNITY_VERSION, which is why the version stamp moves
there -- 6000.1.4f1 is not installed on this machine, so the old stamp made every
command need an override:
EditMode 346/354 (8 skipped, 0 failed)
PlayMode 690/701 (11 skipped, 0 failed)
Both match the last known-good numbers exactly. CI at 6000.0.51f1/6000.1.9f1 is
still the only proof of the matrix, and the raised floors have only been measured
on 6000.5.
One flake worth knowing: the first run after the package graph changed died with
0xC0000005 inside Unity`s own EditorWindow.Close -> DockArea.RemoveTab ->
ContainerWindow.Close teardown, while it was also re-importing. The identical run
straight after was green and it has not come back.
Restore now reaches package assets, not just project ones. A PlayMode run empties
the glyph table of unity/core/Assets/Material Icons/Material Icons SDF - TMP.asset
-- 375 lines of metrics -- on its way to repopulating the dynamic atlas at runtime.
It belongs to no project, so nothing project-relative was reaching it, and it
*ships*: committing it publishes a font with no glyphs. Both suite runs above
reported restoring it.
* refactor(unity): delete the agent bridge, Unity's CLI covers it
686 lines of C# in their own asmdef plus a 219-line client, replaced by a package
dependency both projects now have. Every action had an equivalent, verified against
a live Editor rather than assumed: status -> editor_status, logs -> console,
refresh -> recompile, test -> run_tests, play/stop -> editor_play/editor_stop,
screenshot -> screenshot + capture_game_view/capture_scene_view, menu -> menu.
The Pipeline package goes far past that -- build, eval, player settings, target
switching, package add/remove, the scene and asset surface, and a --runtime mode
that attaches to a running development player, which is a thing the bridge could
never have done. run_tests even returns a structured summary plus a row per test,
so it needs no results file: 6/6 across all three engines on a filtered EditMode
run through the open tests/ Editor.
Two things genuinely lost, both small and both now documented. `quit` had deferred
its response a few frames so the client got an answer before the process went away;
`eval "EditorApplication.Exit(0);"` does quit the Editor but reports
COMMAND_FAILED because the server dies mid-reply, so confirm with `unity status`.
And a repo-specific action in a live Editor is now a [CliCommand] static method
rather than a handler here -- which is the better shape anyway, since `unity list`
discovers it with no release of anything.
What did *not* move is batch mode. `unity test` and `unity build` open the Editor
without snapshotting the files it rewrites for having been opened, and on this repo
that upgrades tests/Packages/manifest.json into a shape CI cannot resolve -- zero
tests reported as a pass. compile, test and player stay here for that reason alone.
`pnpm unity compile tests` is clean with the asmdef gone. The generated
ReactUnity.Editor.AgentBridge.csproj and the .slnx that referenced it are deleted
too; both are gitignored and Unity regenerates them.
* docs(quickjs): close the IL2CPP exit criterion
Phase 3 asked for a desktop IL2CPP player build and got "blocked on tooling" --
no installed editor had the IL2CPP player variation, and scripts/unity had no
batch-mode build entry point. Both are gone: the module is installed, and
`pnpm unity player` builds the player and probes the engines inside it.
QuickJS and Jint pass all seven probe checks under IL2CPP on Windows x64, and
the same player built as Mono passes with ClearScript alongside them. The
section that argued no new stripping surface was introduced now reports a
measurement instead.
Left open deliberately, and said so in both the phase note and the risk: this
covers Windows x64 only. Android and iOS have their own stripping and their own
P/Invoke conventions, and nothing has built those artifacts yet.
* docs: changelog entry for the quickjs-ng migration
* build(quickjs): install the ng build for Linux x64
CI is ubuntu-latest, so it loads Plugins/QuickJS/x64/libquickjs.so -- which was
still the Bellard-era binary from unity-jsb. Every QuickJS fixture died at
BeforeTest with `EntryPointNotFoundException: JS_SetProperty`, on all four matrix
jobs, which is the same reasoning phase 3 used in reverse: JS_GetProperty and
JS_SetProperty are `static inline` in Bellard`s and absent from its exports, the
C# binds both directly, so the old binary cannot satisfy it.
Built from native/quickjs in WSL: 241 atoms and 0 failures from shim-test, and
104 of 104 live P/Invoke names satisfied against the ELF. Nine of the eleven
shipped artifacts are still the old engine and will fail the same way on their
platforms -- Windows x86, four WSA, three Android, iOS, macOS.
check-exports.py could not report that honestly until now. `have_projects()`
asked whether Unity had generated the .csproj, and Unity writes an absolute path
for every `<Compile Include>` outside the project -- which is all of the quickjs
C#. Read the same tree through /mnt and none of those resolve, so the live set
came back missing most of the surface and the drift guard named every function in
it: a report that reads like a regression and is not one. It now also requires
that at least one of those paths resolve, and falls back to the committed
pinvoke-native.txt when they do not. Both paths agree at 104/104 -- the live one
against the Windows DLL, the committed one against the Linux .so.
* build(quickjs): install the ng builds CI could produce, fix the three legs it could not
First real run of native-quickjs.yml. Six of eleven legs green, and the failures
were all in the workflow rather than the port.
Installed, each verified at 104 of 104 live P/Invoke names:
Android arm64-v8a, armeabi-v7a built by the NDK, no glibc dependency
macOS universal bundle 241 atoms and 104/104 checked on the Mac itself
Not installed, and why:
linux-x64 built fine, records a GLIBC_2.38 floor, and the Unity test
container is older -- so Unity could not load it at all. That is
what turned the earlier EntryPointNotFoundException into
DllNotFoundException; right engine, unloadable binary. The .so in
the tree is my WSL build with the same problem, left there rather
than reverted to the old engine, because the next run replaces it.
Android x86 still the old engine. Unity 6 does not offer 32-bit x86 for
Android and the matrix builds x86_64 instead, but that is a new
path needing a .meta Unity has to generate, so it is a separate
step rather than a hand-written importer file.
Three workflow fixes:
- **The Windows and WSA legs never configured.** `-G "Visual Studio 17 2022"`,
and windows-latest ships VS 2026 now: "could not find any instance of Visual
Studio". The generator is gone entirely; `-A <arch>` is all those legs need and
CMake picks the newest it finds, which does not go stale again.
- **linux-x64 now builds in an ubuntu:22.04 container.** glibc is backward
compatible and never forward, so the floor a binary records is the oldest system
it loads on, and a runner-native build on 24.04 records 2.39. 22.04 gives 2.35.
Worth knowing: the binary this replaces asked for 2.17, so users on anything
older than 22.04 lose support -- an older base is the fix if that matters.
- **iOS reached libtool with `${EFFECTIVE_PLATFORM_NAME}` still in the path.**
Under the Xcode generator TARGET_FILE expands to a path holding that
placeholder, which Xcode substitutes in the script phase`s shell -- and VERBATIM
escapes the `$` so it never gets the chance. The merge step is no longer
VERBATIM. This is the step that stops iOS shipping an archive with the shim
alone and no engine in it, so it stays untested until that leg goes green.
* build(quickjs): read Mach-O archives, and give the container a python
Ten of twelve legs green after the last round. Both stragglers were tooling.
**iOS built, and the merge worked** -- 591 symbols in the archive, where an
unmerged one would hold only the shim`s handful. The exports check then matched
none of the 104 wanted names, because the underscore strip was keyed on the
detected format being MACHO and a Mach-O *static archive* detects as ARCHIVE.
Mach-O prefixes every C symbol with an underscore, so 104 of 104 were being
compared against `_JS_NewRuntime` and friends. Both spellings are now recorded,
which is additive: the PE, ELF and Mach-O readers all still report 104/104.
That check is the only thing standing between us and an iOS archive holding the
shim and no engine, so it mattered that it was failing loudly rather than
passing -- a silent version of this bug would have shipped.
**linux-x64 got as far as the exports check and found no `python`.** A bare
ubuntu:22.04 has neither, and the shared step calls `python`, so the container
now installs python3 and python-is-python3.
Unity Tests is still red, still 1662 DllNotFoundException and nothing else: the
Linux .so in the tree is the GLIBC_2.38 build. The container leg replaces it once
that run goes green.
* build(quickjs): install the ng builds for Windows x86, WSA, iOS and Linux
native-quickjs.yml is green on all twelve legs, so these come from CI rather than
from someone`s machine. Every one checked at 104 of 104 live P/Invoke names, on a
platform that can read its format: PE here, ELF in WSL, Mach-O and the archive on
the macOS runners.
iOS is the one worth pointing at. 1182 symbols in the archive and 104 of 104
satisfied -- the first time that artifact has been verified rather than assumed.
An unmerged archive holds the shim alone, and the check now catches that.
Linux comes back at GLIBC_2.35 instead of 2.38, which is the container leg doing
its job. That is the file Unity Tests has been failing on: 1662
DllNotFoundException with no other cause behind it.
Committing this needed the .gitignore fix that came with it. The Visual Studio
template`s `[Xx]64/` and `[Xx]86/` rules match *four* directories under
Plugins/QuickJS, not the two phase 3 negated -- WSA/x64 and WSA/x86 as well as
the desktop pair. Tracked files survive an ignored directory, so nothing looked
wrong until `git add` was handed the directory itself, which is what lint-staged
does: "The following paths are ignored", hook fails, commit refused, and biome
had already passed. The negations use `**/` now. Worth knowing while diagnosing
the next one: `git check-ignore <dir>` reports "not ignored" for these while
`git add` refuses them; only `--no-index` with a trailing slash tells the truth.
Two things this does not cover.
`x64/quickjs.dll` is still the locally-built ng DLL rather than the CI one --
same source, verified 104/104, but the Editor open on kitchen-sink holds the file
and Windows will not let it be replaced. One `cp` once that Editor is closed;
until then the Windows binary is the only one whose provenance is a laptop.
Two old-engine binaries remain, both for targets Unity 6 no longer offers:
WSA/ARM (the Windows SDK dropped 32-bit ARM) and Android/libs/x86 (the matrix
builds x86_64 instead). Removing those and adding x86_64 is a separate change: a
new Android plugin needs `CPU: X86_64` on its importer, which is Unity`s to
generate rather than mine to hand-write.
* build(quickjs): finish the plugin set -- x64 from CI, x86_64 in, two dead targets out
Closes the three gaps the last artifact commit listed.
`x64/quickjs.dll` now comes from CI like the other ten. Same source as the local
build it replaces and the same 104 of 104, but the provenance is a workflow run
rather than a laptop, which is what the exit criterion asks for. It only needed
the Editor holding the file to be closed.
**Android x86_64 added**, at 104 of 104. Unity 6 targets that ABI; it was missing
because unity-jsb shipped 32-bit `x86` instead, which Unity 6 does not offer at
all. Adding a native plugin is not a file copy -- it needs `CPU: X86_64` on the
importer, and a batch import writes only a stub .meta with a guid and no
PluginImporter block. So this went through a live Editor:
`unity command eval_file` against PluginImporter, reporting back
`android=True cpu=X86_64 any=False`. Hand-writing that .meta would have been the
wrong tool; note the generated one uses 6000.5`s newer platformData shape rather
than the older list form the arm64 and armeabi metas still carry, which Unity
migrates on its own whenever those are reimported.
**WSA/ARM and Android/libs/x86 deleted.** Both were still Bellard-era binaries,
and both are for targets Unity 6 no longer offers -- the Windows SDK dropped
32-bit ARM (MSB8087) and Andr…1 parent 1dc5436 commit 08193cf
335 files changed
Lines changed: 31499 additions & 139472 deletions
File tree
- .claude/skills/unity
- .github/workflows
- .tegami
- kitchen-sink
- Assets
- Scenes
- Settings
- Pipeline
- Packages
- ProjectSettings
- react
- native/quickjs
- src
- scripts
- kitchen-sink
- test-transformer
- unity
- tests
- Assets
- Settings
- Pipeline
- Packages
- ProjectSettings
- unity
- core
- Assets/Material Icons
- Editor
- Developer
- AgentBridge
- Resources/ReactUnity/tests/scripts
- Runtime
- Developer
- Scripting
- QuickJS
- Tests
- Editor
- Core
- Scripting
- Runtime
- Base
- Utils
- quickjs
- Plugins/QuickJS
- Android/libs
- arm64-v8a
- armeabi-v7a
- x86_64
- x86
- WSA
- ARM64
- ARM
- x64
- x86
- WebGL
- .source
- typings
- iOS
- quickjs.bundle/Contents/MacOS
- x64
- x86
- Runtime/Source
- Binding
- Editor
- BindingCallback
- BindingInfo
- Codegen
- ValueTypes
- Module
- Native
- Unity
- Callbacks
- Editor
- CustomBindings
- Hotfix
- Editor
- Plugins
- Utils
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
0 commit comments