diff --git a/Jint/AGENTS.md b/Jint/AGENTS.md index cda1fe9a43..d3a5456a9d 100644 --- a/Jint/AGENTS.md +++ b/Jint/AGENTS.md @@ -46,6 +46,10 @@ Jint is AOT-compatible for .NET 7.0+ targets (`IsAotCompatible` is set for net7. ### What counts as a public contract +**A change to any of it is a row in [`docs/v5-migration.md`](../docs/v5-migration.md), written in the same pull request.** +That includes a change that breaks nothing at compile time — a flipped default, a narrowed lane, a message that stops +being detailed. A compiler cannot find those, so the guide is the only place an embedder can. + | Surface | Location | | --- | --- | | `ObjectInstance` overridable virtuals — `GetOwnProperty`, `HasProperty`, `Delete`, `DefineOwnProperty`, `GetOwnPropertyKeys`, `GetOwnProperties`, `RemoveOwnProperty`, `PreventExtensions`, `TryGetProperty`, `Initialize`, plus the `protected internal` `TryGetOwnPropertyValue`, `ProbeOwnProperty`, `SetOwnProperty`, `GetPrototypeOf` | `Jint/Native/Object/ObjectInstance.cs` | diff --git a/README.md b/README.md index cc5e77138d..52999f64fa 100644 --- a/README.md +++ b/README.md @@ -3633,6 +3633,20 @@ timeout and block that thread indefinitely. ## Branches and releases -- The recommended branch is __main__, any PR should target this branch -- The __main__ branch is automatically built and published on [MyGet](https://www.myget.org/feed/Packages/jint). Add this feed to your NuGet sources to use it: https://www.myget.org/F/jint/api/v3/index.json -- The __main__ branch is occasionally published on [NuGet](https://www.nuget.org/packages/jint) +Three branches are live, and which one a change belongs on depends on whether it is allowed to change +observable behaviour. + +| Branch | What it is | What lands on it | +| --- | --- | --- | +| __main__ | Development of the next major version, __5.x__. The default target for a pull request. | Anything, including breaking changes to the public API and to default behaviour. | +| __4.x__ | Maintenance of the __4.16.x__ line, branched from `main` at the last commit before the 5.x work began. | Correctness and conformance fixes backported from `main`, and nothing that changes an existing API or an existing default. | +| __3.x__ | Maintenance of the __3.x__ line, which still parses with Esprima rather than Acornima. | Security and severe-correctness fixes only. | + +- Open a pull request against __main__ unless the change is a backport, in which case target the + maintenance branch it belongs to +- __main__, __4.x__ and __3.x__ are automatically built and published on [MyGet](https://www.myget.org/feed/Packages/jint). Add this feed to your NuGet sources to use it: https://www.myget.org/F/jint/api/v3/index.json +- A release to [NuGet](https://www.nuget.org/packages/jint) is cut by pushing a `vX.Y.Z` tag to the + branch being released; the tag, not `Directory.Build.props`, is the version the package carries + +Upgrading from 4.16.x to 5.x: [docs/v5-migration.md](docs/v5-migration.md) records every change an +embedder has to react to, including the ones that break without a signature change. diff --git a/docs/v5-migration.md b/docs/v5-migration.md new file mode 100644 index 0000000000..49277b5e35 --- /dev/null +++ b/docs/v5-migration.md @@ -0,0 +1,340 @@ +# Migrating from Jint 4.16 to Jint 5 + +Jint 5 is under development on `main`. This document is the running record of every change a +4.16.x embedder has to react to; it is written for someone upgrading, so it says what broke and +what to type instead, and leaves the rationale to the linked pull requests. + +**v5 is not released yet.** `main` is a moving target and rows may still be added, amended or +withdrawn before it ships. Anything not listed here is intended to keep working unchanged. + +- [1. Target frameworks](#1-target-frameworks) +- [2. Removed API](#2-removed-api) +- [3. Renamed and reshaped API](#3-renamed-and-reshaped-api) +- [4. Breaking without a signature change](#4-breaking-without-a-signature-change) +- [5. New in v5](#5-new-in-v5) +- [6. AOT](#6-aot) +- [Keeping this document current](#keeping-this-document-current) + +## 1. Target frameworks + +| Target | 4.16.x | 5.x | +| --- | --- | --- | +| `net462` | yes | **dropped** (planned) | +| `net472` | — | **added** (planned) | +| `netstandard2.0` | yes | yes | +| `netstandard2.1` | yes | yes | +| `net8.0` | yes | yes | +| `net10.0` | yes | yes | + +> **Status: planned, not yet on `main`.** `Jint/Jint.csproj` still lists +> `net462;netstandard2.0;netstandard2.1;net8.0;net10.0`. Update this section when the change lands. + +The `netstandard` and modern .NET targets are deliberately kept. `netstandard2.0` in particular is +what game engines and other embedded runtimes resolve against, and it costs far more to drop than +raising the .NET Framework floor does. + +A consumer pinned to `net462`, `net47` or `net471` stays on the **4.16.x** line, which continues to +receive correctness and conformance fixes on the `4.x` branch. + +.NET Framework support is expected to end with **v6**; treat v5 as the last major version that +carries a `net4x` target. + +## 2. Removed API + +*Nothing recorded yet.* No public member has been removed from `Jint` since `v4.16.0`. + +This table is filled by the pull request that removes the member. + +| Removed | Replacement | PR | +| --- | --- | --- | +| — | — | — | + +## 3. Renamed and reshaped API + +*Nothing recorded yet.* No public member has been renamed, and no public signature reshaped, since +`v4.16.0`. + +Entries here are before/after code, not prose: + +```c# +// 4.16.x +// (before) + +// 5.x +// (after) +``` + +## 4. Breaking without a signature change + +This is the section that matters most, because a compiler cannot find any of it. Every row below +compiles exactly as it did in 4.16 and behaves differently at run time. + +### 4.1 Changed defaults, at a glance + +| PR | Setting | 4.16.x | 5.x | Restore the 4.16 behaviour | +| --- | --- | --- | --- | --- | +| [#3054](https://github.com/sebastienros/jint/pull/3054) | `Interop.AllowWrite` | `true` | `false` | `options.AllowClrWrite()` | +| [#3056](https://github.com/sebastienros/jint/pull/3056) | `Interop.ArrayConversion` | `LiveView` | `Copy` | `options.Interop.ArrayConversion = ArrayConversionMode.LiveView;` | +| [#3057](https://github.com/sebastienros/jint/pull/3057) | `Constraints.StackOverflowGuard` | `false` | `true` | `options.Constraints.StackOverflowGuard = false;` | +| [#3058](https://github.com/sebastienros/jint/pull/3058) | `AgentCanSuspend` | `true` | `false` | `options.AgentCanSuspend = true;` | +| [#3051](https://github.com/sebastienros/jint/pull/3051) | script-visible CLR / module error text | detailed | redacted | `options.ExposeDetailedErrors()` | +| [#3052](https://github.com/sebastienros/jint/pull/3052) | namespace type discovery | implicit assembly search | closed allow-list | `options.AllowClr(typeof(YourType).Assembly)` | + +Each is expanded below. + +### 4.2 Projected CLR writes are disabled by default ([#3054](https://github.com/sebastienros/jint/pull/3054)) + +Script can no longer write through a wrapped CLR object — fields, properties, indexers, dictionary +entries, list and array elements. In sloppy mode the write is silently ignored; in strict mode it +raises a `TypeError`. Calling a CLR method or extension method that mutates host state is +unaffected: that is a capability the host handed out, not a projected write. + +```c# +// 4.16.x — `host.Count = 5` wrote through +var engine = new Engine().SetValue("host", host); + +// 5.x — the same engine now refuses that write; opt back in explicitly +var engine = new Engine(options => options.AllowClrWrite()) + .SetValue("host", host); +``` + +### 4.3 CLR array projection defaults to isolated copies ([#3056](https://github.com/sebastienros/jint/pull/3056)) + +`T[]` crossing into script is snapshotted into a real JavaScript array instead of being exposed as a +live, fixed-size view. Script-side mutations affect only the copy, and CLR-side mutations after the +conversion are not visible through it. (`LiveView` had itself only been the default since 4.14.) + +Two consequences a script can see directly: `Array.isArray` now answers `true` where the view +answered `false`, and `push`, `pop` and `length` writes now succeed on the copy where the fixed-size +view threw `TypeError`. + +```c# +// 5.x — opt back into the live view; write-through additionally needs AllowClrWrite() +var engine = new Engine(options => +{ + options.Interop.ArrayConversion = ArrayConversionMode.LiveView; + options.AllowClrWrite(); +}); +``` + +A copy costs per element on the crossing where a view cost a single wrapper, so a host projecting +large arrays on a hot path should measure before accepting the default. + +### 4.4 CLR type and member resolution is contained ([#3052](https://github.com/sebastienros/jint/pull/3052)) + +`Interop.AllowedAssemblies` is now a **closed** allow-list for namespace resolution. 4.16 fell back +to `Assembly.GetCallingAssembly()`, `Assembly.GetExecutingAssembly()` and `Type.GetType(name)`, so +`importNamespace` could reach types the host never named. Namespace lookup also admits only public +top-level types and nested types whose complete declaring-type chain is public, and the type-widening +`clrHelper` operations now require `Interop.AllowGetType`. + +```c# +// 4.16.x — the host assembly was searched implicitly +var engine = new Engine(options => options.AllowClr()); + +// 5.x — name the assemblies whose namespaces script may resolve +var engine = new Engine(options => options.AllowClr(typeof(MyModel).Assembly)); +``` + +`AllowClr()` with no arguments adds the assembly containing `object`, so `System.*` keeps working. +CLR objects, delegates and `TypeReference` values the host exports explicitly are capabilities in +their own right and are not subject to this policy. + +### 4.5 The stack-overflow guard is enabled by default ([#3057](https://github.com/sebastienros/jint/pull/3057)) + +`Options.Constraints.StackOverflowGuard` defaults to `true`, so an unbounded script recursion raises +`RangeError: Maximum call stack size exceeded` — an ordinary JavaScript error, catchable by the +script and by `catch (JavaScriptException)`, with the engine still usable — instead of exhausting the +native stack and ending the host process with no exception at all. + +It measures the remaining native stack rather than counting calls, which is what lets it cover the +routes a frame count cannot: `new`, a getter, a coercion, a Proxy trap, a host delegate calling back +in, and a recursion whose every level is a *different* function (`eval`, `new Function`). +`LimitRecursion(n)` answers a different question and takes precedence where it is configured. + +Measured cost: recursion-heavy workloads roughly 1.5–3% slower, hot shallow calls within run-to-run +noise. Turn it off only for trusted, independently bounded scripts: + +```c# +var engine = new Engine(options => options.Constraints.StackOverflowGuard = false); +``` + +See [Surviving an unbounded recursion](../README.md#surviving-an-unbounded-recursion). + +### 4.6 Atomics agent suspension is disabled by default ([#3058](https://github.com/sebastienros/jint/pull/3058)) + +`Atomics.wait` on a default engine throws a JavaScript `TypeError` before registering a waiter. +`Atomics.waitAsync` is unaffected and remains available. A script can call `Atomics.wait` with no +timeout, so on a request, UI or event-loop thread the previous default let a script block that +thread indefinitely. + +```c# +// worker-like host where blocking is acceptable +var engine = new Engine(options => options.AgentCanSuspend = true); +``` + +### 4.7 Concurrent `Engine` use is rejected ([#3035](https://github.com/sebastienros/jint/pull/3035)) + +Concurrent use of one `Engine` was always unsupported; it now fails fast with +`InvalidOperationException` instead of racing or appearing to work. Public host entries — execution, +mutation, modules, debugger state, conversion, the event loop — check ownership, and an engine stays +reserved for the whole lifetime of a returned async `Task`. + +What a host has to change: + +- **await before reuse.** `EvaluateAsync`, `ExecuteAsync`, `InvokeAsync`, `Modules.ImportAsync` and + `UnwrapIfPromiseAsync` must complete before the engine is returned to a pool, reused, or disposed. + `Dispose` fails fast while an operation owns the engine, which is observable during exception + unwinding — a `using` scope must not outlive an async engine operation. +- **no nested async entry.** Starting an async engine API from inside an active engine callback is + rejected before any work starts. +- **same-thread re-entry still works**, and a JavaScript callback converted to a CLR delegate may + still be dispatched from another thread inside one of the four callback-admission windows. + +The full contract, including those windows, is in +[Thread-safety](../README.md#thread-safety). + +### 4.8 Memory is accounted across async continuations ([#3036](https://github.com/sebastienros/jint/pull/3036)) + +`LimitMemory` now charges managed allocations to the engine operation across promise reactions, +event-loop jobs and asynchronous module completions, including ones that resume on a different +thread. A budget that only ever saw one synchronous segment in 4.16 can therefore trip where it +previously did not. `MemoryLimitAccuracy` reports what the accounting can and cannot see, and +`MemoryLimitConstraint.Begin`/`End` brackets one budget across a multi-entry host operation. + +### 4.9 Script-visible error text is redacted ([#3051](https://github.com/sebastienros/jint/pull/3051)) + +Host exception messages, module-loader failure messages and CLR resolution details are replaced with +generic text, because they routinely carry filesystem paths, URLs, connection strings and CLR type +names. Nothing host-side is lost: `JintException.TryGetClrException`, `TryGetClrType`, +`TryGetClrMemberName` and the CLR error decorators still see everything. + +```c# +// 5.x — restore the 4.16 development-friendly messages on all three surfaces +var engine = new Engine(options => options + .CatchClrExceptions() + .EnableModules(loader) + .ExposeDetailedErrors()); +``` + +Narrower opt-ins: `Interop.ExposeDetailedExceptionMessages`, `Interop.ExposeDetailedResolutionErrors` +(which already existed and already defaulted to `false`), `Modules.ExposeDetailedLoadErrors`. See +[Detailed development errors](../README.md#detailed-development-errors). + +### 4.10 `*Async` entries have one deterministic failure channel ([#3252](https://github.com/sebastienros/jint/pull/3252)) + +`EvaluateAsync` and `InvokeAsync` used to report parse errors, script throws and constraint failures +by throwing out of the call; `ExecuteAsync` and `Modules.ImportAsync` reported the identical failure +through the returned `Task`. Which one you got could even depend on thread scheduling, because a +host callback on another thread is charged to the same memory budget. A host could not write a +`catch` against that. + +The rule now, for the whole family: + +| Failure | Where it arrives | +| --- | --- | +| parse error, script throw, constraint tripping, promise rejection | the returned `Task` | +| `null` argument, a `Prepared