Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
2ed5ccf
test(opcache): prove BinaryCacheFile::refresh() semantics under real …
claude Aug 19, 2026
2421023
fix(opcache): invalidate before writing in BinaryCacheFile::refresh()
claude Aug 19, 2026
f477970
feat(reflection): copy literals with opcodes to lift the IS_CONST rea…
claude Aug 19, 2026
c720776
feat(gen): export the zend_inheritance_cache_get/add hook points
claude Aug 19, 2026
4795077
feat(core): decline inheritance-cache publication for handler-hooked …
claude Aug 19, 2026
22d1c48
Generate windows FFI engine definitions for PHP 8.4 (#59)
github-actions[bot] Aug 19, 2026
789e007
Merge pull request #251 from lisachenko/claude/opcache-125-shm-refres…
lisachenko Aug 19, 2026
13d5bab
fix(reflection): plateau redefine under opcache - inline caveat + fla…
claude Aug 19, 2026
eae5311
Generate darwin FFI engine definitions for PHP 8.4 (#58)
github-actions[bot] Aug 19, 2026
0a3a4b6
Merge pull request #253 from lisachenko/claude/opcache-252-refresh-or…
lisachenko Aug 19, 2026
8aeab91
Merge branch '8.4' into claude/opcache-241-decline-inheritance-cache
lisachenko Aug 19, 2026
b9a4a52
Merge pull request #254 from lisachenko/claude/opcache-131-literal-reach
lisachenko Aug 19, 2026
b407495
Merge pull request #255 from lisachenko/claude/opcache-241-decline-in…
lisachenko Aug 19, 2026
79f7cbe
feat(opcache): relocate intersection/union type lists in the file-cac…
claude Aug 19, 2026
22a27f4
feat(gen): export zend_deserialize_opcode_handler
claude Aug 19, 2026
9394c05
feat(opcache): apply patched cache images to already-loaded code
claude Aug 19, 2026
949a3fd
Merge remote-tracking branch 'origin/8.4' into claude/opcache-122-hot…
claude Aug 19, 2026
c191ed5
Generate windows FFI engine definitions for PHP 8.4 (#59)
github-actions[bot] Aug 19, 2026
4ef76c7
Generate darwin FFI engine definitions for PHP 8.4 (#58)
github-actions[bot] Aug 19, 2026
35c901d
Merge pull request #256 from lisachenko/claude/opcache-112-relocate-t…
lisachenko Aug 19, 2026
13e9754
feat(opcache): relocate trait names, aliases and precedences in class…
claude Aug 19, 2026
53842e5
test(opcache): make the CacheImageSync plain legs hermetic
claude Aug 19, 2026
c6c083e
Merge pull request #257 from lisachenko/claude/opcache-114-relocate-t…
lisachenko Aug 19, 2026
a756aad
feat(opcache): relocate dynamic function definitions (closures/arrow …
claude Aug 19, 2026
40975ff
Merge pull request #258 from lisachenko/claude/opcache-115-relocate-c…
lisachenko Aug 19, 2026
2da4dad
feat(opcache): relocate iterator and ArrayAccess func structs in clas…
claude Aug 19, 2026
2e437de
Merge pull request #259 from lisachenko/claude/opcache-116-relocate-i…
lisachenko Aug 19, 2026
040e736
feat(opcache): relocate property-hook op_arrays in the file-cache pay…
claude Aug 19, 2026
12f4829
Merge pull request #260 from lisachenko/claude/opcache-113-relocate-p…
lisachenko Aug 19, 2026
158dbac
Merge pull request #262 from lisachenko/claude/opcache-242-redefine-u…
lisachenko Aug 19, 2026
68dddd5
ci: skip Homebrew auto-update/cleanup on the macOS setup-php jobs
claude Aug 19, 2026
1a62dd2
Merge remote-tracking branch 'origin/8.4' into claude/opcache-122-hot…
claude Aug 19, 2026
aae5e66
Merge pull request #272 from lisachenko/claude/opcache-ci-homebrew-sp…
lisachenko Aug 20, 2026
78f2f20
Merge pull request #266 from lisachenko/claude/opcache-122-hotswap-wi…
lisachenko Aug 20, 2026
fb8bda9
feat(opcache): support ZTS file-cache payloads in the relocator
claude Aug 19, 2026
a20e806
feat(opcache): pin down the darwin opline addressing model with a tri…
claude Aug 19, 2026
08ed9b7
feat(opcache): persist-from-graph serializer for graph-growing mutations
claude Aug 19, 2026
7a1f13f
feat(opcache): bounds-validate relocation offsets and document the tr…
claude Aug 19, 2026
0057a39
refactor(opcache): replace path-scoped PHPStan ignores with struct sh…
claude Aug 19, 2026
225bbca
fix(opcache): resolve duplicate $script property from #122/#126 merge
claude Aug 20, 2026
e7c3973
Merge pull request #267 from lisachenko/claude/opcache-118-zts-relocator
lisachenko Aug 20, 2026
83678e3
Merge pull request #268 from lisachenko/claude/opcache-119-darwin-rel…
lisachenko Aug 20, 2026
b7a6d02
Merge pull request #269 from lisachenko/claude/opcache-117-graph-seri…
lisachenko Aug 20, 2026
8b94bfc
Merge pull request #270 from lisachenko/claude/opcache-123-bounds-val…
lisachenko Aug 20, 2026
3d540c9
Merge pull request #271 from lisachenko/claude/opcache-126-phpstan-sh…
lisachenko Aug 20, 2026
31c5f31
merge: cascade 8.4 into master (opcache epic reconciled for PHP 8.5)
claude Aug 20, 2026
93fcd8d
fix(opcache): teach the graph serializer the PHP 8.5 constant-express…
claude Aug 20, 2026
dd4227a
Generate windows FFI engine definitions for PHP 8.5 (#59)
github-actions[bot] Aug 20, 2026
9e0ab94
Generate darwin FFI engine definitions for PHP 8.5 (#58)
github-actions[bot] Aug 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 21 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ jobs:
opcache_gate: composer test:opcache
- ts: zts
ts_label: ZTS
# The file-cache relocator does not support ZTS payloads yet
# (issue #118): its tests self-skip on ZTS, so the non-skip gate
# excludes that group while still proving the SHM tests ran
# Since issue #118 the file-cache relocator supports ZTS payloads,
# so this gate covers the full opcache group (the script is the
# named alias the ZTS legs call; it no longer excludes anything)
opcache_gate: composer test:opcache-zts
steps:
- uses: actions/checkout@v7
Expand Down Expand Up @@ -127,6 +127,14 @@ jobs:
tests-macos:
name: Tests (macOS ${{ matrix.runner-arch.arch }}, ${{ matrix.ts }})
runs-on: ${{ matrix.runner-arch.runner }}
# setup-php installs PHP through Homebrew on macOS, and by default brew runs
# a full `brew update` before every install and a cleanup after it - tens of
# seconds of unrelated formula churn on top of the PHP install this job
# needs. Both are pure overhead here (the runner image already ships a recent
# brew and nothing else is installed), so switch them off for the whole job.
env:
HOMEBREW_NO_AUTO_UPDATE: '1'
HOMEBREW_NO_INSTALL_CLEANUP: '1'
strategy:
fail-fast: false
matrix:
Expand Down Expand Up @@ -195,8 +203,8 @@ jobs:
run: composer test

# Same gates as the Linux legs (issue #124): the opcache/SHM tests must
# have RUN. As on Linux, the ZTS leg excludes the file-cache relocator
# group (no ZTS payload support yet, issue #118).
# have RUN. Since issue #118 the relocator group runs on ZTS too, so
# both legs cover the full opcache group.
- name: Opcache/SHM coverage must not silently skip
if: steps.artifacts.outputs.present == 'true'
run: ${{ matrix.ts == 'zts' && 'composer test:opcache-zts' || 'composer test:opcache' }}
Expand Down Expand Up @@ -376,12 +384,12 @@ jobs:
include:
- ts: nts
ts_label: NTS
# ZTS excludes the relocator tests (no ZTS payload support yet,
# issue #118) but still gates the SHM coverage against silent skips
opcache_args: --group opcache --fail-on-skipped
- ts: zts
ts_label: ZTS
opcache_args: --group opcache --exclude-group opcache-relocator --fail-on-skipped
# Since issue #118 the relocator tests run on ZTS too - both legs
# gate the full opcache group against silent skips
opcache_args: --group opcache --fail-on-skipped
steps:
- uses: actions/checkout@v7

Expand Down Expand Up @@ -603,6 +611,11 @@ jobs:
header-drift-darwin:
name: Generated darwin headers up to date (${{ matrix.runner-arch.arch }}, ${{ matrix.ts }})
runs-on: ${{ matrix.runner-arch.runner }}
# See tests-macos: skip brew's auto-update/cleanup so setup-php only pays for
# the PHP install it actually needs.
env:
HOMEBREW_NO_AUTO_UPDATE: '1'
HOMEBREW_NO_INSTALL_CLEANUP: '1'
strategy:
fail-fast: false
matrix:
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/generate-darwin-headers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ env:
jobs:
generate:
name: Generate darwin-${{ matrix.runner-arch.arch }}-${{ matrix.ts }}
# setup-php installs PHP through Homebrew on macOS; brew's default
# pre-install update and post-install cleanup are tens of seconds of
# unrelated churn this generate step does not need. Switch them off.
env:
HOMEBREW_NO_AUTO_UPDATE: '1'
HOMEBREW_NO_INSTALL_CLEANUP: '1'
strategy:
fail-fast: false
matrix:
Expand Down
14 changes: 8 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,8 @@ PRs), or manually via `workflow_dispatch` against any branch.
Darwin covers **NTS and ZTS**: the workflow's matrix crosses both
architectures with both thread-safety modes (setup-php builds the ZTS PHP via
`phpts: ts`). As on Linux, the ZTS artifacts reach EG/CG through the TSRM
offsets, and the opcache file-cache relocator stays unsupported on ZTS
(issue #118). The 8.4 artifacts are maintained on the `8.4` branch;
offsets; the opcache file-cache relocator runs on ZTS since issue #118.
The 8.4 artifacts are maintained on the `8.4` branch;
`include/8.5/darwin-*` is maintained here - after changing the generator,
refresh it with one `workflow_dispatch` run of the workflow on `master`.
A leg whose thread-safety mode setup-php cannot provide (currently ZTS
Expand Down Expand Up @@ -197,10 +197,12 @@ composer test:internal # destructive/segfault-prone group, process-isolated
- `ZENGINE_STRICT_LAYOUT_CHECK=1` (set in the test bootstrap) makes
`Core::init()` verify every struct layout against `layouts.json` before
touching engine memory — the anti-segfault airbag. Keep it on in development.
- On **ZTS** builds the file-cache relocator tests (`opcache-relocator` group)
self-skip — ZTS payloads are not supported yet (issue #118). The non-skip
gate for the remaining opcache/SHM coverage is `composer test:opcache-zts`;
CI runs both release and debug test legs on NTS **and** ZTS.
- The file-cache relocator supports **ZTS** payloads since issue #118 (the
binary layout is thread-safety-agnostic: zend_file_cache.c has no ZTS
conditionals and every walked struct is layout-identical across the modes).
`composer test:opcache-zts` stays as the named alias CI's ZTS legs call; it
now runs the full opcache group, relocator tests included. CI runs both
release and debug test legs on NTS **and** ZTS.
- `composer test:opcache-runner` runs the suite the way an opcache-enabled
consumer does — `opcache.enable_cli=1` in the **runner process itself**, so
every test file is compiled into shared memory (CI has a dedicated Linux job
Expand Down
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"test": "phpunit",
"test:internal": "phpunit --group internal --process-isolation",
"test:opcache": "phpunit --group opcache --fail-on-skipped",
"test:opcache-zts": "phpunit --group opcache --exclude-group opcache-relocator --fail-on-skipped",
"test:opcache-zts": "phpunit --group opcache --fail-on-skipped",
"test:opcache-runner": "phpunit --exclude-group performance --exclude-group internal --exclude-group opcache-incompatible",
"test:performance": "phpunit --group performance --fail-on-skipped",
"phpstan": "phpstan analyse",
Expand All @@ -53,7 +53,7 @@
"test": "Run the test suite (segfault-prone internal group excluded)",
"test:internal": "Run the segfault-prone internal test group with process isolation (use a debug PHP build)",
"test:opcache": "Run the opcache/shared-memory tests and FAIL if any of them skipped (they are self-skipping when opcache is unavailable)",
"test:opcache-zts": "Same non-skip gate for ZTS builds: excludes the file-cache relocator tests, which do not support ZTS payloads yet (issue #118)",
"test:opcache-zts": "Same non-skip gate on ZTS builds - kept as the named alias CI's ZTS legs call; since issue #118 the file-cache relocator tests run there too",
"test:opcache-runner": "Run the suite for an opcache-ACTIVE runner (opcache.enable_cli=1): excludes the usual performance/internal groups plus opcache-incompatible, the issue-linked tests that cannot hold when the runner's own files live in shared memory",
"test:performance": "Run the excluded performance group (the 'zero FFI at call time' benchmark) and FAIL if it silently skipped - its verdict is timing-based, so the CI leg that runs it is informational",
"phpstan": "Run static analysis at the maximum level",
Expand Down
60 changes: 38 additions & 22 deletions docs/class-specialization.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,34 +115,50 @@ two cases, and both are rejected rather than silently unenforced:

### Un-sharing the opcode array

When a plain `ZEND_RECV` has to be patched, the method's opcodes are copied into request memory
first, because they are shared with the template by design. Three operand encodings matter:
When a plain `ZEND_RECV` has to be patched, the method's opcodes **and literals** are copied
into request memory first, because they are shared with the template by design. The copy
reproduces the engine's own `pass_two()` layout in one block - opcodes at the start, the
literal zvals at the same 16-aligned offset right behind them. Three operand encodings matter:

| Operand | Encoding | Survives the copy? |
|---|---|---|
| jump targets | *signed* byte offset from the opline itself | yes - the whole array moves as a unit, so relative distances are unchanged |
| `IS_CONST` operands | byte offset **from the opline itself** | no - every one is rebased by the distance the array moved |
| `IS_CONST` operands | byte offset **from the opline itself** | no - every one is rebased onto the copied literal at the same index |
| `live_range`, `try_catch_array` | opline indices | yes |

Literals sit immediately after the opcodes in one compiler-arena block, which is why a constant
operand is opline-relative and why moving the array alone would silently make every literal
reference point at the wrong zval. Under `zend.assertions=1` the copy is verified: every
`IS_CONST` operand must resolve to the same address it resolved to before the move, and every
jump offset must still land inside the array.

An `IS_CONST` operand stores a **signed 32-bit** offset, so the relocated opcodes have to stay
within 2GB of the literals they point at. Request memory and the compiler arena are neighbours,
so this holds for an ordinary class - but an **opcache-shared body** lives in an mmap'd region
that can be arbitrarily far from the request heap, and a truncated offset would read whatever sat
at the wrapped address. That case is detected and rejected: substituting a *builtin parameter*
type needs a body that is not in shared memory. Class-like parameters, all return types and all
properties are unaffected, because none of them un-shares the opcodes.

Ownership mirrors the duplicated `arg_info` blocks - `destroy_op_array()` frees whichever
`opcodes` pointer its holder carries once the shared body refcount reaches zero, so one sibling
block is released through the engine and the other is reclaimed by the request allocator at
request end. Bounded at one block per patched method, and only methods that actually need a
patch pay it. An opcache-shared source is safe because it is only ever read.
A constant operand is opline-relative because opcodes and literals normally share one
compiler-arena block, and it stores a **signed 32-bit** offset - which is exactly why the
literals travel with the opcodes. The source literals can be arbitrarily far from the
relocated opcodes (an **opcache-shared body** lives in an mmap'd region well over 2GB from
the request heap, where a truncated offset would read whatever sat at the wrapped address),
but with both halves copied into one block every rebased offset is bounded by the block size
and always fits. Opcache-shared bodies are therefore fully supported. Under
`zend.assertions=1` the copy is verified: every `IS_CONST` operand must resolve to the copied
literal at the very index its source operand resolved to (landing zval-aligned inside the
copied table), and every jump offset must still land inside the array.

The literal zvals are copied **shallowly**: both blocks reference the same payloads (strings,
arrays, ASTs), matching how the engine treats the two of them as one shared body - releases
happen only when the shared body refcount reaches zero, so exactly one dtor pass ever runs
over exactly one of the sibling zval arrays. An opcache-shared source never reaches that pass
at all: its body refcount pointer is NULL, `destroy_op_array()` returns before touching
literals, and the immortal shared-memory payloads (interned strings, immutable arrays) are
never refcounted.

One more thing rides on the un-shared copy: opline **handlers**. Opcache's optimizer assigns
a `mixed` parameter's RECV (cached mask exactly `MAY_BE_ANY`) the `RECV_NOTYPE` handler
variant, which never reads the cached mask - so after writing the new mask, the specializer
also rebinds the patched opline to the engine's generic, mask-checking handler (taken from a
donor opline that can never be NOTYPE-specialized), exactly what the compiler assigns when a
builtin parameter type is written in source.

Ownership mirrors the duplicated `arg_info` blocks - with relative `IS_CONST` addressing the
engine frees literals and opcodes as ONE allocation through the `opcodes` pointer (it never
`efree()`s `literals` separately once `ZEND_ACC_DONE_PASS_TWO` is set, which this block layout
is built for), so one sibling block is released through the engine once the shared body
refcount reaches zero and the other is reclaimed by the request allocator at request end.
Bounded at one block per patched method, and only methods that actually need a patch pay it.
An opcache-shared source is safe because it is only ever read.

### Rejections

Expand Down
Loading
Loading