diff --git a/.github/scripts/download-php-windows-devel.sh b/.github/scripts/download-php-windows-devel.sh new file mode 100644 index 00000000000..14267a84ae2 --- /dev/null +++ b/.github/scripts/download-php-windows-devel.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# Shared by the Windows turbo jobs in phar.yml: exports the extension version +# (the short SHA of the last commit touching turbo-ext/src, the Makefile's +# computation) and fetches the php-devel-pack matching the installed PHP +# (MATRIX_TS: nts/zts, MATRIX_VS: its toolset infix) into C:\php-devel and the +# php-sdk-binary-tools at PHP_SDK_COMMIT into C:\php-sdk. +set -euo pipefail + +SHA=$(git log -1 --format=%H -- turbo-ext/src | cut -c1-7) +echo "extension version: $SHA" +echo "PHPSTANTURBO_VERSION=$SHA" >> "$GITHUB_ENV" + +FULL=$(php -r 'echo PHP_VERSION;') +# thread-safe devel packs carry no infix, NTS ones carry -nts +INFIX=$([ "$MATRIX_TS" = "zts" ] && echo "" || echo "-nts") +PACK="php-devel-pack-$FULL$INFIX-Win32-$MATRIX_VS-x64.zip" +echo "devel pack: $PACK" +curl -fsSLo devel-pack.zip "https://windows.php.net/downloads/releases/$PACK" \ + || curl -fsSLo devel-pack.zip "https://windows.php.net/downloads/releases/archives/$PACK" \ + || curl -fsSLo devel-pack.zip "https://downloads.php.net/~windows/qa/$PACK" \ + || curl -fsSLo devel-pack.zip "https://downloads.php.net/~windows/qa/archives/$PACK" +unzip -q devel-pack.zip -d /c/php-devel + +git clone -q https://github.com/php/php-sdk-binary-tools.git /c/php-sdk +git -C /c/php-sdk checkout -q "$PHP_SDK_COMMIT" diff --git a/.github/scripts/find-turbo-origins.sh b/.github/scripts/find-turbo-origins.sh index 7cfe818c604..5759b1b3c66 100755 --- a/.github/scripts/find-turbo-origins.sh +++ b/.github/scripts/find-turbo-origins.sh @@ -1,10 +1,12 @@ #!/usr/bin/env bash -# Finds, for every turbo extension binary the compile jobs in phar.yml -# produce, the newest earlier run on $BRANCH that compiled it from build -# inputs identical to HEAD's, and writes {"": } to -# the `origins` step output. A compile leg whose artifact is listed -# downloads it from that run instead of building it again (see the -# turbo-origins job in phar.yml). +# Finds, for every shared core the turbo-compile-core jobs in phar.yml +# produce (phpstan_turbo_core-), the newest earlier run on $BRANCH +# that compiled it from build inputs identical to HEAD's, and writes +# {"": } to the `origins` step output. A core leg +# whose artifact is listed downloads it from that run instead of building +# (and profiling) it again (see the turbo-origins job in phar.yml). The +# per-PHP-version extensions are not reused: they compile in a minute, +# against whichever core this run carries. # # The search walks HEAD's first-parent history (on a pull request's merge # commit, the base branch) and asks the API for the runs of each commit @@ -45,11 +47,12 @@ BUILD_INPUT_PATHS=( .github/turbo-build .github/scripts/install-alpine-php.sh .github/scripts/install-php86-windows.sh + .github/scripts/download-php-windows-devel.sh ) WORKFLOW_PATH=".github/workflows/phar.yml" -# Workflow-level env and the three compile jobs, as JSON: comment-only edits -# do not change it. -COMPILE_JOBS_QUERY='[.env, .jobs["turbo-compile"], .jobs["turbo-compile-musl-arm64"], .jobs["turbo-compile-windows"]]' +# Workflow-level env and the three core compile jobs, as JSON: comment-only +# edits do not change it. +COMPILE_JOBS_QUERY='[.env, .jobs["turbo-compile-core"], .jobs["turbo-compile-core-musl-arm64"], .jobs["turbo-compile-core-windows"]]' # Commits whose runs are looked up through the API, newest first. The # commits git alone rules out do not count. COMMITS_LIMIT=50 @@ -123,7 +126,7 @@ while read -r sha; do fi if ! origins="$(jq -c --argjson run "$run_id" --arg artifacts "$artifacts" ' ($artifacts | split("\n")) as $names - | reduce ($names[] | select(startswith("phpstan_turbo-"))) as $name (.; + | reduce ($names[] | select(startswith("phpstan_turbo_core-"))) as $name (.; if has($name) or ($names | any(. == "turbo-reused-" + $name)) then . else .[$name] = $run end) ' <<< "$origins")"; then origins='{}' diff --git a/.github/scripts/turbo-dist-layout.sh b/.github/scripts/turbo-dist-layout.sh new file mode 100755 index 00000000000..6186b0a1a2c --- /dev/null +++ b/.github/scripts/turbo-dist-layout.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# Maps the turbo compile artifacts of a phar.yml run onto the layout +# phpstan/phpstan ships (turbo-ext//...), printing one +# "" line per distributed file. +# +# An artifact phpstan_turbo--php[-zts] carries the extension +# of that PHP version (phpstan_turbo.so, php_phpstan_turbo.dll) and the +# platform's shared core next to it (phpstan_turbo_core*.so/.dll), which the +# extension loads from its own directory: +# extension -> /phpstan_turbo-[-zts]. +# core -> / +# Every leg of a platform links against the one core turbo-compile-core +# built for it, so each of its artifacts carries the same core: it is listed +# once, and different bytes under one name fail the mapping — such a set +# would ship extensions next to a core they were not linked against. +# +# Usage: turbo-dist-layout.sh +set -euo pipefail + +artifacts=$1 +declare -A core_source=() +status=0 +mapped=0 + +for dir in "$artifacts"/phpstan_turbo-*; do + [ -d "$dir" ] || continue + name="${dir#"$artifacts"/phpstan_turbo-}" + target="${name%-php*}" + minor="${name##*-php}" + for file in "$dir"/*; do + [ -f "$file" ] || continue + base="${file##*/}" + case "$base" in + phpstan_turbo_core*) + dest="$target/$base" + if [ -n "${core_source[$dest]:-}" ]; then + if ! cmp -s "${core_source[$dest]}" "$file"; then + echo "::error::$file and ${core_source[$dest]} carry different cores for $dest" >&2 + status=1 + fi + continue + fi + core_source[$dest]="$file" + ;; + *) + dest="$target/phpstan_turbo-$minor.${base##*.}" + ;; + esac + printf '%s\t%s\n' "$file" "$dest" + mapped=$((mapped + 1)) + done +done + +if [ "$mapped" -eq 0 ]; then + echo "::error::no phpstan_turbo-* artifacts found in $artifacts" >&2 + status=1 +fi + +exit "$status" diff --git a/.github/workflows/phar.yml b/.github/workflows/phar.yml index 9855ce2e0bb..9173c27164c 100644 --- a/.github/workflows/phar.yml +++ b/.github/workflows/phar.yml @@ -344,18 +344,18 @@ jobs: turbo-origins: name: "Find Reusable Turbo Extension Builds" # Most pushes and pull requests leave the extension's build inputs - # untouched, and compiling all of its legs takes up to 12 minutes. For - # each binary, this finds the newest earlier run that compiled it from - # identical build inputs (.github/scripts/find-turbo-origins.sh has the - # exact conditions), and its compile leg skips the build altogether: it - # runs on ubuntu-latest, outside any build container, and only carries - # that binary over into this run. The binary still goes through the - # version check and the differential tests of the turbo-differential - # jobs, like a compiled one — they compare the native side with the PHP - # side of this commit, which may have changed. A binary reused on a - # PHP 8.6 leg was compiled against prerelease headers that may have - # moved on since; if it no longer loads, the version check there fails - # until the next change to the build inputs compiles it again. + # untouched, and building a platform's profile-guided core takes up to + # 12 minutes. For each core, this finds the newest earlier run that + # compiled it from identical build inputs + # (.github/scripts/find-turbo-origins.sh has the exact conditions), and + # its turbo-compile-core leg skips the build altogether: it runs on + # ubuntu-latest, outside any build container, and only carries that core + # over into this run. The per-version extensions always compile (a + # minute each) against it, then go through the version check and the + # differential tests of the turbo-differential jobs like any build — + # they compare the native side with the PHP side of this commit, which + # may have changed. An extension compiled against 8.6 prerelease headers + # that moved on since fails its own leg, not the reused core. # Only runs of this workflow triggered by a push to 2.3.x qualify — the # same runs whose compiled binaries the commit job stages into # phpstan/phpstan — so reusing their artifacts trusts no code the @@ -391,106 +391,68 @@ jobs: BRANCH: "2.3.x" run: bash .github/scripts/find-turbo-origins.sh - turbo-compile: - name: "Compile Turbo Extension" - # A leg whose binary turbo-origins found in an earlier run builds - # nothing: it runs on ubuntu-latest without a container and carries that - # binary over (runs-on and container cannot read env, hence the lookup - # of ORIGIN_RUN_ID repeated in both). The tests of every binary, reused - # or compiled, run in the turbo-differential jobs, so the commit job, - # which needs the binaries, does not wait for them. - runs-on: ${{ fromJSON(needs.turbo-origins.outputs.origins || '{}')[format('{0}-{1}-php{2}{3}', matrix.target.artifact, matrix.target.name, matrix.php-version, matrix.target.suffix || '')] && 'ubuntu-latest' || matrix.target.runs-on }} - # Container legs pin the built binaries' userland floor independently of - # the runner image. The gnu legs run in a prebuilt ubuntu:22.04-based + turbo-compile-core: + name: "Compile Turbo Extension Core" + # The distributed binaries are a shared core per platform plus a thin + # extension per PHP version (turbo-ext/README.md, "Shared core"). The + # core is everything but the four version-specific sources: it compiles + # to the same code against every supported PHP's headers + # (turbo-shared-core-gate proves it), so it is built once per platform, + # here, profile-guided against one PHP version (matrix php-version), and + # every turbo-compile leg of the platform links its extension against it. + # A leg whose core turbo-origins found in an earlier run builds nothing: + # it runs on ubuntu-latest without a container and carries that core + # over (runs-on and container cannot read env, hence the lookup of + # ORIGIN_RUN_ID repeated in both). + runs-on: ${{ fromJSON(needs.turbo-origins.outputs.origins || '{}')[format('phpstan_turbo_core-{0}', matrix.target.name)] && 'ubuntu-latest' || matrix.target.runs-on }} + # Container legs pin the binaries' userland floor independently of the + # runner image: the gnu legs run in the prebuilt ubuntu:22.04-based # image (.github/turbo-build/Dockerfile, published to GHCR by - # turbo-build-image.yml) with the PHP toolchain baked in: the glibc - # symbol floor stays at 2.34 (RHEL 9, Ubuntu 22.04+, Debian 12+) and the - # jobs run no apt at all — ports.ubuntu.com (the only Ubuntu arm64 - # mirror) has repeated outages that failed the arm64 legs, and the - # from-scratch PPA setup cost ~1 minute on every x86_64 leg. alpine:3.24 - # builds the musl variant; apk installs take seconds from a CDN, so no - # prebuilt image is needed there. The linux-musl-arm64 leg lives in the - # separate turbo-compile-musl-arm64 job below: JavaScript-based actions - # cannot run in Alpine containers on arm64 runners (the runner only - # ships an x64 musl Node), so that job drives the Alpine container - # through docker exec instead of a `container:` leg. - # Linux ZTS legs (suffix -zts) exist from PHP 8.6 on: the official Docker - # images build every 8.6+ variant thread-safe, cli and alpine included - # (docker-library/php#1686), while distros, ppa:ondrej/php, sury, Remi - # and setup-php keep shipping an NTS CLI — so 8.6+ needs both. Up to 8.5 - # thread-safe PHP on Linux (FrankenPHP, php:*-zts images) is a few - # percent of hosts and gets no binary; the commit job deletes those from - # phpstan/phpstan (TURBO_RETIRED_BINARIES). Windows has ZTS legs for - # every minor: XAMPP, WampServer and Scoop default to TS PHP. - # The gnu-php8.6 and gnu-php8.6-zts images build the official 8.6 - # prerelease tarball (see the Dockerfile); the musl ZTS legs build the - # same tarball in alpine:3.24, since Alpine packages no thread-safe PHP. - # Alpine's NTS php86 packages are currently only in edge/testing. - container: ${{ !(fromJSON(needs.turbo-origins.outputs.origins || '{}')[format('{0}-{1}-php{2}{3}', matrix.target.artifact, matrix.target.name, matrix.php-version, matrix.target.suffix || '')]) && (matrix.target.family == 'gnu' && format('ghcr.io/phpstan/turbo-build:gnu-php{0}{1}', matrix.php-version, matrix.target.suffix || '') || matrix.target.family == 'musl' && matrix.php-version == '8.6' && matrix.target.suffix != '-zts' && 'alpine:edge' || matrix.target.container) || '' }} + # turbo-build-image.yml) — the glibc symbol floor stays at 2.34 (RHEL 9, + # Ubuntu 22.04+, Debian 12+) and the jobs run no apt at all — and + # alpine:3.24 builds the musl variant. The linux-musl-arm64 core is in + # turbo-compile-core-musl-arm64 (see turbo-compile-musl-arm64 for why). + container: ${{ !(fromJSON(needs.turbo-origins.outputs.origins || '{}')[format('phpstan_turbo_core-{0}', matrix.target.name)]) && (matrix.target.family == 'gnu' && format('ghcr.io/phpstan/turbo-build:gnu-php{0}', matrix.target.php-version) || matrix.target.container) || '' }} needs: turbo-origins timeout-minutes: 30 permissions: contents: read # actions/checkout of this repository - actions: read # downloads a reused binary from an earlier run, see turbo-origins + actions: read # downloads a reused core from an earlier run, see turbo-origins env: - PHP_MINOR: ${{ matrix.php-version }} - PHP_ZTS: ${{ matrix.target.suffix == '-zts' && '1' || '0' }} - TURBO_ARTIFACT: "${{ matrix.target.artifact }}-${{ matrix.target.name }}-php${{ matrix.php-version }}${{ matrix.target.suffix }}" - ORIGIN_RUN_ID: ${{ fromJSON(needs.turbo-origins.outputs.origins || '{}')[format('{0}-{1}-php{2}{3}', matrix.target.artifact, matrix.target.name, matrix.php-version, matrix.target.suffix || '')] }} + PHP_MINOR: ${{ matrix.target.php-version }} + PHP_ZTS: "0" + TURBO_ARTIFACT: "phpstan_turbo_core-${{ matrix.target.name }}" + ORIGIN_RUN_ID: ${{ fromJSON(needs.turbo-origins.outputs.origins || '{}')[format('phpstan_turbo_core-{0}', matrix.target.name)] }} strategy: fail-fast: false matrix: - php-version: ["8.3", "8.4", "8.5", "8.6"] + # php-version: the PHP the core is compiled and profiled against — + # any supported one gives the same code (the gate); the newest + # stable one runs the training workload the way most users will target: - name: "linux-gnu-x86_64" runs-on: "ubuntu-latest" family: "gnu" - artifact: "phpstan_turbo" + php-version: "8.5" - name: "linux-gnu-arm64" runs-on: "ubuntu-24.04-arm" family: "gnu" - artifact: "phpstan_turbo" + php-version: "8.5" - name: "linux-musl-x86_64" runs-on: "ubuntu-latest" container: "alpine:3.24" family: "musl" - artifact: "phpstan_turbo" - # arm64 (Apple Silicon) only: setup-php dropped Intel macOS. An - # x86_64 PHP — native Intel, or Rosetta on Apple Silicon, which - # reports x86_64 too — finds no binary: TurboExtensionSelector - # resolves Darwin to macos-arm64 only when php_uname('m') says arm64. + php-version: "8.5" + # arm64 (Apple Silicon) only: setup-php dropped Intel macOS, and + # TurboExtensionSelector resolves Darwin to macos-arm64 only when + # php_uname('m') says arm64. - name: "macos-arm64" runs-on: "macos-latest" family: "macos" - artifact: "phpstan_turbo" - # thread-safe builds, 8.6+ only (see above); the musl arm64 one is in - # turbo-compile-musl-arm64 - include: - - php-version: "8.6" - target: - name: "linux-gnu-x86_64" - runs-on: "ubuntu-latest" - family: "gnu" - artifact: "phpstan_turbo" - suffix: "-zts" - - php-version: "8.6" - target: - name: "linux-gnu-arm64" - runs-on: "ubuntu-24.04-arm" - family: "gnu" - artifact: "phpstan_turbo" - suffix: "-zts" - - php-version: "8.6" - target: - name: "linux-musl-x86_64" - runs-on: "ubuntu-latest" - container: "alpine:3.24" - family: "musl" - artifact: "phpstan_turbo" - suffix: "-zts" + php-version: "8.5" steps: - name: Harden the runner (Audit all outbound calls) @@ -500,7 +462,7 @@ jobs: egress-policy: audit # See turbo-origins. The build steps below are all skipped on this leg. - - name: "Download the extension compiled from identical build inputs" + - name: "Download the core compiled from identical build inputs" if: env.ORIGIN_RUN_ID != '' uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: @@ -509,7 +471,7 @@ jobs: run-id: ${{ env.ORIGIN_RUN_ID }} github-token: ${{ github.token }} - - name: "Record the run that compiled the extension" + - name: "Record the run that compiled the core" if: env.ORIGIN_RUN_ID != '' run: echo "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$ORIGIN_RUN_ID" | tee turbo-reused.txt @@ -525,8 +487,8 @@ jobs: uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 with: persist-credentials: false - # The Makefile bakes the extension version via git log over the - # watched paths; a shallow clone would resolve every build to the + # The Makefile bakes the version via git log over the watched + # paths; a shallow clone would resolve every build to the # checked-out commit itself — on pull requests the synthetic merge # commit, which matches nothing. fetch-depth: 0 @@ -539,7 +501,6 @@ jobs: if: env.ORIGIN_RUN_ID == '' run: git config --global --add safe.directory "$GITHUB_WORKSPACE" - # The build needs php/php-config on PATH for the matrix PHP version. - name: "Install PHP and build tools (Alpine container)" if: env.ORIGIN_RUN_ID == '' && matrix.target.family == 'musl' run: sh .github/scripts/install-alpine-php.sh @@ -549,7 +510,7 @@ jobs: uses: "shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962" # 2.40.0 with: coverage: "none" - php-version: "${{ matrix.php-version }}" + php-version: "${{ matrix.target.php-version }}" # The training run of `make pgo` (bin/pgo-train.sh) runs bin/phpstan on # the checkout, so the dependencies must be in place before the build. @@ -569,74 +530,64 @@ jobs: php composer.phar install --no-interaction --no-progress rm composer.phar - - name: "Compile phpstan_turbo with strict warnings (profile-guided)" + - name: "Compile the core with strict warnings (profile-guided)" if: env.ORIGIN_RUN_ID == '' working-directory: "turbo-ext" - # `make pgo`: an instrumented build, a training run of PHPStan over - # its own sources with that build loaded, then the final build using - # the recorded profile — the same three stages `make pgo` runs - # locally, so a profile-guided binary can be reproduced outside CI. + # `make SPLIT=1 pgo`: an instrumented core and extension, a training + # run of PHPStan over its own sources with them loaded, then the + # final build using the recorded profile — the same three stages + # locally, so a profile-guided core can be reproduced outside CI. # The strict set itself lives in turbo-ext/Makefile - # (STRICT_WARN_FLAGS), which documents why each exemption is there — - # every one is a third-party macro expansion, not our code. Reading - # it here keeps one definition instead of three copies that drift. + # (STRICT_WARN_FLAGS), which documents why each exemption is there. run: | - make pgo WARN_FLAGS="$(make -s print-warn-flags)" -j"$(getconf _NPROCESSORS_ONLN 2>/dev/null || nproc)" + make SPLIT=1 pgo WARN_FLAGS="$(make -s print-warn-flags)" -j"$(getconf _NPROCESSORS_ONLN 2>/dev/null || nproc)" # Before signing (stripping invalidates a signature) and before the - # upload, so the turbo-differential jobs test the exact binary that - # ships. The symbol tables are a quarter of the .so; see `strip` + # upload. The symbol tables are a quarter of the binary; see `strip` # in the Makefile. - name: "Strip symbol tables" if: env.ORIGIN_RUN_ID == '' working-directory: "turbo-ext" run: | - ls -l phpstan_turbo.so - make strip - ls -l phpstan_turbo.so + ls -l phpstan_turbo_core.so phpstan_turbo.so + make SPLIT=1 strip + ls -l phpstan_turbo_core.so phpstan_turbo.so # On arm64 AMFI refuses to map unsigned code, so the binary must carry - # at least an ad-hoc signature. The linker applies one itself, but - # re-sign deliberately instead of relying on that. Quarantine (and thus + # at least an ad-hoc signature. Quarantine (and thus # Gatekeeper/notarization) only applies to browser downloads — # composer/git/curl installs never see it — so an ad-hoc signature is # enough and needs no secrets. - - name: "Ad-hoc sign the extension (macOS)" + - name: "Ad-hoc sign the core (macOS)" if: env.ORIGIN_RUN_ID == '' && matrix.target.family == 'macos' working-directory: "turbo-ext" run: | - codesign --force --sign - phpstan_turbo.so - codesign --verify --verbose=2 phpstan_turbo.so + codesign --force --sign - phpstan_turbo_core.so phpstan_turbo.so + codesign --verify --verbose=2 phpstan_turbo_core.so - # Stays here rather than in turbo-differential: the commit job stages - # this binary, and one baked with the wrong version would ship - # inactive. A reused binary passed this check in the run compiling it, - # and find-turbo-origins.sh only picks runs whose last commit touching - # turbo-ext/src, which the version is baked from, matches HEAD's. - - name: "Verify the built extension reports the expected version" + # The extension the training run loaded proves the core loads and + # carries the version the extension expects; a reused core passed + # this in the run compiling it. + - name: "Verify the core reports the expected version" if: env.ORIGIN_RUN_ID == '' run: | REPORTED="$(php -d extension="$PWD/turbo-ext/phpstan_turbo.so" -r 'echo phpversion("phpstan_turbo");')" EXPECTED="$(sed -n "s/.*EXPECTED_EXTENSION_VERSION = '\([^']*\)'.*/\1/p" src/Turbo/TurboExtensionEnabler.php)" - echo "built extension reports: $REPORTED, enabler expects: $EXPECTED" + echo "core + extension report: $REPORTED, enabler expects: $EXPECTED" [ "$REPORTED" = "$EXPECTED" ] - # the loader rejects a ts-mismatched binary, so php loading it above - # proves the binary matches the interpreter; assert the interpreter - # itself so a -zts artifact cannot silently carry an NTS build - [ "$(php -r 'echo (int) ((bool) PHP_ZTS);')" = "$PHP_ZTS" ] - # A reused binary is uploaded again under the same name, so the jobs - # downstream find every binary in this run. - - name: "Upload extension artifact" + # A reused core is uploaded again under the same name, so the + # turbo-compile legs find it in this run. + - name: "Upload core artifact" uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ env.TURBO_ARTIFACT }} - path: "turbo-ext/phpstan_turbo.so" + path: "turbo-ext/phpstan_turbo_core.so" if-no-files-found: "error" - # Keeps later runs from reusing the copy above: they take the binary + # Keeps later runs from reusing the copy above: they take the core # from the run that compiled it (see find-turbo-origins.sh). - - name: "Mark the extension artifact as reused" + - name: "Mark the core artifact as reused" if: env.ORIGIN_RUN_ID != '' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: @@ -644,39 +595,25 @@ jobs: path: "turbo-reused.txt" if-no-files-found: "error" - turbo-compile-musl-arm64: - name: "Compile Turbo Extension (musl arm64)" - # The arm64 counterpart of the linux-musl-x86_64 leg in turbo-compile. - # It cannot be a `container: alpine` leg there: JavaScript-based actions - # cannot run in Alpine containers on arm64 runners (the runner only - # ships an x64 musl Node). Instead the workflow steps run on the arm64 - # host and only the build commands run inside a long-lived Alpine - # container via docker exec — native arm64, no QEMU. The steps mirror - # the musl legs of turbo-compile one for one, including the reuse of a - # binary compiled in an earlier run, whose leg runs on ubuntu-latest. - runs-on: ${{ fromJSON(needs.turbo-origins.outputs.origins || '{}')[format('phpstan_turbo-linux-musl-arm64-php{0}{1}', matrix.php-version, matrix.ts == 'zts' && '-zts' || '')] && 'ubuntu-latest' || 'ubuntu-24.04-arm' }} + turbo-compile-core-musl-arm64: + name: "Compile Turbo Extension Core (musl arm64)" + # The arm64 counterpart of the linux-musl-x86_64 leg of + # turbo-compile-core, driving an Alpine container through docker exec + # (see turbo-compile-musl-arm64 for why it cannot be a container leg). + runs-on: ${{ fromJSON(needs.turbo-origins.outputs.origins || '{}')['phpstan_turbo_core-linux-musl-arm64'] && 'ubuntu-latest' || 'ubuntu-24.04-arm' }} needs: turbo-origins timeout-minutes: 30 permissions: contents: read # actions/checkout of this repository - actions: read # downloads a reused binary from an earlier run, see turbo-origins + actions: read # downloads a reused core from an earlier run, see turbo-origins env: - PHP_MINOR: ${{ matrix.php-version }} - PHP_ZTS: ${{ matrix.ts == 'zts' && '1' || '0' }} - TURBO_ARTIFACT: "phpstan_turbo-linux-musl-arm64-php${{ matrix.php-version }}${{ matrix.ts == 'zts' && '-zts' || '' }}" - ORIGIN_RUN_ID: ${{ fromJSON(needs.turbo-origins.outputs.origins || '{}')[format('phpstan_turbo-linux-musl-arm64-php{0}{1}', matrix.php-version, matrix.ts == 'zts' && '-zts' || '')] }} - - strategy: - fail-fast: false - matrix: - php-version: ["8.3", "8.4", "8.5", "8.6"] - ts: ["nts"] - # thread-safe from 8.6 on, see turbo-compile - include: - - php-version: "8.6" - ts: "zts" + # the PHP the core is compiled and profiled against, as in turbo-compile-core + PHP_MINOR: "8.5" + PHP_ZTS: "0" + TURBO_ARTIFACT: "phpstan_turbo_core-linux-musl-arm64" + ORIGIN_RUN_ID: ${{ fromJSON(needs.turbo-origins.outputs.origins || '{}')['phpstan_turbo_core-linux-musl-arm64'] }} steps: - name: Harden the runner (Audit all outbound calls) @@ -684,8 +621,8 @@ jobs: with: egress-policy: audit - # See the same two steps in turbo-compile. - - name: "Download the extension compiled from identical build inputs" + # See the same two steps in turbo-compile-core. + - name: "Download the core compiled from identical build inputs" if: env.ORIGIN_RUN_ID != '' uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: @@ -694,7 +631,7 @@ jobs: run-id: ${{ env.ORIGIN_RUN_ID }} github-token: ${{ github.token }} - - name: "Record the run that compiled the extension" + - name: "Record the run that compiled the core" if: env.ORIGIN_RUN_ID != '' run: echo "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$ORIGIN_RUN_ID" | tee turbo-reused.txt @@ -703,21 +640,15 @@ jobs: uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 with: persist-credentials: false - # The Makefile bakes the extension version via git log over the - # watched paths; a shallow clone would resolve every build to the - # checked-out commit itself — on pull requests the synthetic merge - # commit, which matches nothing. - fetch-depth: 0 + fetch-depth: 0 # the version is baked from git log over turbo-ext/src - name: "Start the Alpine build container" if: env.ORIGIN_RUN_ID == '' - env: - ALPINE_IMAGE: ${{ matrix.php-version == '8.6' && matrix.ts == 'nts' && 'alpine:edge' || 'alpine:3.24' }} run: | docker run -d --name alpine-build \ -v "$PWD:/work" -w /work \ -e PHP_MINOR -e PHP_ZTS -e COMPOSER_VERSION -e COMPOSER_PHAR_SHA256 \ - "$ALPINE_IMAGE" sleep 7200 + alpine:3.24 sleep 7200 - name: "Install PHP and build tools" if: env.ORIGIN_RUN_ID == '' @@ -725,14 +656,12 @@ jobs: # The workspace volume is owned by the host runner user while the # container runs as root, so git refuses the repo ("dubious - # ownership") — which would silently bake the version as "dev" via - # the Makefile's $(shell git log ...) fallback. + # ownership") — which would silently bake the version as "dev". - name: "Trust the checkout despite the container/host uid mismatch" if: env.ORIGIN_RUN_ID == '' run: docker exec alpine-build git config --global --add safe.directory /work - # The training run of `make pgo` runs bin/phpstan on the checkout, so - # the dependencies must be in place before the build. + # The training run of `make pgo` runs bin/phpstan on the checkout. - name: "Install Composer dependencies (pinned composer)" if: env.ORIGIN_RUN_ID == '' run: | @@ -743,26 +672,282 @@ jobs: rm composer.phar EOF - - name: "Compile phpstan_turbo with strict warnings (profile-guided)" + - name: "Compile the core with strict warnings (profile-guided)" if: env.ORIGIN_RUN_ID == '' run: | docker exec -i -w /work/turbo-ext alpine-build sh -e <<'EOF' - make pgo WARN_FLAGS="$(make -s print-warn-flags)" -j"$(getconf _NPROCESSORS_ONLN 2>/dev/null || nproc)" + make SPLIT=1 pgo WARN_FLAGS="$(make -s print-warn-flags)" -j"$(getconf _NPROCESSORS_ONLN 2>/dev/null || nproc)" + make SPLIT=1 strip + ls -l phpstan_turbo_core.so phpstan_turbo.so EOF - # before the upload, so turbo-differential-musl tests the exact binary - # that ships - - name: "Strip symbol tables" + - name: "Verify the core reports the expected version" if: env.ORIGIN_RUN_ID == '' + run: | + docker exec -i alpine-build sh -e <<'EOF' + REPORTED="$(php -d extension="$PWD/turbo-ext/phpstan_turbo.so" -r 'echo phpversion("phpstan_turbo");')" + EXPECTED="$(sed -n "s/.*EXPECTED_EXTENSION_VERSION = '\([^']*\)'.*/\1/p" src/Turbo/TurboExtensionEnabler.php)" + echo "core + extension report: $REPORTED, enabler expects: $EXPECTED" + [ "$REPORTED" = "$EXPECTED" ] + EOF + + - name: "Upload core artifact" + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ${{ env.TURBO_ARTIFACT }} + path: "turbo-ext/phpstan_turbo_core.so" + if-no-files-found: "error" + + - name: "Mark the core artifact as reused" + if: env.ORIGIN_RUN_ID != '' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: "turbo-reused-${{ env.TURBO_ARTIFACT }}" + path: "turbo-reused.txt" + if-no-files-found: "error" + + turbo-compile: + name: "Compile Turbo Extension" + # The extension of one PHP version: the four version-specific sources + # (the Makefile's SHIM_SOURCES) compiled against that version's headers + # and linked against the platform's core from turbo-compile-core. A + # minute's work, so it always runs; the core is what earlier runs' + # builds are reused for. The artifact carries the core next to the + # extension — the tests of the turbo-differential jobs load it from + # there, and the commit job checks that every leg of a platform carries + # the same core. + runs-on: ${{ matrix.target.runs-on }} + # Container legs pin the binaries' userland floor independently of the + # runner image, like turbo-compile-core. Linux ZTS legs (suffix -zts) + # exist from PHP 8.6 on: the official Docker images build every 8.6+ + # variant thread-safe, cli and alpine included (docker-library/php#1686), + # while distros, ppa:ondrej/php, sury, Remi and setup-php keep shipping + # an NTS CLI — so 8.6+ needs both. Up to 8.5 thread-safe PHP on Linux + # (FrankenPHP, php:*-zts images) is a few percent of hosts and gets no + # binary; the commit job deletes those from phpstan/phpstan + # (TURBO_RETIRED_BINARIES). The thread-safe extension links against the + # same core: the core never touches TSRM (turbo-shared-core-gate compares + # the two builds too). The gnu-php8.6 and gnu-php8.6-zts images build the + # official 8.6 prerelease tarball (see the Dockerfile); the musl ZTS legs + # build the same tarball in alpine:3.24, since Alpine packages no + # thread-safe PHP. Alpine's NTS php86 packages are currently only in + # edge/testing. + container: ${{ matrix.target.family == 'gnu' && format('ghcr.io/phpstan/turbo-build:gnu-php{0}{1}', matrix.php-version, matrix.target.suffix || '') || matrix.target.family == 'musl' && matrix.php-version == '8.6' && matrix.target.suffix != '-zts' && 'alpine:edge' || matrix.target.container || '' }} + needs: turbo-compile-core + # one platform's failed core must not skip every platform's legs: a leg + # whose core is missing fails at its download instead + if: ${{ !cancelled() }} + timeout-minutes: 20 + + env: + PHP_MINOR: ${{ matrix.php-version }} + PHP_ZTS: ${{ matrix.target.suffix == '-zts' && '1' || '0' }} + TURBO_ARTIFACT: "phpstan_turbo-${{ matrix.target.name }}-php${{ matrix.php-version }}${{ matrix.target.suffix }}" + + strategy: + fail-fast: false + matrix: + php-version: ["8.3", "8.4", "8.5", "8.6"] + target: + - name: "linux-gnu-x86_64" + runs-on: "ubuntu-latest" + family: "gnu" + - name: "linux-gnu-arm64" + runs-on: "ubuntu-24.04-arm" + family: "gnu" + - name: "linux-musl-x86_64" + runs-on: "ubuntu-latest" + container: "alpine:3.24" + family: "musl" + # arm64 (Apple Silicon) only, like turbo-compile-core + - name: "macos-arm64" + runs-on: "macos-latest" + family: "macos" + # thread-safe builds, 8.6+ only (see above); the musl arm64 one is in + # turbo-compile-musl-arm64 + include: + - php-version: "8.6" + target: + name: "linux-gnu-x86_64" + runs-on: "ubuntu-latest" + family: "gnu" + suffix: "-zts" + - php-version: "8.6" + target: + name: "linux-gnu-arm64" + runs-on: "ubuntu-24.04-arm" + family: "gnu" + suffix: "-zts" + - php-version: "8.6" + target: + name: "linux-musl-x86_64" + runs-on: "ubuntu-latest" + container: "alpine:3.24" + family: "musl" + suffix: "-zts" + + steps: + - name: Harden the runner (Audit all outbound calls) + if: matrix.target.family == 'macos' + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + + # actions/checkout needs git inside the Alpine container; PHP itself is + # installed after checkout, by a script from the repository. The GNU + # container legs have the toolchain baked in. + - name: "Install git (Alpine container)" + if: matrix.target.family == 'musl' + run: apk add --no-cache git + + - name: "Checkout" + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + with: + persist-credentials: false + # the version is baked from git log over turbo-ext/src, see + # turbo-compile-core + fetch-depth: 0 + + # see turbo-compile-core + - name: "Trust the checkout despite the container/host uid mismatch" + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + + # The build needs php/php-config on PATH for the matrix PHP version. + - name: "Install PHP and build tools (Alpine container)" + if: matrix.target.family == 'musl' + run: sh .github/scripts/install-alpine-php.sh + + - name: "Install PHP (macOS)" + if: matrix.target.family == 'macos' + uses: "shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962" # 2.40.0 + with: + coverage: "none" + php-version: "${{ matrix.php-version }}" + + - name: "Download the platform's core" + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: "phpstan_turbo_core-${{ matrix.target.name }}" + path: "turbo-ext" + + # The strict set lives in turbo-ext/Makefile (STRICT_WARN_FLAGS). + - name: "Compile the extension with strict warnings" + working-directory: "turbo-ext" + run: | + make SPLIT=1 extension WARN_FLAGS="$(make -s print-warn-flags)" -j"$(getconf _NPROCESSORS_ONLN 2>/dev/null || nproc)" + + # Before signing and the upload; a plain `make strip` leaves the core + # (already stripped and signed by its own job) untouched. + - name: "Strip symbol tables" + working-directory: "turbo-ext" + run: | + ls -l phpstan_turbo.so phpstan_turbo_core.so + make strip + ls -l phpstan_turbo.so phpstan_turbo_core.so + + # see turbo-compile-core + - name: "Ad-hoc sign the extension (macOS)" + if: matrix.target.family == 'macos' + working-directory: "turbo-ext" + run: | + codesign --force --sign - phpstan_turbo.so + codesign --verify --verbose=2 phpstan_turbo.so + + # Stays here rather than in turbo-differential: the commit job stages + # this binary, and one baked with the wrong version — or paired with a + # core of another version, which leaves it reporting "core-mismatch" — + # would ship inactive. + - name: "Verify the built extension reports the expected version" + run: | + REPORTED="$(php -d extension="$PWD/turbo-ext/phpstan_turbo.so" -r 'echo phpversion("phpstan_turbo");')" + EXPECTED="$(sed -n "s/.*EXPECTED_EXTENSION_VERSION = '\([^']*\)'.*/\1/p" src/Turbo/TurboExtensionEnabler.php)" + echo "built extension reports: $REPORTED, enabler expects: $EXPECTED" + [ "$REPORTED" = "$EXPECTED" ] + # the loader rejects a ts-mismatched binary, so php loading it above + # proves the binary matches the interpreter; assert the interpreter + # itself so a -zts artifact cannot silently carry an NTS build + [ "$(php -r 'echo (int) ((bool) PHP_ZTS);')" = "$PHP_ZTS" ] + + - name: "Upload extension artifact" + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ${{ env.TURBO_ARTIFACT }} + path: | + turbo-ext/phpstan_turbo.so + turbo-ext/phpstan_turbo_core.so + if-no-files-found: "error" + + turbo-compile-musl-arm64: + name: "Compile Turbo Extension (musl arm64)" + # The arm64 counterpart of the linux-musl-x86_64 leg in turbo-compile. + # It cannot be a `container: alpine` leg there: JavaScript-based actions + # cannot run in Alpine containers on arm64 runners (the runner only + # ships an x64 musl Node). Instead the workflow steps run on the arm64 + # host and only the build commands run inside a long-lived Alpine + # container via docker exec — native arm64, no QEMU. + runs-on: "ubuntu-24.04-arm" + needs: turbo-compile-core-musl-arm64 + if: ${{ !cancelled() }} + timeout-minutes: 20 + + env: + PHP_MINOR: ${{ matrix.php-version }} + PHP_ZTS: ${{ matrix.ts == 'zts' && '1' || '0' }} + TURBO_ARTIFACT: "phpstan_turbo-linux-musl-arm64-php${{ matrix.php-version }}${{ matrix.ts == 'zts' && '-zts' || '' }}" + + strategy: + fail-fast: false + matrix: + php-version: ["8.3", "8.4", "8.5", "8.6"] + ts: ["nts"] + # thread-safe from 8.6 on, see turbo-compile + include: + - php-version: "8.6" + ts: "zts" + + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + + - name: "Checkout" + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + with: + persist-credentials: false + fetch-depth: 0 # the version is baked from git log over turbo-ext/src + + - name: "Download the platform's core" + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: "phpstan_turbo_core-linux-musl-arm64" + path: "turbo-ext" + + - name: "Start the Alpine build container" + env: + ALPINE_IMAGE: ${{ matrix.php-version == '8.6' && matrix.ts == 'nts' && 'alpine:edge' || 'alpine:3.24' }} + run: | + docker run -d --name alpine-build \ + -v "$PWD:/work" -w /work \ + -e PHP_MINOR -e PHP_ZTS \ + "$ALPINE_IMAGE" sleep 7200 + + - name: "Install PHP and build tools" + run: docker exec alpine-build sh .github/scripts/install-alpine-php.sh + + # see turbo-compile-core-musl-arm64 + - name: "Trust the checkout despite the container/host uid mismatch" + run: docker exec alpine-build git config --global --add safe.directory /work + + - name: "Compile the extension with strict warnings" run: | docker exec -i -w /work/turbo-ext alpine-build sh -e <<'EOF' - ls -l phpstan_turbo.so + make SPLIT=1 extension WARN_FLAGS="$(make -s print-warn-flags)" -j"$(getconf _NPROCESSORS_ONLN 2>/dev/null || nproc)" make strip - ls -l phpstan_turbo.so + ls -l phpstan_turbo.so phpstan_turbo_core.so EOF - name: "Verify the built extension reports the expected version" - if: env.ORIGIN_RUN_ID == '' run: | docker exec -i alpine-build sh -e <<'EOF' REPORTED="$(php -d extension="$PWD/turbo-ext/phpstan_turbo.so" -r 'echo phpversion("phpstan_turbo");')" @@ -776,16 +961,42 @@ jobs: uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ env.TURBO_ARTIFACT }} - path: "turbo-ext/phpstan_turbo.so" + path: | + turbo-ext/phpstan_turbo.so + turbo-ext/phpstan_turbo_core.so if-no-files-found: "error" - - name: "Mark the extension artifact as reused" - if: env.ORIGIN_RUN_ID != '' - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + turbo-shared-core-gate: + name: "Turbo Shared Core Gate" + # The core every platform ships is compiled against one PHP version's + # headers and runs on all of them, so the sources it is built from must + # compile to identical code against each supported version's — a source + # reading a member whose offset moved, a constant whose value changed or + # a function whose signature did would otherwise misbehave on the other + # versions only, on the paths that touch it. Compiles every source + # against each version's (and the 8.6 thread-safe) headers in the CI + # build images with clang, and fails on any difference outside the + # version-specific sources (turbo-ext/bin/shared-core/check-linux.sh). + runs-on: "ubuntu-latest" + timeout-minutes: 40 + + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 with: - name: "turbo-reused-${{ env.TURBO_ARTIFACT }}" - path: "turbo-reused.txt" - if-no-files-found: "error" + egress-policy: audit + + - name: "Checkout" + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + with: + persist-credentials: false + + - name: "Compare the shared code across the supported PHP versions" + env: + SHARED_CORE_WORK_DIR: ${{ runner.temp }}/shared-core + run: | + mkdir -p "$SHARED_CORE_WORK_DIR" + bash turbo-ext/bin/shared-core/check-linux.sh turbo-docker-run: name: "Run with Turbo Extension (Docker PHP builds)" @@ -968,29 +1179,154 @@ jobs: SCRIPT: ${{ matrix.script }} run: docker run --rm --shm-size=1g -v "$PWD:/work" -v "$RUNNER_TEMP/turbo-docker.ini:/usr/local/etc/php/conf.d/zz-turbo.ini" -w /work "$IMAGE" bash -c "$SCRIPT" - turbo-compile-windows: - name: "Compile Turbo Extension (Windows)" - # windows-latest and windows-2025 serve the VS2026 image, whose 14.5x - # toolset links DLLs the official php.net binaries (built with vs17, - # toolset 14.4x) refuse to load — PHP's loader rejects modules linked - # with a newer toolset generation than the core. The windows-2022 image - # is the one that ships VS2022. - # PHP 8.6's vs18 development packs need the VS2026 toolchain. - # A leg reusing a binary (see turbo-compile) runs on ubuntu-latest. - runs-on: ${{ fromJSON(needs.turbo-origins.outputs.origins || '{}')[format('phpstan_turbo-windows-x86_64-php{0}{1}', matrix.php-version, matrix.ts == 'zts' && '-zts' || '')] && 'ubuntu-latest' || (matrix.php-version == '8.6' && 'windows-2025-vs2026' || 'windows-2022') }} + turbo-compile-core-windows: + name: "Compile Turbo Extension Core (Windows)" + # The Windows core, once per thread-safety flavour: a DLL imports the + # engine from php8.dll or php8ts.dll by name, so the two cannot share + # one. Built against the oldest supported minor's devel pack with its + # toolset (VS16, linker 14.2x): PHP's loader only checks the toolset of + # the extension DLL it loads (turbo-compile-windows builds each with its + # minor's), and a core linked with an older toolset runs on every newer + # VC runtime. + runs-on: ${{ fromJSON(needs.turbo-origins.outputs.origins || '{}')[format('phpstan_turbo_core-windows-x86_64{0}', matrix.ts == 'zts' && '-zts' || '')] && 'ubuntu-latest' || 'windows-2022' }} needs: turbo-origins timeout-minutes: 40 permissions: contents: read # actions/checkout of this repository - actions: read # downloads a reused binary from an earlier run, see turbo-origins + actions: read # downloads a reused core from an earlier run, see turbo-origins env: # php/php-sdk-binary-tools — the toolchain every PHP Windows build # uses (ships bison and the unix tools phpize/configure expect). PHP_SDK_COMMIT: "c73faaf1cce914e2fc04da5587132f8f425996af" # php-sdk-2.7.1 + TURBO_ARTIFACT: "phpstan_turbo_core-windows-x86_64${{ matrix.ts == 'zts' && '-zts' || '' }}" + ORIGIN_RUN_ID: ${{ fromJSON(needs.turbo-origins.outputs.origins || '{}')[format('phpstan_turbo_core-windows-x86_64{0}', matrix.ts == 'zts' && '-zts' || '')] }} + + strategy: + fail-fast: false + matrix: + ts: ["nts", "zts"] + php-version: ["8.3"] + vs: ["vs16"] + toolset: ["14.29"] + + steps: + # See the same two steps in turbo-compile-core; this leg runs on + # ubuntu-latest. + - name: "Download the core compiled from identical build inputs" + if: env.ORIGIN_RUN_ID != '' + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ env.TURBO_ARTIFACT }} + path: "turbo-ext" + run-id: ${{ env.ORIGIN_RUN_ID }} + github-token: ${{ github.token }} + + - name: "Record the run that compiled the core" + if: env.ORIGIN_RUN_ID != '' + run: echo "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$ORIGIN_RUN_ID" | tee turbo-reused.txt + + - name: "Checkout" + if: env.ORIGIN_RUN_ID == '' + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + with: + persist-credentials: false + fetch-depth: 0 # the version is baked from git log over turbo-ext/src + + - name: "Install PHP" + if: env.ORIGIN_RUN_ID == '' + uses: "shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962" # 2.40.0 + env: + phpts: "${{ matrix.ts == 'zts' && 'ts' || 'nts' }}" + with: + coverage: "none" + php-version: "${{ matrix.php-version }}" + + - name: "Compute the extension version and download the build tools" + if: env.ORIGIN_RUN_ID == '' + shell: bash + env: + MATRIX_TS: ${{ matrix.ts }} + MATRIX_VS: ${{ matrix.vs }} + run: bash .github/scripts/download-php-windows-devel.sh + + - name: "Set up MSVC environment" + if: env.ORIGIN_RUN_ID == '' + uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756 # v1.13.0 + with: + arch: x64 + toolset: ${{ matrix.toolset }} + + - name: "Build the core" + if: env.ORIGIN_RUN_ID == '' + shell: cmd + working-directory: turbo-ext + run: | + for /d %%d in (C:\php-devel\php-*-devel-*) do set DEVEL=%%d + set PATH=%DEVEL%;C:\php-sdk\bin;C:\php-sdk\msys2\usr\bin;%PATH% + call phpize.bat + if errorlevel 1 exit /b 1 + call configure.bat --enable-phpstan-turbo=core + if errorlevel 1 exit /b 1 + nmake + + # the DLL and its import library, which turbo-compile-windows links + # each extension against + - name: "Collect the core" + if: env.ORIGIN_RUN_ID == '' + shell: bash + env: + MATRIX_TS: ${{ matrix.ts }} + run: | + CORE=$([ "$MATRIX_TS" = "zts" ] && echo phpstan_turbo_core-zts || echo phpstan_turbo_core) + mkdir -p core-out + DLL=$(find turbo-ext -name "$CORE.dll" | head -1) + LIB=$(find turbo-ext -name "$CORE.lib" | head -1) + [ -n "$DLL" ] && [ -n "$LIB" ] + cp "$DLL" "$LIB" core-out/ + ls -l core-out + echo "CORE_DIR=core-out" >> "$GITHUB_ENV" + + - name: "Point the upload at the reused core" + if: env.ORIGIN_RUN_ID != '' + run: echo "CORE_DIR=turbo-ext" >> "$GITHUB_ENV" + + - name: "Upload core artifact" + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ${{ env.TURBO_ARTIFACT }} + path: "${{ env.CORE_DIR }}/phpstan_turbo_core*" + if-no-files-found: "error" + + - name: "Mark the core artifact as reused" + if: env.ORIGIN_RUN_ID != '' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: "turbo-reused-${{ env.TURBO_ARTIFACT }}" + path: "turbo-reused.txt" + if-no-files-found: "error" + + turbo-compile-windows: + name: "Compile Turbo Extension (Windows)" + # The extension DLL of one PHP version, linked against the core of + # turbo-compile-core-windows (its import library) and delay-loading it + # from its own directory at run time (main.cpp, config.w32). + # windows-latest and windows-2025 serve the VS2026 image, whose 14.5x + # toolset links DLLs the official php.net binaries (built with vs17, + # toolset 14.4x) refuse to load — PHP's loader rejects modules linked + # with a newer toolset generation than the core. The windows-2022 image + # is the one that ships VS2022. + # PHP 8.6's vs18 development packs need the VS2026 toolchain. + runs-on: ${{ matrix.php-version == '8.6' && 'windows-2025-vs2026' || 'windows-2022' }} + needs: turbo-compile-core-windows + # see turbo-compile + if: ${{ !cancelled() }} + timeout-minutes: 30 + + env: + PHP_SDK_COMMIT: "c73faaf1cce914e2fc04da5587132f8f425996af" # php-sdk-2.7.1, see turbo-compile-core-windows TURBO_ARTIFACT: "phpstan_turbo-windows-x86_64-php${{ matrix.php-version }}${{ matrix.ts == 'zts' && '-zts' || '' }}" - ORIGIN_RUN_ID: ${{ fromJSON(needs.turbo-origins.outputs.origins || '{}')[format('phpstan_turbo-windows-x86_64-php{0}{1}', matrix.php-version, matrix.ts == 'zts' && '-zts' || '')] }} strategy: fail-fast: false @@ -1014,25 +1350,7 @@ jobs: vs: "vs18" steps: - # See the same two steps in turbo-compile; this leg runs on - # ubuntu-latest. - - name: "Download the extension compiled from identical build inputs" - if: env.ORIGIN_RUN_ID != '' - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: ${{ env.TURBO_ARTIFACT }} - path: "turbo-ext" - run-id: ${{ env.ORIGIN_RUN_ID }} - github-token: ${{ github.token }} - - - name: "Record the run that compiled the extension" - if: env.ORIGIN_RUN_ID != '' - run: | - echo "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$ORIGIN_RUN_ID" | tee turbo-reused.txt - echo "DLL_PATH=turbo-ext/php_phpstan_turbo.dll" >> "$GITHUB_ENV" - - name: "Checkout" - if: env.ORIGIN_RUN_ID == '' uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 with: persist-credentials: false @@ -1041,7 +1359,6 @@ jobs: fetch-depth: 0 - name: "Install PHP" - if: env.ORIGIN_RUN_ID == '' uses: "shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962" # 2.40.0 env: phpts: "${{ matrix.ts == 'zts' && 'ts' || 'nts' }}" @@ -1050,39 +1367,28 @@ jobs: php-version: "${{ matrix.php-version }}" - name: "Install the matching PHP 8.6 prerelease runtime" - if: env.ORIGIN_RUN_ID == '' && matrix.php-version == '8.6' + if: matrix.php-version == '8.6' shell: bash env: MATRIX_TS: ${{ matrix.ts }} run: bash .github/scripts/install-php86-windows.sh - name: "Compute the extension version and download the build tools" - if: env.ORIGIN_RUN_ID == '' shell: bash env: MATRIX_TS: ${{ matrix.ts }} MATRIX_VS: ${{ matrix.vs }} - run: | - SHA=$(git log -1 --format=%H -- turbo-ext/src | cut -c1-7) - echo "extension version: $SHA" - echo "PHPSTANTURBO_VERSION=$SHA" >> "$GITHUB_ENV" - - FULL=$(php -r 'echo PHP_VERSION;') - # thread-safe devel packs carry no infix, NTS ones carry -nts - INFIX=$([ "$MATRIX_TS" = "zts" ] && echo "" || echo "-nts") - PACK="php-devel-pack-$FULL$INFIX-Win32-$MATRIX_VS-x64.zip" - echo "devel pack: $PACK" - curl -fsSLo devel-pack.zip "https://windows.php.net/downloads/releases/$PACK" \ - || curl -fsSLo devel-pack.zip "https://windows.php.net/downloads/releases/archives/$PACK" \ - || curl -fsSLo devel-pack.zip "https://downloads.php.net/~windows/qa/$PACK" \ - || curl -fsSLo devel-pack.zip "https://downloads.php.net/~windows/qa/archives/$PACK" - unzip -q devel-pack.zip -d /c/php-devel - - git clone -q https://github.com/php/php-sdk-binary-tools.git /c/php-sdk - git -C /c/php-sdk checkout -q "$PHP_SDK_COMMIT" + run: bash .github/scripts/download-php-windows-devel.sh + + # the DLL and its import library, into the extension directory where + # config.w32 expects the library + - name: "Download the core" + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: "phpstan_turbo_core-windows-x86_64${{ matrix.ts == 'zts' && '-zts' || '' }}" + path: "turbo-ext" - name: "Set up MSVC environment" - if: env.ORIGIN_RUN_ID == '' uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756 # v1.13.0 with: arch: x64 @@ -1090,7 +1396,6 @@ jobs: toolset: ${{ matrix.toolset }} - name: "Build the extension" - if: env.ORIGIN_RUN_ID == '' shell: cmd working-directory: turbo-ext run: | @@ -1098,21 +1403,24 @@ jobs: set PATH=%DEVEL%;C:\php-sdk\bin;C:\php-sdk\msys2\usr\bin;%PATH% call phpize.bat if errorlevel 1 exit /b 1 - call configure.bat --enable-phpstan-turbo + call configure.bat --enable-phpstan-turbo=extension if errorlevel 1 exit /b 1 nmake - # See the same step in turbo-compile. + # See the same step in turbo-compile. The extension loads its core + # from its own directory, so the core goes next to the built DLL first. - name: "Verify the built extension reports the expected version" - if: env.ORIGIN_RUN_ID == '' shell: bash env: MATRIX_TS: ${{ matrix.ts }} run: | + CORE=$([ "$MATRIX_TS" = "zts" ] && echo phpstan_turbo_core-zts.dll || echo phpstan_turbo_core.dll) DLL=$(find turbo-ext -name "php_phpstan_turbo.dll" | head -1) [ -n "$DLL" ] - echo "DLL_PATH=$DLL" >> "$GITHUB_ENV" - REPORTED="$(php -d extension="$(cygpath -w "$PWD/$DLL")" -r 'echo phpversion("phpstan_turbo");')" + mkdir -p ext-out + cp "$DLL" ext-out/php_phpstan_turbo.dll + cp "turbo-ext/$CORE" ext-out/ + REPORTED="$(php -d extension="$(cygpath -w "$PWD/ext-out/php_phpstan_turbo.dll")" -r 'echo phpversion("phpstan_turbo");')" EXPECTED="$(sed -n "s/.*EXPECTED_EXTENSION_VERSION = '\([^']*\)'.*/\1/p" src/Turbo/TurboExtensionEnabler.php)" echo "built extension reports: $REPORTED, enabler expects: $EXPECTED" [ "$REPORTED" = "$EXPECTED" ] @@ -1126,15 +1434,7 @@ jobs: uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ env.TURBO_ARTIFACT }} - path: "${{ env.DLL_PATH }}" - if-no-files-found: "error" - - - name: "Mark the extension artifact as reused" - if: env.ORIGIN_RUN_ID != '' - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: "turbo-reused-${{ env.TURBO_ARTIFACT }}" - path: "turbo-reused.txt" + path: "ext-out/*.dll" if-no-files-found: "error" turbo-artifact: @@ -1152,6 +1452,13 @@ jobs: with: egress-policy: audit + # before the download: checkout empties the workspace + - name: "Checkout" + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + with: + persist-credentials: false + sparse-checkout: .github/scripts + - name: "Download extension artifacts" uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: @@ -1159,20 +1466,16 @@ jobs: path: "turbo-artifacts" - name: "Arrange the distribution layout" - # phpstan_turbo--php[-zts] → - # /phpstan_turbo-[-zts]., derived from the - # artifact names so new targets need no change here. - # The integration/extension/other test workflows in phpstan/phpstan - # download this artifact next to the phar under test. + # each artifact's extension and the platform's core, mapped by + # .github/scripts/turbo-dist-layout.sh (which also refuses legs of + # one platform carrying different cores) — so new targets need no + # change here. The integration/extension/other test workflows in + # phpstan/phpstan download this artifact next to the phar under test. run: | - for dir in turbo-artifacts/phpstan_turbo-*; do - name="${dir#turbo-artifacts/phpstan_turbo-}" - target="${name%-php*}" - minor="${name##*-php}" - for file in "$dir"/*; do - install -D -m 644 "$file" "turbo-ext-dist/$target/phpstan_turbo-$minor.${file##*.}" - done - done + bash .github/scripts/turbo-dist-layout.sh turbo-artifacts > layout.tsv + while IFS=$'\t' read -r file dest; do + install -D -m 644 "$file" "turbo-ext-dist/$dest" + done < layout.tsv find turbo-ext-dist -type f | sort - name: "Upload aggregated artifact" @@ -1715,6 +2018,7 @@ jobs: run: | mkdir -p tmp/turbo-ext/windows-x86_64 cp tmp/turbo-artifact/php_phpstan_turbo.dll "tmp/turbo-ext/windows-x86_64/phpstan_turbo-$PHP_VERSION.dll" + cp tmp/turbo-artifact/phpstan_turbo_core*.dll tmp/turbo-ext/windows-x86_64/ - name: "Diagnose" # Without pcntl the main process cannot restart itself, so the @@ -2001,6 +2305,7 @@ jobs: needs: - compiler-tests - turbo-version + - turbo-shared-core-gate - turbo-compile - turbo-compile-musl-arm64 - turbo-compile-windows @@ -2095,20 +2400,15 @@ jobs: fi # A new compile-matrix leg (new PHP minor or platform) produces # binaries the dist does not carry even though the extension - # version is unchanged. - for dir in turbo-artifacts/phpstan_turbo-*; do - [ -d "$dir" ] || continue - name="${dir#turbo-artifacts/phpstan_turbo-}" - target="${name%-php*}" - minor="${name##*-php}" - for file in "$dir"/*; do - dist_file="phpstan-dist/turbo-ext/$target/phpstan_turbo-$minor.${file##*.}" - if [[ ! -f "$dist_file" ]]; then - echo "missing from dist: $dist_file" - RESULT=different - fi - done - done + # version is unchanged. The layout script maps each artifact file + # to its dist path (extensions and the platforms' cores). + bash phpstan-src/.github/scripts/turbo-dist-layout.sh turbo-artifacts > turbo-layout.tsv + while IFS=$'\t' read -r _ dest; do + if [[ ! -f "phpstan-dist/turbo-ext/$dest" ]]; then + echo "missing from dist: phpstan-dist/turbo-ext/$dest" + RESULT=different + fi + done < turbo-layout.tsv # A retired binary still in the dist needs a commit deleting it, # even at an unchanged extension version. for pattern in $TURBO_RETIRED_BINARIES; do @@ -2191,15 +2491,13 @@ jobs: echo "::error::no turbo extension artifacts were downloaded" MISSING=yes fi - for dist_file in phpstan-dist/turbo-ext/*/phpstan_turbo-*; do + # every binary the dist carries — extensions and cores alike — + # needs a freshly built replacement in the layout + for dist_file in phpstan-dist/turbo-ext/*/phpstan_turbo*; do [ -f "$dist_file" ] || continue rel="${dist_file#phpstan-dist/turbo-ext/}" - target="${rel%%/*}" - base="${rel##*/}" - minor="${base#phpstan_turbo-}" - minor="${minor%.*}" - if ! ls "turbo-artifacts/phpstan_turbo-$target-php$minor"/* >/dev/null 2>&1; then - echo "::error::$dist_file has no freshly built replacement (artifact phpstan_turbo-$target-php$minor missing) - refusing to bump turbo-ext/.version over a stale binary." + if ! cut -f2 turbo-layout.tsv | grep -qxF "$rel"; then + echo "::error::$dist_file has no freshly built replacement in the downloaded artifacts - refusing to bump turbo-ext/.version over a stale binary." MISSING=yes fi done @@ -2207,18 +2505,12 @@ jobs: exit 1 fi fi - for dir in turbo-artifacts/phpstan_turbo-*; do - [ -d "$dir" ] || continue - name="${dir#turbo-artifacts/phpstan_turbo-}" - target="${name%-php*}" - minor="${name##*-php}" - for file in "$dir"/*; do - dest="phpstan-dist/turbo-ext/$target/phpstan_turbo-$minor.${file##*.}" - if [[ "$VERSION_CHANGED" == "yes" || ! -f "$dest" ]]; then - install -D -m 644 "$file" "$dest" - fi - done - done + while IFS=$'\t' read -r file rel; do + dest="phpstan-dist/turbo-ext/$rel" + if [[ "$VERSION_CHANGED" == "yes" || ! -f "$dest" ]]; then + install -D -m 644 "$file" "$dest" + fi + done < turbo-layout.tsv echo "$TURBO_VERSION" > phpstan-dist/turbo-ext/.version - name: "Install lucky_commit" diff --git a/src/Turbo/TurboExtensionEnabler.php b/src/Turbo/TurboExtensionEnabler.php index a7155bcc38f..88c83cac563 100644 --- a/src/Turbo/TurboExtensionEnabler.php +++ b/src/Turbo/TurboExtensionEnabler.php @@ -33,7 +33,7 @@ final class TurboExtensionEnabler { - public const EXPECTED_EXTENSION_VERSION = '48d4823'; + public const EXPECTED_EXTENSION_VERSION = '28c29b8'; private static bool $active = false; diff --git a/src/Turbo/TurboExtensionSelector.php b/src/Turbo/TurboExtensionSelector.php index 5bfbffcd861..84f8f361dcd 100644 --- a/src/Turbo/TurboExtensionSelector.php +++ b/src/Turbo/TurboExtensionSelector.php @@ -25,7 +25,11 @@ * The binaries are committed to the phpstan/phpstan repository next to * phpstan.phar (turbo-ext//phpstan_turbo-.so, .dll on * Windows) by the phar.yml commit job, so they only exist for phar-based - * installations — + * installations. Each is a thin per-PHP-version library loading the + * platform's shared core from its own directory (phpstan_turbo_core.so; + * phpstan_turbo_core.dll and phpstan_turbo_core-zts.dll on Windows, whose + * thread-safe PHP is a different DLL to link against), so both must be + * there — * a source checkout loads its locally built extension through php.ini * instead. Only non-debug builds for PHP >= MINIMUM_PHP_VERSION_ID are * shipped; a ZTS variant (-zts filename @@ -105,9 +109,30 @@ public static function findExtension(): ?string return null; } + // without its core next to it, PHP would warn at startup that it + // cannot load the extension + if (!is_file(dirname($file) . '/' . self::resolveCoreFileName(PHP_OS_FAMILY, (bool) PHP_ZTS))) { + return null; + } + return $file; } + /** + * The shared core the extension binary loads, in the same directory. + */ + public static function resolveCoreFileName(string $osFamily, bool $zts): string + { + if ($osFamily === 'Windows') { + return $zts ? 'phpstan_turbo_core-zts.dll' : 'phpstan_turbo_core.dll'; + } + + // one core serves the thread-safe and the non-thread-safe extension: + // it reaches the engine's globals through the pointers the extension + // hands it, never through TSRM itself + return 'phpstan_turbo_core.so'; + } + public static function resolvePlatformDirectory(string $osFamily, string $machine, bool $isMusl): ?string { if ($osFamily === 'Darwin') { diff --git a/tests/PHPStan/Turbo/TurboExtensionSelectorTest.php b/tests/PHPStan/Turbo/TurboExtensionSelectorTest.php index a4dc6d09f9c..513fdee3b20 100644 --- a/tests/PHPStan/Turbo/TurboExtensionSelectorTest.php +++ b/tests/PHPStan/Turbo/TurboExtensionSelectorTest.php @@ -35,6 +35,24 @@ public function testResolvePlatformDirectory(string $osFamily, string $machine, $this->assertSame($expected, TurboExtensionSelector::resolvePlatformDirectory($osFamily, $machine, $isMusl)); } + /** + * @return iterable + */ + public static function dataResolveCoreFileName(): iterable + { + yield ['Linux', false, 'phpstan_turbo_core.so']; + yield ['Linux', true, 'phpstan_turbo_core.so']; + yield ['Darwin', false, 'phpstan_turbo_core.so']; + yield ['Windows', false, 'phpstan_turbo_core.dll']; + yield ['Windows', true, 'phpstan_turbo_core-zts.dll']; + } + + #[DataProvider('dataResolveCoreFileName')] + public function testResolveCoreFileName(string $osFamily, bool $zts, string $expected): void + { + $this->assertSame($expected, TurboExtensionSelector::resolveCoreFileName($osFamily, $zts)); + } + /** * @return iterable */ diff --git a/turbo-ext/.gitignore b/turbo-ext/.gitignore index f97555fed6e..ecbe646f11f 100644 --- a/turbo-ext/.gitignore +++ b/turbo-ext/.gitignore @@ -2,6 +2,7 @@ /src/*.o /src/parser/*.o /phpstan_turbo.so +/phpstan_turbo_core.so /version.stamp # Generated and committed into the phpstan/turbo-ext subsplit by diff --git a/turbo-ext/CLAUDE.md b/turbo-ext/CLAUDE.md index b4d7e69b37d..581b24450bf 100644 --- a/turbo-ext/CLAUDE.md +++ b/turbo-ext/CLAUDE.md @@ -315,6 +315,29 @@ run no restart, so `-d extension=` is fine there.) Output identity: `--error-format=raw` runs in both modes must diff empty. +## Shared core: code must not depend on the PHP version's layout + +Everything outside `Abi.cpp`, `Shadow.cpp`, `TrustedTypes.cpp` and +`main.cpp` is compiled once per platform into the shared core and runs on +every supported PHP (README.md, "Shared core"). In those sources: + +- never read a `zend_class_entry` member after `ce_flags` directly — use + `PT_CE(ce, member)`, adding the member to `PT_ABI_CLASS_ENTRY_MEMBERS` if + it is new; object handlers through `PT_OBJ_HANDLER()` / `Z_OBJ_HANDLER()` + (add new ones to `PT_ABI_OBJECT_HANDLERS`), `internal_function.handler` + through `PT_INTERNAL_HANDLER()`, user `arg_info[i]` through `PT_ARG_INFO()`; +- a new `EG()`/`CG()` field fails to compile until it is added to + `PT_ABI_EG_FIELDS` / `PT_ABI_CG_FIELDS` — add it there; +- no `#if PHP_VERSION_ID`: `PT_ABI_SINCE(version, then, otherwise)`, or an + engine call whose signature differs goes into `Abi.cpp` behind a `pt_abi` + function pointer; +- the version-specific sources call the core only through `PT_CORE_API` + functions (never data), and the core never calls them. + +Run `turbo-ext/bin/shared-core/check-linux.sh` (needs Docker) after touching +engine-facing code; CI's `turbo-shared-core-gate` fails on any difference it +finds, and never dismiss one without naming its mechanism. + ## Zend-level gotchas (each of these cost real debugging time) - PHP literal `[]` is the read-only `zend_empty_array` in RODATA: diff --git a/turbo-ext/Makefile b/turbo-ext/Makefile index a84a8e7fa4c..79f43f2bb7f 100644 --- a/turbo-ext/Makefile +++ b/turbo-ext/Makefile @@ -207,25 +207,100 @@ CXXFLAGS += $(PGO_FLAGS) SOURCES := $(wildcard src/*.cpp) $(wildcard src/parser/*.cpp) OBJECTS := $(SOURCES:.cpp=.o) +# The shared core and the per-PHP-version extension (README.md, "Shared +# core"). The sources below are the only ones compiled against a PHP +# version's own layout; every other object compiles to the same code against +# each supported version's headers (bin/shared-core/gate-*.sh proves it), so +# it is built once per platform into the core. TerminateHandler.cpp goes into +# both: each links libstdc++ in statically. +SHIM_SOURCES := src/Abi.cpp src/Shadow.cpp src/TrustedTypes.cpp src/main.cpp +SHIM_OBJECTS := $(SHIM_SOURCES:.cpp=.o) src/TerminateHandler.o +CORE_OBJECTS := $(filter-out $(SHIM_SOURCES:.cpp=.o),$(OBJECTS)) +CORE_LIB := phpstan_turbo_core.so + +# `make SPLIT=1` builds the pair — phpstan_turbo_core.so and a +# phpstan_turbo.so loading it from its own directory — instead of the one +# self-contained phpstan_turbo.so a plain `make` (and the phpize build) +# produces; main.cpp then checks the core's version at startup +ifeq ($(SPLIT),1) +CXXFLAGS += -DPHPSTANTURBO_SPLIT +endif + +ifeq ($(UNAME_S),Darwin) +CORE_LINK_FLAGS := -install_name @loader_path/$(CORE_LIB) +EXTENSION_LINK_FLAGS := +else +# --exclude-libs: the statically folded libstdc++ stays out of the dynamic +# symbol tables of both, so neither binds the other's copy +CORE_LINK_FLAGS := -Wl,-soname,$(CORE_LIB) -Wl,--exclude-libs,ALL +EXTENSION_LINK_FLAGS := -Wl,-rpath,'$$ORIGIN' -Wl,--exclude-libs,ALL +endif + +# the symbols a shared object exports — a stripped one keeps only its +# dynamic table on ELF +ifeq ($(UNAME_S),Darwin) +NM_EXPORTS := nm -g +else +NM_EXPORTS := nm -D --defined-only +endif + +# `make` builds the extension (with SPLIT=1, the pair) +.DEFAULT_GOAL := phpstan_turbo.so + +# the objects alone — what bin/shared-core/gate-*.sh compare between the PHP +# versions' headers +objects: $(OBJECTS) +.PHONY: objects + +# a shared object links with undefined symbols allowed (the engine's are +# resolved at load time), so a helper declared but never defined only +# surfaces as a jump to NULL at run time — fail the build instead; the +# extension's may come from the core, which must then export them +define pt_check_undefined + @undefined=$$(nm -u $(1) | grep -E 'pt_[a-z_]+|phpstanturbo' | awk '{print $$NF}' | while read -r sym; do \ + if [ -n "$(2)" ] && $(NM_EXPORTS) $(2) 2>/dev/null | awk '$$2 != "U" {print $$3}' | grep -qx "$$sym"; then continue; fi; \ + echo "$$sym"; done); \ + if [ -n "$$undefined" ]; then echo "undefined extension symbols in $(1):"; echo "$$undefined"; rm -f $(1); exit 1; fi +endef + +ifeq ($(SPLIT),1) +phpstan_turbo.so: $(SHIM_OBJECTS) $(CORE_LIB) + $(CXX) `$(PHP_CONFIG) --ldflags` -shared $(LTO_FLAGS) $(LINK_FLAGS) $(EXTENSION_LINK_FLAGS) $(PGO_FLAGS) -o $@ $(SHIM_OBJECTS) $(CORE_LIB) + $(call pt_check_undefined,$@,$(CORE_LIB)) +else phpstan_turbo.so: $(OBJECTS) $(CXX) `$(PHP_CONFIG) --ldflags` -shared $(LTO_FLAGS) $(LINK_FLAGS) $(PGO_FLAGS) -o $@ $(OBJECTS) - @# a shared object links with undefined symbols allowed (the engine's are - @# resolved at load time), so a helper declared but never defined only - @# surfaces as a jump to NULL at run time — fail the build instead - @if nm -u $@ | grep -E 'pt_[a-z_]+|phpstanturbo' > /dev/null; then echo "undefined extension symbols in $@:"; nm -u $@ | grep -E 'pt_[a-z_]+|phpstanturbo'; rm -f $@; exit 1; fi + $(call pt_check_undefined,$@,) +endif + +$(CORE_LIB): $(CORE_OBJECTS) + $(CXX) `$(PHP_CONFIG) --ldflags` -shared $(LTO_FLAGS) $(LINK_FLAGS) $(CORE_LINK_FLAGS) $(PGO_FLAGS) -o $@ $(CORE_OBJECTS) + $(call pt_check_undefined,$@,) + +# the extension alone, against a $(CORE_LIB) already in place (built for +# another PHP version, by the CI job building the platform's core): the +# per-version CI legs compile only $(SHIM_SOURCES) +extension: $(SHIM_OBJECTS) + @test -f $(CORE_LIB) || { echo "$(CORE_LIB) is missing — build it, or put the platform's core here"; exit 1; } + $(CXX) `$(PHP_CONFIG) --ldflags` -shared $(LTO_FLAGS) $(LINK_FLAGS) $(EXTENSION_LINK_FLAGS) $(PGO_FLAGS) -o phpstan_turbo.so $(SHIM_OBJECTS) $(CORE_LIB) + $(call pt_check_undefined,phpstan_turbo.so,$(CORE_LIB)) +.PHONY: extension # The distributed binaries ship without symbol tables: a quarter of the .so # (2.4 MB on Linux, 3.2 MB on macOS) that the loader never reads, since the # only symbol PHP looks up is get_module, which stays in the dynamic table. # Local builds keep them — sample and perf name frames from them. Not a # dependency of phpstan_turbo.so on purpose: this runs after `make pgo`, and -# must never rebuild what that produced with other flags. +# must never rebuild what that produced with other flags. With SPLIT=1 the +# core too; a plain `make strip` leaves a core put in place untouched (a +# per-version leg must ship the core leg's bytes — on macOS restripping +# would also invalidate its signature). strip: @test -f phpstan_turbo.so || { echo "phpstan_turbo.so is missing — build it first"; exit 1; } ifeq ($(UNAME_S),Darwin) - strip -x phpstan_turbo.so + strip -x phpstan_turbo.so $(if $(filter 1,$(SPLIT)),$(CORE_LIB)) else - strip --strip-unneeded phpstan_turbo.so + strip --strip-unneeded phpstan_turbo.so $(if $(filter 1,$(SPLIT)),$(CORE_LIB)) endif # every object depends on every header: the op tables and the trait helper @@ -235,7 +310,7 @@ HEADERS := $(wildcard src/*.h) $(wildcard src/generated/*.h) src/%.o: src/%.cpp $(HEADERS) $(CXX) $(CXXFLAGS) -c -o $@ $< -src/main.o: version.stamp +src/main.o src/Core.o: version.stamp version.stamp: FORCE @echo '$(PHPSTANTURBO_VERSION)' | cmp -s - $@ 2>/dev/null || echo '$(PHPSTANTURBO_VERSION)' > $@ @@ -257,7 +332,7 @@ pgo: $(MAKE) pgo-clean $(MAKE) PGO_FLAGS="$(PGO_GEN_FLAGS)" phpstan_turbo.so PGO_DIR="$(PGO_DIR)" bin/pgo-train.sh - rm -f $(OBJECTS) phpstan_turbo.so + rm -f $(OBJECTS) phpstan_turbo.so $(CORE_LIB) ifeq ($(CXX_IS_CLANG),1) $(LLVM_PROFDATA) merge -output=$(PGO_DIR)/turbo.profdata $(PGO_DIR)/*.profraw endif @@ -268,7 +343,7 @@ endif LLVM_PROFDATA ?= $(shell command -v llvm-profdata 2>/dev/null || (command -v xcrun > /dev/null 2>&1 && echo "xcrun llvm-profdata") || ls /usr/bin/llvm-profdata-* 2>/dev/null | sort -V | tail -1) pgo-clean: - rm -f $(OBJECTS) phpstan_turbo.so version.stamp + rm -f $(OBJECTS) phpstan_turbo.so $(CORE_LIB) version.stamp rm -rf $(PGO_DIR) find src -name '*.gcda' -delete diff --git a/turbo-ext/README.md b/turbo-ext/README.md index c266e056000..3ab0afa499b 100644 --- a/turbo-ext/README.md +++ b/turbo-ext/README.md @@ -373,6 +373,176 @@ environment, falling back to `VERSION.txt`). [php-sdk-binary-tools]: https://github.com/php/php-sdk-binary-tools +## Shared core + +The distributed binaries are not one self-contained extension per PHP +version and platform: each platform ships one **shared core** with all the +code, and per PHP version a **thin extension** (~70 KB) that PHP loads and +that loads the core from its own directory: + +``` +turbo-ext/linux-gnu-x86_64/phpstan_turbo_core.so the core (one per platform) +turbo-ext/linux-gnu-x86_64/phpstan_turbo-8.3.so the extension PHP 8.3 loads +turbo-ext/linux-gnu-x86_64/phpstan_turbo-8.6-zts.so ... and thread-safe 8.6 (same core) +turbo-ext/windows-x86_64/phpstan_turbo_core.dll Windows: one core per +turbo-ext/windows-x86_64/phpstan_turbo_core-zts.dll thread-safety flavour +``` + +A self-contained build per version used to repeat ~7 MB of identical +machine code 33 times; the split keeps the same code once per platform. + +### What is version-specific + +Four sources are compiled per PHP version into the extension — the +Makefile's `SHIM_SOURCES`, and the list in `config.w32`: + +- `main.cpp` — the module entry (it carries the engine's API number), the + version handshake with the core, and on Windows the loading of the core; +- `Abi.cpp` — fills the `pt_abi` table (`abi.h`) at startup with everything + of the running engine whose layout, value or signature differs between + minors, and implements per minor what cannot be expressed as data; +- `Shadow.cpp`, `TrustedTypes.cpp` — they build `zend_class_entry` / + `zend_op_array` structures by hand and walk them. + +Everything else is the core, compiled once per platform against any one +version's headers. That is only correct because it compiles to **identical +code** against every supported version's headers — the shared-core gate +(below) proves it on every change. The shared sources reach anything whose +layout moved through `abi.h`: + +| what changed between minors | in shared code | +|---|---| +| `EG()`/`CG()` field offsets (every minor) | `EG()`/`CG()` are redefined to read through pointers (`PT_ABI_EG_FIELDS`) | +| `zend_class_entry` members after `ce_flags` (8.6 inserted `ce_flags2`) | `PT_CE(ce, member)` (refuses anything but a class entry) | +| `zend_object_handlers` members (8.5 inserted `clone_obj_with`) | `PT_OBJ_HANDLER(obj, member)`, `Z_OBJ_HANDLER()` | +| `zend_internal_function.handler` (8.4), `zend_arg_info` stride (8.6) | `PT_INTERNAL_HANDLER(fn)`, `PT_ARG_INFO(argInfo, i)` | +| `zend_function_entry` (8.4, 8.6 grew it) | `reg::FunctionEntry`, turned into the engine's by `pt_abi` | +| `ZSTR_KNOWN()` ids, `IS_REFERENCE_EX`, `ZEND_ACC_USE_GUARDS`, lazy-object flags | `pt_abi` values (`PT_ABI_KNOWN_STRINGS`, …) | +| ZPP error codes / `Z_EXPECTED_*` numbering, parse slow paths (8.6) | neutral ZPP macros in `abi.h`, translated by `Abi.cpp` | +| functions whose signature or behaviour changed (`zend_create_closure`, `zend_dval_to_lval`, …) or that stopped being exported (`zend_call_known_function`, …) | function pointers in `pt_abi`, implemented in `Abi.cpp` | +| `#if PHP_VERSION_ID` gates | `PT_ABI_SINCE(version, then, otherwise)`, decided at run time | +| helpers re-spelled without changing behaviour (macro → inline function) | one definition pinned in `abi.h` ("codegen pins") | + +The dependency runs one way: the extension calls the core (the functions +marked `PT_CORE_API` — the only symbols the core exports — and only +functions, since a Windows DLL can delay-load functions but not data); the +core never references the extension. The extension refuses a core of +another version (`pt_core_version()`): it registers nothing and reports +`core-mismatch`, which `TurboExtensionEnabler` rejects, so a torn update +runs PHPStan without the extension rather than mixing builds. + +Loading: on Linux the extension records the core as a dependency found next +to itself (`DT_NEEDED` + `$ORIGIN`), on macOS likewise (`@loader_path`). On +Windows the dependency is delay-loaded: PHP loads extensions with plain +`LoadLibrary()`, which searches the application's directory, not the +extension's, so `get_module()` (`main.cpp`) loads the core by its full path +first and the delay-load helper finds it by name. One Linux core serves the +thread-safe and the non-thread-safe extension (the core never touches TSRM); +Windows needs two, since a DLL imports the engine from `php8.dll` or +`php8ts.dll` by name. + +### Building the pair + +```bash +cd turbo-ext +make SPLIT=1 # phpstan_turbo_core.so + phpstan_turbo.so (loads the core next to it) +make SPLIT=1 pgo # the same, profile-guided (what turbo-compile-core runs) +make SPLIT=1 extension # only the extension, against a phpstan_turbo_core.so already here +make SPLIT=1 strip # both; a plain `make strip` leaves the core untouched +``` + +A plain `make`, the phpize build (`config.m4`, what PIE drives) and +`configure --enable-phpstan-turbo` on Windows still build one +self-contained extension. On Windows, `configure --enable-phpstan-turbo=core` +builds the core DLL and its import library, and `=extension` the extension +against them (the import library expected in `turbo-ext/`). + +In CI (`.github/workflows/phar.yml`), `turbo-compile-core*` build one core +per platform — profile-guided, against `php-version` of its matrix (Windows: +against 8.3 with VS16, the oldest toolset, since PHP checks the toolset of +the DLL it loads, the extension, and a core linked with an older one runs on +any newer VC runtime) — and are what `turbo-origins` reuses from earlier +runs; `turbo-compile*` link each version's extension against them in a +minute and upload both, so every test job finds the core next to the +extension. `.github/scripts/turbo-dist-layout.sh` maps the artifacts onto +the layout above for the aggregate artifact and the commit job, and refuses +legs of one platform carrying different cores. + +### The shared-core gate + +```bash +SHARED_CORE_WORK_DIR=/tmp/shared-core turbo-ext/bin/shared-core/check-linux.sh +``` + +compiles every source against each supported version's headers (thread-safe +8.6 included) in the CI build images with clang, and fails on any function, +data section or relocation that differs outside the four version-specific +objects. It runs as `turbo-shared-core-gate` in `phar.yml`, and the commit +job waits for it. clang, not the GCC the Linux binaries ship with: the +property proven is the source's (does it mean the same against every header +set?), and GCC's register allocation drifts with incidental header spelling +(a `const` added to a helper's parameter) where clang's output does not. +CI runs it on x86_64; run it for the other architecture too when the result +matters (`GATE_PLATFORM=linux/amd64` on an arm64 machine, or the reverse) — +some differences only exist in one architecture's code, like an `int` +parameter that became `size_t`, which arm64 passes with the same +instruction and x86_64 does not. + +A failure lists the functions. Find the cause with the tools next to it — +`gate-linux.sh` (any image/compiler, `GATE_VERSIONS`, `GATE_MAKE_ARGS`; +`PGO_FLAGS='-fno-inline -fno-ipa-sra -fno-ipa-cp -fno-partial-inlining'` +keeps each inline helper a function of its own, so the differing one shows +up by name), `compare-functions.py`, `compare-data-linux.sh`, and their +macOS counterparts (`gate-mac.sh`, `compare-data-mac.sh`, +`link-split-mac.sh`, with Homebrew's `php@8.x`); `link-split-linux.sh` links +a core from one version's objects with every version's extension and runs +`tests/smoke.php` with each. Never accept a difference as "compiler noise" +without naming its mechanism: one such difference was a `zend_arg_info` +whose 8.6 stride crashed the 8.4-built core on 8.6. Windows-only header +branches are not covered by the gate (it compiles Linux headers); the +Windows legs' tests are their check. + +### Supporting a new PHP version + +1. **Gate it.** Build the new version's CI image (`.github/turbo-build`), + add the version to `VERSIONS` in `bin/shared-core/check-linux.sh` and run + it. Every function it reports reads something that changed; for each, + find what (diff the headers' struct layouts — `sizeof`/`offsetof` of the + structs the function touches — macro values, enum numbering and + function declarations between the old and the new version) and route it + through `abi.h`/`Abi.cpp` by the table above: a new `PT_ABI_*` list + entry, a `PT_CE`-style accessor, a pin, or a function pointer + implemented in `Abi.cpp` with an `#if PHP_VERSION_ID` there. Repeat until + it reports nothing. +2. **Check the exports.** A function the new version stops exporting makes + the core fail to load on it, which the gate cannot see — the build legs + and `link-split-linux.sh` load it on every version. +3. **Version-specific sources.** `Abi.cpp`, `Shadow.cpp`, `TrustedTypes.cpp` + and `main.cpp` compile against the new headers like any extension: fix + them with `#if PHP_VERSION_ID` as needed. +4. **CI.** In `.github/workflows/phar.yml`, add the version to the + `php-version` lists of the thin legs (`turbo-compile`, + `turbo-compile-musl-arm64`, `turbo-compile-windows` — with its `vs` + infix and toolset in `include`) and of the jobs testing them + (`turbo-differential`, `turbo-differential-musl`, `turbo-run`, + `turbo-docker-run`), and give the new version a `gnu-php` image + for the Linux gnu legs. The cores need no change: they keep compiling + against the version they are pinned to (`php-version` in + `turbo-compile-core`'s targets, `["8.3"]` with the vs16 toolset in + `turbo-compile-core-windows`). Run the smoke test of every version + against one core (`link-split-linux.sh`); in CI the + `turbo-shared-core-gate` job reruns step 1. +5. **Dropping a version** is the reverse of step 4 plus + `TURBO_RETIRED_BINARIES` for the files phpstan/phpstan should delete. If + it is the version a core is pinned to, move that pin to a version still + supported — on Windows to the oldest one, because a DLL linked with a + newer toolset than the PHP loading it is refused (see + `turbo-compile-windows`). +6. **If a minor breaks too much to route** — a hot-path structure such as + `zval`, `zend_string` or `HashTable` changing layout — it gets a second + core: build the platform's core per group of minors and pick it by + version in `TurboExtensionSelector`. Nothing so far has needed it. + ### Build flags: hardening and size `make` applies protections by default, each *probed* against the compiler in diff --git a/turbo-ext/bin/shared-core/check-linux.sh b/turbo-ext/bin/shared-core/check-linux.sh new file mode 100755 index 00000000000..b0f2ff1d0ea --- /dev/null +++ b/turbo-ext/bin/shared-core/check-linux.sh @@ -0,0 +1,68 @@ +#!/bin/bash +# The shared-core gate (phar.yml turbo-shared-core-gate, and locally): the +# core is compiled once per platform and runs on every supported PHP, so +# every source but the version-specific ones (the Makefile's SHIM_SOURCES) +# must compile to the same code against each version's headers — thread-safe +# ones included. Builds them all in the CI images with clang (whose output +# does not drift with header spelling the way GCC's register allocation +# does; the property proven is the source's, and the GCC build ships it), +# then fails on any function, data section or relocation that differs +# outside Abi.o, Shadow.o, TrustedTypes.o and main.o. +# +# Needs docker and an llvm-objdump (LLVM_OBJDUMP, else the newest found). +# Usage: SHARED_CORE_WORK_DIR= check-linux.sh +set -euo pipefail + +S=${SHARED_CORE_WORK_DIR:?set SHARED_CORE_WORK_DIR to a scratch directory} +HERE=$(cd "$(dirname "$0")" && pwd) +VERSIONS=${GATE_VERSIONS:-8.3 8.4 8.5 8.6 8.6-zts} +SHIM_OBJECTS='src/(Abi|Shadow|TrustedTypes|main)\.o' +if [ -z "${LLVM_OBJDUMP:-}" ]; then + LLVM_OBJDUMP=$(command -v llvm-objdump || { ls /usr/bin/llvm-objdump-* /opt/homebrew/opt/llvm/bin/llvm-objdump 2>/dev/null || true; } | sort -V | tail -1) +fi +export LLVM_OBJDUMP +[ -x "$LLVM_OBJDUMP" ] || { echo "no llvm-objdump found (set LLVM_OBJDUMP)"; exit 2; } + +# the CI build images plus clang, built once per run +for v in $VERSIONS; do + printf 'FROM ghcr.io/phpstan/turbo-build:gnu-php%s\nRUN apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq clang-14 > /dev/null && rm -rf /var/lib/apt/lists/*\n' "$v" \ + | docker build -q ${GATE_PLATFORM:+--platform "$GATE_PLATFORM"} -t "turbo-build-clang:gnu-php$v" - > /dev/null +done + +# -fgnuc-version=4.3: the headers before 8.6 enable ZEND_COLD for GCC 4.3+ +# only and clang reports 4.2 by default; -U_FORTIFY_SOURCE: glibc's fortify +# wrappers then expect GCC builtins clang lacks. Neither matters to the +# property — they keep the header sets' compiler-identity branches alike. +GATE_IMAGE=turbo-build-clang \ +GATE_VERSIONS="$VERSIONS" \ +GATE_MAKE_ARGS="CXX=clang++-14 PGO_FLAGS='-fgnuc-version=4.3 -U_FORTIFY_SOURCE'" \ + "$HERE/gate-linux.sh" + +cd "$S" +first=$(echo "$VERSIONS" | awk '{print $1}') +failed=0 +for v in $VERSIONS; do + [ "$v" = "$first" ] && continue + python3 -I "$HERE/compare-functions.py" "lx-$first" "lx-$v" > /dev/null + functions=$(grep -v -E "$SHIM_OBJECTS" "diff-lx-$v.txt" || true) + data=$("$HERE/compare-data-linux.sh" "lx-$first" "lx-$v" | grep -v -E "$SHIM_OBJECTS" || true) + if [ -n "$functions$data" ]; then + failed=1 + echo "::error::shared code compiles differently against PHP $first and PHP $v headers" + [ -n "$functions" ] && echo "$functions" | while read -r size object name; do + echo " $object: $(echo "$name" | c++filt) ($size bytes)" + done + [ -n "$data" ] && echo "$data" | sed 's/^/ /' + else + echo "PHP $first vs PHP $v: shared code identical" + fi +done +if [ "$failed" = 1 ]; then + cat <<'EOF' +A source outside the version-specific ones reads something whose layout, +value or signature differs between PHP versions. Route it through pt_abi +(abi.h, filled by Abi.cpp) or move the code into a version-specific source; +turbo-ext/README.md ("Shared core") lists the mechanisms. +EOF +fi +exit "$failed" diff --git a/turbo-ext/bin/shared-core/compare-data-linux.sh b/turbo-ext/bin/shared-core/compare-data-linux.sh new file mode 100755 index 00000000000..ff2f9c39ecb --- /dev/null +++ b/turbo-ext/bin/shared-core/compare-data-linux.sh @@ -0,0 +1,14 @@ +#!/bin/bash +# compare every object's non-code section contents and all relocations +# between two builds (ELF; one objdump call per object and kind) +S=${SHARED_CORE_WORK_DIR:?set SHARED_CORE_WORK_DIR to a scratch directory} +O=${LLVM_OBJDUMP:-/opt/homebrew/opt/llvm/bin/llvm-objdump} +A=$1; B=$2 +norm_relocs() { $O -r "$1" | grep -v 'file format' | sed -E 's/(\.L[A-Za-z_.]*)\.?[0-9]+/\1/g; s/16ZEND_RESULT_CODE/11zend_result/g'; } +data() { $O -s "$1" | sed 's/16ZEND_RESULT_CODE/11zend_result/g' | awk '/^Contents of section / { keep = ($4 !~ /^\.(text|comment|strtab|shstrtab|symtab)/) } keep'; } +cd "$S/$A" +for o in $(find src -name '*.o' | sort); do + [ "$(data $o | cksum)" != "$(data $S/$B/$o | cksum)" ] && echo "DATA DIFF $o" + [ "$(norm_relocs $o | cksum)" != "$(norm_relocs $S/$B/$o | cksum)" ] && echo "RELOC DIFF $o" +done +true diff --git a/turbo-ext/bin/shared-core/compare-data-mac.sh b/turbo-ext/bin/shared-core/compare-data-mac.sh new file mode 100755 index 00000000000..df60424c0d1 --- /dev/null +++ b/turbo-ext/bin/shared-core/compare-data-mac.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# compare every non-code section (contents + relocations) of each object between two builds +S=${SHARED_CORE_WORK_DIR:?set SHARED_CORE_WORK_DIR to a scratch directory} +O=/opt/homebrew/opt/llvm/bin/llvm-objdump +A=$1; B=$2 +cd $S/$A +for o in $(find src -name '*.o' | sort); do + sects=$($O -h $o | awk 'NR>3 && $2 !~ /__text|__compact_unwind|__eh_frame/ && $2 != "" {print $2}' | sort -u) + for sec in $sects; do + a=$($O -s -j "$sec" $o 2>/dev/null | tail -n +3 | md5) + b=$($O -s -j "$sec" $S/$B/$o 2>/dev/null | tail -n +3 | md5) + [ "$a" != "$b" ] && echo "DATA DIFF $o $sec" + done + ra=$($O -r $o | grep -v 'file format' | sed -E 's/OUTLINED_FUNCTION_[0-9]+/OF/; s/l_\.str[.0-9]*/l_.str/; s/lCPI[0-9_]+/lCPI/; s/ltmp[0-9]+/ltmp/; s/\$_[0-9]+/$_/' | md5) + rb=$($O -r $S/$B/$o | grep -v 'file format' | sed -E 's/OUTLINED_FUNCTION_[0-9]+/OF/; s/l_\.str[.0-9]*/l_.str/; s/lCPI[0-9_]+/lCPI/; s/ltmp[0-9]+/ltmp/; s/\$_[0-9]+/$_/' | md5) + [ "$ra" != "$rb" ] && echo "RELOC DIFF $o" +done diff --git a/turbo-ext/bin/shared-core/compare-functions.py b/turbo-ext/bin/shared-core/compare-functions.py new file mode 100644 index 00000000000..4a2ba0c4fac --- /dev/null +++ b/turbo-ext/bin/shared-core/compare-functions.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +# Per-function fingerprinting of arm64 Mach-O object files built against +# different PHP versions: identical source -> identical bytes + relocations +# unless a Zend header (struct layout, inline function, macro) differs. +import hashlib, os, re, subprocess, sys +from concurrent.futures import ThreadPoolExecutor + +OBJDUMP = os.environ.get('LLVM_OBJDUMP', '/opt/homebrew/opt/llvm/bin/llvm-objdump') +func_re = re.compile(r'^<(.+)>:$') +addr_re = re.compile(r'0x[0-9a-f]+ <') + + +def fingerprints(obj): + out = subprocess.run([OBJDUMP, '-d', '-r', '--no-leading-addr', '--no-show-raw-insn', obj], + capture_output=True, text=True).stdout + funcs = {} + cur = None + lines = [] + n = 0 + def flush(): + if cur is not None: + funcs[cur] = (hashlib.sha1('\n'.join(lines).encode()).hexdigest(), n * 4) + for line in out.splitlines(): + m = func_re.match(line) + if m: + flush() + cur, lines, n = m.group(1).replace('16ZEND_RESULT_CODE', '11zend_result'), [], 0 + continue + if cur is None: + continue + s = line.strip() + if not s or s.startswith('Disassembly of section'): + continue + # branch targets print absolute section offsets; keep only the symbolic part + s = addr_re.sub('<', s) + s = re.sub(r'OUTLINED_FUNCTION_\d+', 'OUTLINED_FUNCTION', s) + if s.startswith('adrp') or s.startswith('adr\t'): + s = re.sub(r'<.*>', '', s) + s = re.sub(r'\$_\d+', '$_', s) + s = re.sub(r'\.cold\.\d+', '.cold', s) + s = re.sub(r'(l_\.str|ltmp|LJTI|lJTI|l___const|lCPI)[\.\d_]+', r'\1', s) + # 8.6 renamed enum ZEND_RESULT_CODE to zend_result: a different + # mangling of the same type in our own functions' names + s = s.replace('16ZEND_RESULT_CODE', '11zend_result') + # local labels: GCC's .LC0, .LANCHOR1, .L42 and clang's .L.str.356 — + # numbered in the order the translation unit declares them, so one + # more string literal in a header renumbers every later one (their + # contents are compared by compare-data-*.sh) + s = re.sub(r'(\.L[A-Za-z_.]*?)\.?\d+', r'\1', s) + # GCC addresses merged strings through the section of whichever + # function emitted them first: section order, not content + s = re.sub(r'\.rodata\.[^ +]*\.str1\.\d+(\+0x[0-9a-f]+)?', '.rodata.str', s) + # relocation lines print an offset column too + s = re.sub(r'^[0-9a-f]+:\s+', '', s) + lines.append(s) + if not s.startswith(('ARM64_RELOC', 'R_AARCH64', 'R_X86_64')): + n += 1 + flush() + return funcs + + +def collect(builddir): + objs = [] + for root, _, files in os.walk(os.path.join(builddir, 'src')): + objs += [os.path.join(root, f) for f in files if f.endswith('.o')] + res = {} + with ThreadPoolExecutor(10) as ex: + for obj, funcs in zip(objs, ex.map(fingerprints, objs)): + rel = os.path.relpath(obj, builddir) + for name, v in funcs.items(): + res[(rel, name)] = v + return res + + +def main(): + dirs = sys.argv[1:] + data = [collect(d) for d in dirs] + base = data[0] + total = sum(sz for _, sz in base.values()) + print(f'{dirs[0]}: {len(base)} functions, {total/1048576:.2f} MB code') + for d, other in zip(dirs[1:], data[1:]): + same = diff = only = 0 + per_file = {} + for k, (h, sz) in base.items(): + if k not in other: + only += sz + continue + if other[k][0] == h: + same += sz + else: + diff += sz + per_file.setdefault(k[0], [0, 0]) + per_file[k[0]][0] += sz + per_file[k[0]][1] += 1 + newonly = sum(sz for k, (_, sz) in other.items() if k not in base) + print(f'vs {d}: identical {same/1048576:.2f} MB ({100*same/total:.1f}%), differing {diff/1048576:.2f} MB ({100*diff/total:.1f}%), ' + f'only in base {only/1048576:.2f} MB, only in other {newonly/1048576:.2f} MB') + with open(f'diff-{os.path.basename(d)}.txt', 'w') as fh: + for k, (h, sz) in sorted(base.items(), key=lambda x: -x[1][1]): + if k in other and other[k][0] != h: + fh.write(f'{sz:8d} {k[0]} {k[1]}\n') + print(' top files by differing bytes:') + for f, (sz, cnt) in sorted(per_file.items(), key=lambda x: -x[1][0])[:12]: + print(f' {f:50s} {sz/1024:8.1f} KB in {cnt} fns') + # identical across all + if len(data) > 2: + allsame = sum(sz for k, (h, sz) in base.items() if all(k in o and o[k][0] == h for o in data[1:])) + print(f'identical across all {len(dirs)}: {allsame/1048576:.2f} MB ({100*allsame/total:.1f}%)') + + +main() diff --git a/turbo-ext/bin/shared-core/gate-linux.sh b/turbo-ext/bin/shared-core/gate-linux.sh new file mode 100755 index 00000000000..01498b659d8 --- /dev/null +++ b/turbo-ext/bin/shared-core/gate-linux.sh @@ -0,0 +1,24 @@ +#!/bin/bash +# The Linux counterpart of gate-mac.sh: compiles turbo-ext in the CI build +# images (ghcr.io/phpstan/turbo-build:gnu-php, GCC 11.4) against each +# PHP version's headers — LTO off, so the objects hold machine code — and +# compares the results function by function. +S=${SHARED_CORE_WORK_DIR:?set SHARED_CORE_WORK_DIR to a scratch directory} +WT=$(cd "$(dirname "$0")/../.." && pwd) +VERSIONS=${GATE_VERSIONS:-8.3 8.4 8.5 8.6} +JOBS=${GATE_JOBS:-3} +# GATE_PLATFORM (e.g. linux/amd64) runs the images of another architecture — +# code generation differs between them, and CI gates on x86_64 +rm -f "$S"/lx-*.failed +for v in $VERSIONS; do + mkdir -p "$S/lx-$v" + rsync -a --exclude '*.o' --exclude '*.so' --exclude PHP-CPP --exclude pgo --exclude version.stamp "$WT/" "$S/lx-$v/" + echo "${GATE_VERSION_TXT:-dev}" > "$S/lx-$v/VERSION.txt" + docker run --rm ${GATE_PLATFORM:+--platform "$GATE_PLATFORM"} -v "$S/lx-$v:/build" -w /build "${GATE_IMAGE:-ghcr.io/phpstan/turbo-build}:gnu-php$v" \ + sh -c "make -k -j$JOBS LTO_FLAGS= WARN_FLAGS=\"${GATE_WARN_FLAGS:--Wall}\" ${GATE_MAKE_ARGS} ${GATE_TARGET:-objects} > build.log 2>&1" \ + || { touch "$S/lx-$v.failed"; echo "BUILD FAILED $v"; grep -E ': (error|warning):' "$S/lx-$v/build.log" | head -20; } & +done +wait +ls "$S"/lx-*.failed >/dev/null 2>&1 && exit 1 +first=$(echo $VERSIONS | awk '{print $1}') +cd "$S" && python3 -I "$WT/bin/shared-core/compare-functions.py" $(for v in $VERSIONS; do printf 'lx-%s ' $v; done) diff --git a/turbo-ext/bin/shared-core/gate-mac.sh b/turbo-ext/bin/shared-core/gate-mac.sh new file mode 100755 index 00000000000..72c1bc5f9fb --- /dev/null +++ b/turbo-ext/bin/shared-core/gate-mac.sh @@ -0,0 +1,13 @@ +#!/bin/bash +# Build turbo-ext against PHP 8.3/8.4/8.5 headers (objects +# kept between runs for incremental builds) and compare per function. +S=${SHARED_CORE_WORK_DIR:?set SHARED_CORE_WORK_DIR to a scratch directory} +WT=$(cd "$(dirname "$0")/../.." && pwd) +rm -f "$S"/g-*.failed +for v in 8.3 8.4 8.5; do + rsync -a --exclude '*.o' --exclude 'phpstan_turbo.so' --exclude PHP-CPP --exclude pgo --exclude version.stamp "$WT/" "$S/g-$v/" + (cd "$S/g-$v" && make -k -j4 ${GATE_MAKE_ARGS} PHP_CONFIG=/opt/homebrew/opt/php@$v/bin/php-config ${GATE_TARGET:-phpstan_turbo.so} > build.log 2>&1 || { touch "$S/g-$v.failed"; echo "BUILD FAILED $v"; grep -E 'error' build.log; }) & +done +wait +ls "$S"/g-*.failed >/dev/null 2>&1 && exit 1 +cd "$S" && python3 -I "$WT/bin/shared-core/compare-functions.py" g-8.3 g-8.4 g-8.5 diff --git a/turbo-ext/bin/shared-core/link-split-linux.sh b/turbo-ext/bin/shared-core/link-split-linux.sh new file mode 100755 index 00000000000..966744809c4 --- /dev/null +++ b/turbo-ext/bin/shared-core/link-split-linux.sh @@ -0,0 +1,38 @@ +#!/bin/bash +# The Linux counterpart of link-split-mac.sh, on the objects gate-linux.sh +# built: one core shared object linked from the $CORE_FROM objects, one thin +# extension per PHP version linked against it (found next to itself through +# $ORIGIN), each loaded in its version's CI image, which then runs the smoke +# test with it. +set -e +S=${SHARED_CORE_WORK_DIR:?set SHARED_CORE_WORK_DIR to a scratch directory} +REPO=$(cd "$(dirname "$0")/../../.." && pwd) +VERSIONS=${GATE_VERSIONS:-8.3 8.4 8.5 8.6} +CORE_FROM=${CORE_FROM:-8.4} +OUT=$S/split-linux +rm -rf "$OUT"; mkdir -p "$OUT" + +SHIM='src/Abi.o src/Shadow.o src/TrustedTypes.o src/main.o src/TerminateHandler.o' +# the link flags of the Makefile's Linux build; --exclude-libs keeps the +# statically folded libstdc++ out of both dynamic symbol tables +FLAGS='-static-libstdc++ -static-libgcc -Wl,--exclude-libs,ALL -Wl,--gc-sections -Wl,-z,relro,-z,now -Wl,-z,noexecstack' + +docker run --rm -v "$S/lx-$CORE_FROM:/build" -v "$OUT:/out" -w /build "ghcr.io/phpstan/turbo-build:gnu-php$CORE_FROM" sh -ec " + core=\$(find src -name '*.o' | grep -v -E 'src/(Abi|Shadow|TrustedTypes|main)\.o\$' | sort) + g++ -shared $FLAGS -Wl,-soname,libphpstan_turbo_core.so -o /out/libphpstan_turbo_core.so \$core + echo \"core exports: \$(nm -D --defined-only /out/libphpstan_turbo_core.so | wc -l)\" +" + +for v in $VERSIONS; do + docker run --rm -v "$S/lx-$v:/build" -v "$OUT:/out" -v "$REPO:/repo:ro" -w /build "ghcr.io/phpstan/turbo-build:gnu-php$v" sh -ec " + g++ -shared $FLAGS -Wl,-rpath,'\$ORIGIN' -o /out/phpstan_turbo-$v.so $SHIM -L/out -lphpstan_turbo_core + # every extension symbol must come from the core: undefined pt_* + # symbols would only fail at load time + missing=\$(nm -D --undefined-only /out/phpstan_turbo-$v.so | awk '{print \$2}' | grep -E '^(_Z.*phpstanturbo|_Z.*pt_|pt_)' | while read sym; do nm -D --defined-only /out/libphpstan_turbo_core.so | awk '{print \$3}' | grep -qx \"\$sym\" || echo \$sym; done) + [ -z \"\$missing\" ] || { echo \"PHP $v: undefined extension symbols: \$missing\"; exit 1; } + cd /repo + echo \"PHP $v: \$(php -n -d extension=/out/phpstan_turbo-$v.so -r 'echo PHP_VERSION, \" turbo=\", phpversion(\"phpstan_turbo\");')\" + TURBO_DLL=/out/phpstan_turbo-$v.so php -d memory_limit=4G -d extension=/out/phpstan_turbo-$v.so turbo-ext/tests/smoke.php 2>&1 | tail -1 + " || echo "PHP $v: FAILED" +done +ls -l "$OUT" diff --git a/turbo-ext/bin/shared-core/link-split-mac.sh b/turbo-ext/bin/shared-core/link-split-mac.sh new file mode 100755 index 00000000000..8439c408d77 --- /dev/null +++ b/turbo-ext/bin/shared-core/link-split-mac.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# Link one core dylib (from the objects of $CORE_FROM) and one thin extension per PHP version. +set -e +S=${SHARED_CORE_WORK_DIR:?set SHARED_CORE_WORK_DIR to a scratch directory} +N=/opt/homebrew/opt/llvm/bin/llvm-nm +CORE_FROM=${CORE_FROM:-g-8.4} +OUT=$S/split; rm -rf $OUT; mkdir -p $OUT +SHIM_RE='src/(Abi|Shadow|TrustedTypes|main)\.o$' +cd $S/$CORE_FROM +CORE=$(find src -name '*.o' | grep -v -E "$SHIM_RE" | sort) +SHIM="src/Abi.o src/Shadow.o src/TrustedTypes.o src/main.o" +# the boundary, both directions +$N -u $SHIM | grep -v ':$' | awk '{print $NF}' | sort -u > $OUT/shim-undef +$N --defined-only $CORE | grep -E ' [TDBSC] ' | awk '{print $3}' | sort -u > $OUT/core-def +comm -12 $OUT/shim-undef $OUT/core-def > $OUT/core.exp +$N -u $CORE | grep -v ':$' | awk '{print $NF}' | sort -u > $OUT/core-undef +$N --defined-only $SHIM | grep -E ' [TDBSC] ' | awk '{print $3}' | sort -u > $OUT/shim-def +comm -12 $OUT/core-undef $OUT/shim-def > $OUT/shim.exp +echo _get_module >> $OUT/shim.exp +c++ -shared -undefined dynamic_lookup -Wl,-dead_strip \ + -install_name @loader_path/libphpstan_turbo_core.dylib \ + -Wl,-exported_symbols_list,$OUT/core.exp \ + -o $OUT/libphpstan_turbo_core.dylib $CORE +for v in 8.3 8.4 8.5; do + cd $S/g-$v + c++ -shared -undefined dynamic_lookup -Wl,-dead_strip \ + -Wl,-exported_symbols_list,$OUT/shim.exp \ + -o $OUT/phpstan_turbo-$v.so $SHIM $OUT/libphpstan_turbo_core.dylib +done +ls -l $OUT/*.dylib $OUT/*.so diff --git a/turbo-ext/config.w32 b/turbo-ext/config.w32 index cb0367f25d7..23376c4ebe5 100644 --- a/turbo-ext/config.w32 +++ b/turbo-ext/config.w32 @@ -6,22 +6,47 @@ // builds there have no usable git history); with neither it defaults to // "dev", which the enabler rejects. -ARG_ENABLE("phpstan-turbo", "phpstan turbo extension", "no"); +// "yes" builds the one self-contained php_phpstan_turbo.dll; "core" and +// "extension" build the pair the distributed binaries are (README.md, +// "Shared core"): the core DLL once per thread-safety flavour, and per PHP +// version the extension DLL, which delay-loads the core from its own +// directory (main.cpp) and links against its import library — expected in +// the extension's directory when configuring with "extension". +ARG_ENABLE("phpstan-turbo", "phpstan turbo extension (yes, core or extension)", "no"); + +// The sources compiled against a PHP version's own layout — the Makefile's +// SHIM_SOURCES; everything else goes into the core. +var PHPSTAN_TURBO_SHIM_SOURCES = ["Abi.cpp", "Shadow.cpp", "TrustedTypes.cpp", "main.cpp"]; // Every .cpp file of a directory under the extension, sorted, as the // space-separated list EXTENSION() and ADD_SOURCES() take, each name behind // the given prefix — the sources are discovered like the Makefile's // wildcard and config.m4's glob, not listed. buildconf concatenates every // extension's config.w32 into one configure.js, hence the prefixed name. -function phpstan_turbo_sources(dir, prefix) +// `select` picks the shim sources (true), the others (false), or all +// (undefined); TerminateHandler.cpp only matters to the Linux static +// libstdc++ link and goes with the core. +function phpstan_turbo_sources(dir, prefix, select) { var names = []; var files = new Enumerator(FSO.GetFolder(configure_module_dirname + "\\" + dir).Files); for (; !files.atEnd(); files.moveNext()) { var name = FSO.GetFileName(files.item()); - if (/\.cpp$/i.test(name)) { - names.push(prefix + name); + if (!/\.cpp$/i.test(name)) { + continue; + } + if (typeof(select) != "undefined") { + var shim = false; + for (var i = 0; i < PHPSTAN_TURBO_SHIM_SOURCES.length; i++) { + if (PHPSTAN_TURBO_SHIM_SOURCES[i].toLowerCase() == name.toLowerCase()) { + shim = true; + } + } + if (shim != select) { + continue; + } } + names.push(prefix + name); } names.sort(); return names.join(" "); @@ -47,11 +72,30 @@ if (PHP_PHPSTAN_TURBO != "no") { // backslash-separated paths: confutils' ADD_SOURCES derives the object // directory by splitting on backslashes only — with forward slashes the // objects compile flat while the link list expects the subpath - EXTENSION("phpstan_turbo", - phpstan_turbo_sources("src", "src\\"), - true, - flags); - ADD_SOURCES(configure_module_dirname + "/src/parser", - phpstan_turbo_sources("src\\parser", ""), - "phpstan_turbo"); + var core = PHP_ZTS == "yes" ? "phpstan_turbo_core-zts" : "phpstan_turbo_core"; + if (PHP_PHPSTAN_TURBO == "core") { + EXTENSION("phpstan_turbo_core", + phpstan_turbo_sources("src", "src\\", false), + true, + flags + " /DPHPSTANTURBO_SPLIT /DPHPSTANTURBO_CORE_BUILD", + core + ".dll"); + ADD_SOURCES(configure_module_dirname + "/src/parser", + phpstan_turbo_sources("src\\parser", ""), + "phpstan_turbo_core"); + } else if (PHP_PHPSTAN_TURBO == "extension") { + EXTENSION("phpstan_turbo", + phpstan_turbo_sources("src", "src\\", true), + true, + flags + " /DPHPSTANTURBO_SPLIT"); + ADD_FLAG("LIBS_PHPSTAN_TURBO", configure_module_dirname + "\\" + core + ".lib delayimp.lib"); + ADD_FLAG("LDFLAGS_PHPSTAN_TURBO", "/DELAYLOAD:" + core + ".dll"); + } else { + EXTENSION("phpstan_turbo", + phpstan_turbo_sources("src", "src\\"), + true, + flags); + ADD_SOURCES(configure_module_dirname + "/src/parser", + phpstan_turbo_sources("src\\parser", ""), + "phpstan_turbo"); + } } diff --git a/turbo-ext/src/Abi.cpp b/turbo-ext/src/Abi.cpp new file mode 100644 index 00000000000..e49d77d0d60 --- /dev/null +++ b/turbo-ext/src/Abi.cpp @@ -0,0 +1,376 @@ +/* + * The version-specific side of abi.h: the only translation unit reading the + * engine globals by their compiled-in layout. + */ + +#define PHPSTANTURBO_ABI_IMPL +#include "support.h" +#include "reg.h" + +#include "Engine.h" +#include "zend_closures.h" +#include "zend_fibers.h" +#include "zend_weakrefs.h" + + +static const zend_object_handlers *abi_object_handlers(const pt_abi_handlers &overrides) +{ + auto *handlers = (zend_object_handlers *) pemalloc(sizeof(zend_object_handlers), 1); + memcpy(handlers, &std_object_handlers, sizeof(zend_object_handlers)); + handlers->offset = overrides.offset; + if (overrides.free_obj != NULL) handlers->free_obj = overrides.free_obj; + if (overrides.get_gc != NULL) handlers->get_gc = overrides.get_gc; + if (overrides.clone_obj != NULL) handlers->clone_obj = overrides.clone_obj; + if (overrides.get_closure != NULL) handlers->get_closure = overrides.get_closure; + if (overrides.compare != NULL) handlers->compare = overrides.compare; + return handlers; +} + +static zend_function_entry *abi_function_entries(const reg::FunctionEntry *entries, size_t count) +{ + zend_function_entry *functions = (zend_function_entry *) ecalloc(count + 1, sizeof(zend_function_entry)); + for (size_t i = 0; i < count; i++) { + functions[i].fname = entries[i].fname; + functions[i].handler = entries[i].handler; + functions[i].arg_info = entries[i].argInfo; + functions[i].num_args = entries[i].numArgs; + functions[i].flags = entries[i].flags; + } + return functions; +} + +static zend_class_entry *abi_register_internal_class(const char *name, const reg::FunctionEntry *entries, size_t count) +{ + zend_function_entry *functions = abi_function_entries(entries, count); + zend_class_entry ce; + INIT_CLASS_ENTRY_EX(ce, name, strlen(name), functions); + zend_class_entry *registered = zend_register_internal_class(&ce); + efree(functions); + return registered; +} + +static void abi_create_closure(zval *out, zend_function *fn, zend_class_entry *scope, zend_class_entry *calledScope, zend_object *thisObject) +{ +#if PHP_VERSION_ID >= 80600 + /* php-src fbb2e1f23d6: $this is passed as zend_object* from 8.6 on */ + zend_create_closure(out, fn, scope, calledScope, thisObject); +#else + zval thisZv; + if (thisObject != NULL) { + ZVAL_OBJ(&thisZv, thisObject); + } + zend_create_closure(out, fn, scope, calledScope, thisObject != NULL ? &thisZv : NULL); +#endif +} + +static zend_object *abi_closure_this(zval *closure) +{ +#if PHP_VERSION_ID >= 80600 + /* PHP 8.6 holds the bound $this as a zend_object (NULL when unbound) */ + return zend_get_closure_this_ptr(closure); +#else + zval *thisZv = zend_get_closure_this_ptr(closure); + return thisZv != NULL && Z_TYPE_P(thisZv) == IS_OBJECT ? Z_OBJ_P(thisZv) : NULL; +#endif +} + +/* zend_weakrefs_hash_clean()/_destroy() only exist since PHP 8.5; before, + * the same unregister-then-destroy is spelled out with the API there is */ +static void abi_weakrefs_hash_clean(HashTable *ht) +{ +#if PHP_VERSION_ID >= 80500 + zend_weakrefs_hash_clean(ht); +#else + zend_ulong objKey; + ZEND_HASH_MAP_FOREACH_NUM_KEY(ht, objKey) { + zend_weakrefs_hash_del(ht, zend_weakref_key_to_object(objKey)); + } ZEND_HASH_FOREACH_END(); +#endif +} + +static void abi_weakrefs_hash_destroy(HashTable *ht) +{ +#if PHP_VERSION_ID >= 80500 + zend_weakrefs_hash_destroy(ht); +#else + abi_weakrefs_hash_clean(ht); + zend_hash_destroy(ht); +#endif +} + +static void abi_call_stack_size_error() +{ +#if PHP_VERSION_ID >= 80400 + zend_call_stack_size_error(); +#else + /* static in PHP 8.3; the same message */ + zend_throw_error(nullptr, "Maximum call stack size of %zu bytes (zend.max_allowed_stack_size - zend.reserved_stack_size) reached. Infinite recursion?", + (size_t) ((uintptr_t) EG(stack_base) - (uintptr_t) EG(stack_limit))); +#endif +} + +static void abi_try_assign_ref_str(zval *zv, zend_string *str) +{ + ZEND_TRY_ASSIGN_REF_STR(zv, str); +} + +static void abi_try_assign_ref_true(zval *zv) +{ + ZEND_TRY_ASSIGN_REF_TRUE(zv); +} + +/* the neutral ZPP numbering of abi.h, translated for this engine */ +static const int abi_zpp_errors[] = { + ZPP_ERROR_OK, + ZPP_ERROR_FAILURE, + ZPP_ERROR_WRONG_CALLBACK, + ZPP_ERROR_WRONG_CLASS, + ZPP_ERROR_WRONG_CLASS_OR_NULL, + ZPP_ERROR_WRONG_CLASS_OR_STRING, + ZPP_ERROR_WRONG_CLASS_OR_STRING_OR_NULL, + ZPP_ERROR_WRONG_CLASS_OR_LONG, + ZPP_ERROR_WRONG_CLASS_OR_LONG_OR_NULL, + ZPP_ERROR_WRONG_ARG, + ZPP_ERROR_UNEXPECTED_EXTRA_NAMED, + ZPP_ERROR_WRONG_CALLBACK_OR_NULL, +}; +static const zend_expected_type abi_expected_types[] = { + Z_EXPECTED_LONG, + Z_EXPECTED_LONG_OR_NULL, + Z_EXPECTED_BOOL, + Z_EXPECTED_BOOL_OR_NULL, + Z_EXPECTED_STRING, + Z_EXPECTED_STRING_OR_NULL, + Z_EXPECTED_ARRAY, + Z_EXPECTED_ARRAY_OR_NULL, + Z_EXPECTED_ARRAY_OR_LONG, + Z_EXPECTED_ARRAY_OR_LONG_OR_NULL, + Z_EXPECTED_ITERABLE, + Z_EXPECTED_ITERABLE_OR_NULL, + Z_EXPECTED_FUNC, + Z_EXPECTED_FUNC_OR_NULL, + Z_EXPECTED_RESOURCE, + Z_EXPECTED_RESOURCE_OR_NULL, + Z_EXPECTED_PATH, + Z_EXPECTED_PATH_OR_NULL, + Z_EXPECTED_OBJECT, + Z_EXPECTED_OBJECT_OR_NULL, + Z_EXPECTED_DOUBLE, + Z_EXPECTED_DOUBLE_OR_NULL, + Z_EXPECTED_NUMBER, + Z_EXPECTED_NUMBER_OR_NULL, + Z_EXPECTED_NUMBER_OR_STRING, + Z_EXPECTED_NUMBER_OR_STRING_OR_NULL, + Z_EXPECTED_ARRAY_OR_STRING, + Z_EXPECTED_ARRAY_OR_STRING_OR_NULL, + Z_EXPECTED_STRING_OR_LONG, + Z_EXPECTED_STRING_OR_LONG_OR_NULL, +#if PHP_VERSION_ID >= 80600 + Z_EXPECTED_CLASS_NAME, +#else + Z_EXPECTED_LONG, /* not produced before 8.6 */ +#endif +#if PHP_VERSION_ID >= 80600 + Z_EXPECTED_CLASS_NAME_OR_NULL, +#else + Z_EXPECTED_LONG, /* not produced before 8.6 */ +#endif + Z_EXPECTED_OBJECT_OR_CLASS_NAME, + Z_EXPECTED_OBJECT_OR_CLASS_NAME_OR_NULL, + Z_EXPECTED_OBJECT_OR_STRING, + Z_EXPECTED_OBJECT_OR_STRING_OR_NULL, +}; + +static void abi_wrong_parameter_error(int errorCode, uint32_t num, char *name, int expectedType, zval *arg) +{ +#if PHP_VERSION_ID >= 80600 + zend_wrong_parameter_error((zpp_error) abi_zpp_errors[errorCode], num, name, abi_expected_types[expectedType], arg); +#else + zend_wrong_parameter_error(abi_zpp_errors[errorCode], num, name, abi_expected_types[expectedType], arg); +#endif +} + +static bool abi_parse_arg_bool_slow(const zval *arg, bool *dest, uint32_t num) +{ +#if PHP_VERSION_ID >= 80600 + zpp_parse_bool_status status = zend_parse_arg_bool_slow(arg, num); + if (UNEXPECTED(status == ZPP_PARSE_BOOL_STATUS_ERROR)) { + return false; + } + *dest = status; + return true; +#else + return zend_parse_arg_bool_slow(arg, dest, num); +#endif +} + +static bool abi_parse_arg_double_slow(const zval *arg, double *dest, uint32_t num) +{ +#if PHP_VERSION_ID >= 80600 + *dest = zend_parse_arg_double_slow(arg, num); + return !zend_isnan(*dest); +#else + return zend_parse_arg_double_slow(arg, dest, num); +#endif +} + +static bool abi_parse_arg_str_slow(zval *arg, zend_string **dest, uint32_t num) +{ +#if PHP_VERSION_ID >= 80600 + *dest = zend_parse_arg_str_slow(arg, num); + return *dest != NULL; +#else + return zend_parse_arg_str_slow(arg, dest, num); +#endif +} + +static void abi_call_known_function(zend_function *fn, zend_object *object, zend_class_entry *calledScope, zval *retval, uint32_t paramCount, zval *params, HashTable *namedParams) +{ + zend_call_known_function(fn, object, calledScope, retval, paramCount, params, namedParams); +} + +static int abi_stream_free(php_stream *stream, int closeOptions) +{ + return php_stream_free(stream, closeOptions); +} + +static ssize_t abi_stream_read(php_stream *stream, char *buf, size_t count) +{ + return php_stream_read(stream, buf, count); +} + +/* {{{ continuing a call on a fresh C stack (pt_engine_with_stack()) */ + +namespace { + +/* one call continued on a fresh stack: the body, and the VM state it left + * behind — zend_fiber_switch_context() restores the caller's state from + * before the switch, which the body may legitimately have changed */ +struct FreshStackCall +{ + void (*body)(void *); + void *data; + bool bailout; + zend_vm_stack vmStack; + zval *vmStackTop; + zval *vmStackEnd; + size_t vmStackPageSize; + int errorReporting; +}; + +/* the call the coroutine starting next runs (read once, at its start) */ +FreshStackCall *pt_fresh_stack_call = nullptr; + +/* identifies the contexts in fiber observers */ +char pt_fresh_stack_kind; + +ZEND_STACK_ALIGNED void freshStackCoroutine(zend_fiber_transfer *transfer) +{ + (void) transfer; /* already addresses the caller, which is where it returns */ + FreshStackCall *call = pt_fresh_stack_call; + zend_fiber_context *context = EG(current_fiber_context); +#ifdef ZEND_CHECK_STACK_LIMIT + EG(stack_base) = zend_fiber_stack_base(context->stack); + EG(stack_limit) = zend_fiber_stack_limit(context->stack); +#endif + zend_try { + call->body(call->data); + } zend_catch { + call->bailout = true; + } zend_end_try(); + call->vmStack = EG(vm_stack); + call->vmStackTop = EG(vm_stack_top); + call->vmStackEnd = EG(vm_stack_end); + call->vmStackPageSize = EG(vm_stack_page_size); + call->errorReporting = EG(error_reporting); +} + +} // namespace + +static void abi_run_on_fresh_stack(void (*body)(void *), void *data) +{ + zend_fiber_context context; + memset(&context, 0, sizeof(context)); + if (UNEXPECTED(zend_fiber_init_context(&context, &pt_fresh_stack_kind, freshStackCoroutine, PT_ENGINE_FRESH_STACK_SIZE_LIMIT) == FAILURE)) { + /* no stack to be had: continue here, where the engine's own stack + * limit decides */ + if (EG(exception) != NULL) { + zend_clear_exception(); + } + body(data); + return; + } + + FreshStackCall call = { body, data, false, NULL, NULL, NULL, 0, 0 }; + FreshStackCall *previous = pt_fresh_stack_call; + pt_fresh_stack_call = &call; + zend_fiber_transfer transfer; + transfer.context = &context; + transfer.flags = 0; + ZVAL_NULL(&transfer.value); + /* returns once the body finished; the dead context is destroyed then */ + zend_fiber_switch_context(&transfer); + pt_fresh_stack_call = previous; + + EG(vm_stack) = call.vmStack; + EG(vm_stack_top) = call.vmStackTop; + EG(vm_stack_end) = call.vmStackEnd; + EG(vm_stack_page_size) = call.vmStackPageSize; + EG(error_reporting) = call.errorReporting; + if (UNEXPECTED(call.bailout)) { + zend_bailout(); + } +} + +/* }}} */ + +void pt_abi_init() +{ +#define PT_ABI_EG_FIELD(field) pt_abi.eg_##field = &EG(field); +#define PT_ABI_CG_FIELD(field) pt_abi.cg_##field = &CG(field); +#define PT_ABI_KNOWN_STRING(id) pt_abi.known_##id = ZSTR_KNOWN(id); +#define PT_ABI_OBJECT_HANDLER(member) pt_abi.handler_offset_##member = offsetof(zend_object_handlers, member); +#define PT_ABI_CLASS_ENTRY_MEMBER(member) pt_abi.ce_offset_##member = offsetof(zend_class_entry, member); + PT_ABI_EG_FIELDS(PT_ABI_EG_FIELD) + PT_ABI_CG_FIELDS(PT_ABI_CG_FIELD) + PT_ABI_KNOWN_STRINGS(PT_ABI_KNOWN_STRING) + PT_ABI_OBJECT_HANDLERS(PT_ABI_OBJECT_HANDLER) + PT_ABI_CLASS_ENTRY_MEMBERS(PT_ABI_CLASS_ENTRY_MEMBER) +#undef PT_ABI_EG_FIELD +#undef PT_ABI_CLASS_ENTRY_MEMBER +#undef PT_ABI_CG_FIELD +#undef PT_ABI_KNOWN_STRING +#undef PT_ABI_OBJECT_HANDLER + + pt_abi.offset_internal_function_handler = offsetof(zend_internal_function, handler); + pt_abi.sizeof_arg_info = sizeof(zend_arg_info); + pt_abi.php_version_id = PHP_VERSION_ID; +#if PHP_VERSION_ID >= 80400 + static_assert(offsetof(zend_object, extra_flags) == offsetof(zend_object, handle) + sizeof(uint32_t), "pt_abi_object_is_lazy() reads extra_flags right after handle"); + pt_abi.object_lazy_flags = IS_OBJ_LAZY_UNINITIALIZED | IS_OBJ_LAZY_PROXY; +#else + pt_abi.object_lazy_flags = 0; +#endif + pt_abi.is_reference_ex = IS_REFERENCE_EX; + pt_abi.acc_use_guards = ZEND_ACC_USE_GUARDS; + pt_abi.dval_to_lval = [](double d) -> zend_long { return zend_dval_to_lval(d); }; + pt_abi.function_entries = abi_function_entries; + pt_abi.register_internal_class = abi_register_internal_class; + pt_abi.object_handlers = abi_object_handlers; + pt_abi.create_closure = abi_create_closure; + pt_abi.closure_this = abi_closure_this; + pt_abi.weakrefs_hash_clean = abi_weakrefs_hash_clean; + pt_abi.weakrefs_hash_destroy = abi_weakrefs_hash_destroy; + pt_abi.call_stack_size_error = abi_call_stack_size_error; + pt_abi.try_assign_ref_str = abi_try_assign_ref_str; + pt_abi.try_assign_ref_true = abi_try_assign_ref_true; + pt_abi.wrong_parameter_error = abi_wrong_parameter_error; + pt_abi.parse_arg_bool_slow = abi_parse_arg_bool_slow; + pt_abi.parse_arg_double_slow = abi_parse_arg_double_slow; + pt_abi.parse_arg_str_slow = abi_parse_arg_str_slow; + pt_abi.call_known_function = abi_call_known_function; + pt_abi.stream_free = abi_stream_free; + pt_abi.stream_read = abi_stream_read; + pt_abi.run_on_fresh_stack = abi_run_on_fresh_stack; + pt_abi.is_true = [](const zval *op) -> bool { return zend_is_true((zval *) op); }; +} diff --git a/turbo-ext/src/ArenaCache.cpp b/turbo-ext/src/ArenaCache.cpp index 77d3dcac3d5..592cc1aa820 100644 --- a/turbo-ext/src/ArenaCache.cpp +++ b/turbo-ext/src/ArenaCache.cpp @@ -294,12 +294,12 @@ static bool objectClassCodecable(zend_class_entry *ce) { if (ce->type == ZEND_INTERNAL_CLASS && ce != zend_standard_class_def) return false; if ((ce->ce_flags & (ZEND_ACC_INTERFACE | ZEND_ACC_ABSTRACT | ZEND_ACC_ENUM)) != 0) return false; - if (ce->__serialize != NULL || ce->__unserialize != NULL) return false; - if (zend_hash_str_exists(&ce->function_table, "__wakeup", sizeof("__wakeup") - 1) - || zend_hash_str_exists(&ce->function_table, "__sleep", sizeof("__sleep") - 1)) { + if (PT_CE(ce, __serialize) != NULL || PT_CE(ce, __unserialize) != NULL) return false; + if (zend_hash_str_exists(&PT_CE(ce, function_table), "__wakeup", sizeof("__wakeup") - 1) + || zend_hash_str_exists(&PT_CE(ce, function_table), "__sleep", sizeof("__sleep") - 1)) { return false; } - if (ce->create_object != NULL) return false; + if (PT_CE(ce, create_object) != NULL) return false; return true; } @@ -376,7 +376,7 @@ static bool serializeValue(WriteBuffer &out, zval *value, uint32_t depth, Serial * slots (mangled keys for private/protected) plus dynamic ones; * uninitialized typed props are UNDEF after deref and skipped — * the same shape serialize() writes for hook-free classes */ - HashTable *props = obj->handlers->get_properties(obj); + HashTable *props = PT_OBJ_HANDLER(obj, get_properties)(obj); uint32_t propCount = 0; for (zv::ArrayEntry entry : zv::TableRef(props)) { zval *propValue = entry.value().raw(); diff --git a/turbo-ext/src/ArgumentsHandler.cpp b/turbo-ext/src/ArgumentsHandler.cpp index be0428704c9..f6dcb74d7fa 100644 --- a/turbo-ext/src/ArgumentsHandler.cpp +++ b/turbo-ext/src/ArgumentsHandler.cpp @@ -48,12 +48,8 @@ zend_class_entry *pt_ce_arguments_handler = nullptr; /* the twin's closure names, for the engine's messages */ #define PT_AH_CLASS "PHPStan\\Analyser\\ArgumentsHandler" -#if PHP_VERSION_ID >= 80400 -#define PT_AH_CLOSURE(method, line) PT_AH_CLASS "::{closure:" PT_AH_CLASS "::" method "():" line "}" -#else -/* PHP 8.3 names a closure by its namespace alone */ -#define PT_AH_CLOSURE(method, line) PT_AH_CLASS "::PHPStan\\Analyser\\{closure}" -#endif +/* the closure names differ: PHP 8.3 names a closure by its namespace alone */ +#define PT_AH_CLOSURE(method, line) PT_ABI_SINCE(80400, PT_AH_CLASS "::{closure:" PT_AH_CLASS "::" method "():" line "}", PT_AH_CLASS "::PHPStan\\Analyser\\{closure}") namespace { @@ -880,7 +876,7 @@ class ArgumentsHandler SkeletonFrame *frame = static_cast(data); ZVAL_DEREF(inner); if (UNEXPECTED(Z_TYPE_P(inner) != IS_OBJECT)) { - zend_type_error(PT_AH_CLOSURE("gatherArrayArgTypeSkeleton", "977") "(): Argument #1 ($inner) must be of type PhpParser\\Node\\Expr, %s given", zend_zval_value_name(inner)); + zend_type_error("%s(): Argument #1 ($inner) must be of type PhpParser\\Node\\Expr, %s given", PT_AH_CLOSURE("gatherArrayArgTypeSkeleton", "977"), zend_zval_value_name(inner)); return zv::Val(); } ArgumentsHandler handler(frame->self); @@ -916,7 +912,7 @@ class ArgumentsHandler static void skeletonTypeBody(zval *captures, uint32_t argc, zval *argv, zval *return_value) { if (UNEXPECTED(argc < 1)) { - zend_throw_error(zend_ce_argument_count_error, "Too few arguments to function " PT_AH_CLOSURE("gatherArrayArgTypeSkeleton", "977") "(), %u passed and exactly 1 expected", argc); + zend_throw_error(zend_ce_argument_count_error, "Too few arguments to function %s(), %u passed and exactly 1 expected", PT_AH_CLOSURE("gatherArrayArgTypeSkeleton", "977"), argc); return; } SkeletonFrame frame{Z_OBJ(captures[0]), &captures[1], &captures[2], &captures[3]}; diff --git a/turbo-ext/src/Assertions.cpp b/turbo-ext/src/Assertions.cpp index 89e70665662..274862f834e 100644 --- a/turbo-ext/src/Assertions.cpp +++ b/turbo-ext/src/Assertions.cpp @@ -47,12 +47,8 @@ zv::Val tagCall(pt_method_site &site, zval *tag, const char *lcname, size_t len, /* the twin's closure names, for the parameter TypeError */ #define PT_AS_CLASS "PHPStan\\Reflection\\Assertions" -#if PHP_VERSION_ID >= 80400 -#define PT_AS_CLOSURE(method, line) PT_AS_CLASS "::{closure:" PT_AS_CLASS "::" method "():" line "}" -#else -/* PHP 8.3 names a closure by its namespace alone */ -#define PT_AS_CLOSURE(method, line) PT_AS_CLASS "::PHPStan\\Reflection\\{closure}" -#endif +/* the closure names differ: PHP 8.3 names a closure by its namespace alone */ +#define PT_AS_CLOSURE(method, line) PT_ABI_SINCE(80400, PT_AS_CLASS "::{closure:" PT_AS_CLASS "::" method "():" line "}", PT_AS_CLASS "::PHPStan\\Reflection\\{closure}") /* the closure's `AssertTag $assert` parameter check; false = TypeError raised */ [[nodiscard]] bool requireAssertTag(zval *value, const char *closureName, const char *parameterName) @@ -108,7 +104,7 @@ zval *emptySlot() if (CE_STATIC_MEMBERS(ce) == NULL) { zend_class_init_statics(ce); } - zend_property_info *info = (zend_property_info *) zend_hash_str_find_ptr(&ce->properties_info, ZEND_STRL("empty")); + zend_property_info *info = (zend_property_info *) zend_hash_str_find_ptr(&PT_CE(ce, properties_info), ZEND_STRL("empty")); ZEND_ASSERT(info != NULL && (info->flags & ZEND_ACC_STATIC) != 0); pt_as_empty_slot = CE_STATIC_MEMBERS(ce) + info->offset; pt_as_empty_ce = ce; diff --git a/turbo-ext/src/BinaryOpHandler.cpp b/turbo-ext/src/BinaryOpHandler.cpp index c33158d8f24..06f10c60b7d 100644 --- a/turbo-ext/src/BinaryOpHandler.cpp +++ b/turbo-ext/src/BinaryOpHandler.cpp @@ -260,7 +260,7 @@ int namedCall(zval *node, bool requireCallable, NamedCall &out) /* (string) $node->name: Name::__toString() is its $name */ zend_class_entry *nameCe = pt_class(PT_CLASS_NAME); if (UNEXPECTED(nameCe == NULL)) return -1; - zend_function *toString = Z_OBJCE_P(name)->__tostring; + zend_function *toString = PT_CE(Z_OBJCE_P(name), __tostring); zval *nameString = toString != NULL && toString->common.scope == nameCe ? pt_boh_name_name.of(Z_OBJ_P(name)) : NULL; if (nameString != NULL && Z_TYPE_P(nameString) == IS_STRING) { out.name = zv::Str::copyOf(Z_STR_P(nameString)); diff --git a/turbo-ext/src/CalledMethodProcessor.cpp b/turbo-ext/src/CalledMethodProcessor.cpp index 46dc38f3f7c..c1e4a47ea75 100644 --- a/turbo-ext/src/CalledMethodProcessor.cpp +++ b/turbo-ext/src/CalledMethodProcessor.cpp @@ -391,7 +391,7 @@ class CalledMethodProcessor zend_string *name = zval_try_get_string(subNodeName); if (UNEXPECTED(name == NULL)) return false; zval rv; - zval *subNode = Z_OBJ_P(node)->handlers->read_property(Z_OBJ_P(node), name, BP_VAR_R, NULL, &rv); + zval *subNode = PT_OBJ_HANDLER(Z_OBJ_P(node), read_property)(Z_OBJ_P(node), name, BP_VAR_R, NULL, &rv); zend_string_release(name); if (UNEXPECTED(EG(exception))) { if (subNode == &rv) zval_ptr_dtor(&rv); diff --git a/turbo-ext/src/ClassReflection.cpp b/turbo-ext/src/ClassReflection.cpp index 335931da9ea..34c5e3297d7 100644 --- a/turbo-ext/src/ClassReflection.cpp +++ b/turbo-ext/src/ClassReflection.cpp @@ -593,7 +593,7 @@ class ClassReflection } return zv::Val(); } - zend_function *fn = (zend_function *) zend_hash_str_find_ptr(&ce->function_table, lcmethod, methodLen); + zend_function *fn = (zend_function *) zend_hash_str_find_ptr(&PT_CE(ce, function_table), lcmethod, methodLen); if (UNEXPECTED(fn == NULL)) { zend_throw_error(NULL, "phpstan_turbo: method %s::%s not found", className, lcmethod); return zv::Val(); @@ -837,7 +837,7 @@ class ClassReflection if (CE_STATIC_MEMBERS(ce) == NULL) { zend_class_init_statics(ce); } - zend_property_info *info = (zend_property_info *) zend_hash_str_find_ptr(&ce->properties_info, PT_LC("resolvingTypeAliasImports")); + zend_property_info *info = (zend_property_info *) zend_hash_str_find_ptr(&PT_CE(ce, properties_info), PT_LC("resolvingTypeAliasImports")); if (UNEXPECTED(info == NULL || (info->flags & ZEND_ACC_STATIC) == 0)) { zend_throw_error(NULL, "phpstan_turbo: ClassReflection has no static $resolvingTypeAliasImports"); return NULL; diff --git a/turbo-ext/src/ConditionalTypeForParameter.cpp b/turbo-ext/src/ConditionalTypeForParameter.cpp index e86f9de269c..bb59e3f028f 100644 --- a/turbo-ext/src/ConditionalTypeForParameter.cpp +++ b/turbo-ext/src/ConditionalTypeForParameter.cpp @@ -30,15 +30,10 @@ zend_class_entry *pt_ce_conditional_type_for_parameter = nullptr; /* the twin's closure names, for the engine's messages */ #define PT_CTFP_CLASS "PHPStan\\Type\\ConditionalTypeForParameter" -#if PHP_VERSION_ID >= 80400 -#define PT_CTFP_CLOSURE(method, line) PT_CTFP_CLASS "::{closure:" PT_CTFP_CLASS "::" method "():" line "}" +/* the closure names differ: PHP 8.3 names a closure by its namespace alone */ +#define PT_CTFP_CLOSURE(method, line) PT_ABI_SINCE(80400, PT_CTFP_CLASS "::{closure:" PT_CTFP_CLASS "::" method "():" line "}", PT_CTFP_CLASS "::PHPStan\\Type\\{closure}") /* a closure created by a closure of the method, both on the same line */ -#define PT_CTFP_NESTED_CLOSURE(method, line) PT_CTFP_CLASS "::{closure:{closure:" PT_CTFP_CLASS "::" method "():" line "}:" line "}" -#else -/* PHP 8.3 names a closure by its namespace alone */ -#define PT_CTFP_CLOSURE(method, line) PT_CTFP_CLASS "::PHPStan\\Type\\{closure}" -#define PT_CTFP_NESTED_CLOSURE(method, line) PT_CTFP_CLOSURE(method, line) -#endif +#define PT_CTFP_NESTED_CLOSURE(method, line) PT_ABI_SINCE(80400, PT_CTFP_CLASS "::{closure:{closure:" PT_CTFP_CLASS "::" method "():" line "}:" line "}", PT_CTFP_CLASS "::PHPStan\\Type\\{closure}") namespace phpstanturbo { @@ -393,12 +388,12 @@ class ConditionalTypeForParameter static void narrowedSubjectTypeBody(zval *captures, uint32_t argc, zval *argv, zval *return_value) { if (UNEXPECTED(argc < 1)) { - zend_throw_error(zend_ce_argument_count_error, "Too few arguments to function " PT_CTFP_NESTED_CLOSURE("resolveWithSubject", "141") "(), 0 passed and exactly 1 expected"); + zend_throw_error(zend_ce_argument_count_error, "Too few arguments to function %s(), 0 passed and exactly 1 expected", PT_CTFP_NESTED_CLOSURE("resolveWithSubject", "141")); return; } zval *name = &argv[0]; if (UNEXPECTED(Z_TYPE_P(name) != IS_STRING)) { - zend_type_error(PT_CTFP_NESTED_CLOSURE("resolveWithSubject", "141") "(): Argument #1 ($parameterName) must be of type string, %s given", zend_zval_value_name(name)); + zend_type_error("%s(): Argument #1 ($parameterName) must be of type string, %s given", PT_CTFP_NESTED_CLOSURE("resolveWithSubject", "141"), zend_zval_value_name(name)); return; } zv::Val subjectType; @@ -420,7 +415,7 @@ class ConditionalTypeForParameter static void resolveInTypeCallback(zval *getSubjectType, zval *passedArgs, uint32_t argc, zval *argv, zval *return_value) { if (UNEXPECTED(argc < 2)) { - zend_throw_error(zend_ce_argument_count_error, "Too few arguments to function " PT_CTFP_CLOSURE("resolveInType", "116") "(), %u passed and exactly 2 expected", argc); + zend_throw_error(zend_ce_argument_count_error, "Too few arguments to function %s(), %u passed and exactly 2 expected", PT_CTFP_CLOSURE("resolveInType", "116"), argc); return; } zval *type = &argv[0]; diff --git a/turbo-ext/src/ConstantArrayType.cpp b/turbo-ext/src/ConstantArrayType.cpp index 4fdd18240c1..3caf61d12a9 100644 --- a/turbo-ext/src/ConstantArrayType.cpp +++ b/turbo-ext/src/ConstantArrayType.cpp @@ -6821,7 +6821,7 @@ class ConstantArrayType zend_class_entry *ce = pt_class(PT_CLASS_ARRAY_SHAPE_NODE); if (UNEXPECTED(ce == NULL)) return zv::Val(); const char *name = asList ? "KIND_LIST" : "KIND_ARRAY"; - zend_class_constant *constant = (zend_class_constant *) zend_hash_str_find_ptr(&ce->constants_table, name, strlen(name)); + zend_class_constant *constant = (zend_class_constant *) zend_hash_str_find_ptr(&PT_CE(ce, constants_table), name, strlen(name)); if (UNEXPECTED(constant == NULL)) { zend_throw_error(NULL, "phpstan_turbo: %s::%s not found", ZSTR_VAL(ce->name), name); return zv::Val(); @@ -6854,7 +6854,7 @@ class ConstantArrayType zend_type_error("phpstan_turbo: cannot clone a non-object"); return zv::Val(); } - zend_object *cloned = Z_OBJ_HT_P(object)->clone_obj(Z_OBJ_P(object)); + zend_object *cloned = Z_OBJ_HANDLER_P(object, clone_obj)(Z_OBJ_P(object)); if (UNEXPECTED(cloned == NULL || EG(exception))) { if (cloned != NULL) { OBJ_RELEASE(cloned); diff --git a/turbo-ext/src/ConstantStringType.cpp b/turbo-ext/src/ConstantStringType.cpp index debe028a489..fe0955e5409 100644 --- a/turbo-ext/src/ConstantStringType.cpp +++ b/turbo-ext/src/ConstantStringType.cpp @@ -56,7 +56,7 @@ zend_class_entry *pt_ce_constant_string_type = nullptr; * exception */ static zval *pt_class_constant(zend_class_entry *ce, const char *name, size_t len) { - zend_class_constant *constant = (zend_class_constant *) zend_hash_str_find_ptr(&ce->constants_table, name, len); + zend_class_constant *constant = (zend_class_constant *) zend_hash_str_find_ptr(&PT_CE(ce, constants_table), name, len); if (UNEXPECTED(constant == NULL)) { zend_throw_error(NULL, "phpstan_turbo: %s::%s not found", ZSTR_VAL(ce->name), name); return NULL; @@ -623,7 +623,7 @@ class ConstantStringType zval genericRaw; if (UNEXPECTED(object_init_ex(&genericRaw, pt_ce_generic_class_string_type) != SUCCESS)) return zv::Val(); zv::Val generic = zv::Val::adopt(genericRaw); - zend_call_known_instance_method(pt_ce_generic_class_string_type->constructor, Z_OBJ_P(generic.raw()), NULL, 1, innerObjectType.raw()); + zend_call_known_instance_method(PT_CE(pt_ce_generic_class_string_type, constructor), Z_OBJ_P(generic.raw()), NULL, 1, innerObjectType.raw()); if (UNEXPECTED(EG(exception))) return zv::Val(); zv::Arr types = zv::Arr::create(2); types.push(std::move(objectType)); diff --git a/turbo-ext/src/Core.cpp b/turbo-ext/src/Core.cpp new file mode 100644 index 00000000000..88dc9ad7d01 --- /dev/null +++ b/turbo-ext/src/Core.cpp @@ -0,0 +1,123 @@ +/* + * The shared core's own state and the entry points the version-specific + * library (main.cpp, Abi.cpp, Shadow.cpp, TrustedTypes.cpp) calls into. + * Everything here is version-neutral: the core is compiled once per + * platform and loaded by the thin per-PHP-version extension, and it never + * references a symbol of that extension — what differs between minors is + * read through pt_abi (abi.h), which the extension fills in at startup. + */ + +#include "support.h" +#include "reg.h" +#include "Engine.h" +#include "version.h" + +#include + +pt_abi_globals pt_abi; + +/* the core's data, for the version-specific library: through functions, + * since a Windows DLL can delay-load functions only */ +PT_CORE_API pt_abi_globals *pt_core_abi() +{ + return &pt_abi; +} + +PT_CORE_API pt_globals_t *pt_core_globals() +{ + return &pt_globals; +} + +PT_CORE_API const pt_type_op_info *pt_core_type_op_infos() +{ + return pt_type_op_infos; +} + +/* the version the core was built at — the extension refuses a core of + * another version (main.cpp) */ +PT_CORE_API const char *pt_core_version() +{ + return PHPSTANTURBO_VERSION; +} + +/* the PT_MINIT_REGISTRATION() functions of every file, in name order; a + * constant-initialized pointer, so it is null before any of the file-static + * registrations construct */ +static pt_minit_registration *pt_minit_registrations = nullptr; + +pt_minit_registration::pt_minit_registration(const char *name, void (*run)()) noexcept + : name(name), run(run), next(nullptr) +{ + pt_minit_registration **slot = &pt_minit_registrations; + while (*slot != nullptr && strcmp((*slot)->name, name) < 0) { + slot = &(*slot)->next; + } + next = *slot; + *slot = this; +} + +PT_CORE_API void pt_minit_registrations_run() +{ + for (const pt_minit_registration *registration = pt_minit_registrations; registration != nullptr; registration = registration->next) { + registration->run(); + } +} + +/* the shadowing plans recorded at module startup (reg::Class::shadow()), + * declared by Runtime::activateShadowing() (Shadow.cpp) */ +PT_CORE_API std::vector &pt_shadow_plans() +{ + static std::vector plans; + return plans; +} + +void pt_shadow_plan_add(reg::ShadowPlan &&plan) +{ + pt_shadow_plans().push_back(std::move(plan)); +} + +bool pt_shadow_instanceof(zend_class_entry *ce, zend_class_entry *nativeCe, const char *realName, size_t realNameLen) +{ + if (EXPECTED(nativeCe != NULL && instanceof_function(ce, nativeCe))) return true; + if (nativeCe == NULL || zend_string_equals_cstr(nativeCe->name, realName, realNameLen)) return false; + zend_string *name = zend_string_init(realName, realNameLen, 0); + zend_class_entry *twin = zend_lookup_class_ex(name, NULL, ZEND_FETCH_CLASS_NO_AUTOLOAD); + zend_string_release(name); + return twin != NULL && instanceof_function(ce, twin); +} + +PT_CORE_API void pt_core_rinit() +{ + pt_support_rinit(); + pt_node_traverser_rinit(); + pt_scope_ops_rinit(); + pt_type_combinator_cache_rinit(); + pt_is_super_type_of_result_rinit(); + pt_accepts_result_rinit(); + pt_integer_range_type_rinit(); + pt_object_type_rinit(); + pt_static_type_factory_rinit(); + pt_scope_access_rinit(); + pt_reflection_access_rinit(); + pt_mutating_scope_rinit(); + pt_variable_flow_rinit(); + pt_engine_rinit(); +} + +PT_CORE_API void pt_core_rshutdown() +{ + pt_scope_ops_rshutdown(); + pt_node_traverser_rshutdown(); + pt_type_combinator_cache_rshutdown(); + pt_is_super_type_of_result_rshutdown(); + pt_accepts_result_rshutdown(); + pt_support_rshutdown(); + pt_object_type_rshutdown(); + pt_static_type_factory_rshutdown(); + pt_engine_rshutdown(); +} + +PT_CORE_API void pt_core_mshutdown() +{ + pt_arena_mshutdown(); +} diff --git a/turbo-ext/src/Engine.cpp b/turbo-ext/src/Engine.cpp index 2974361619c..4e46ba4b2f3 100644 --- a/turbo-ext/src/Engine.cpp +++ b/turbo-ext/src/Engine.cpp @@ -30,7 +30,7 @@ struct NativeClosureObject zend_object std; }; -zend_object_handlers pt_native_closure_handlers; +const zend_object_handlers *pt_native_closure_handlers; /* the class's __invoke() (a zend_class_entry has no slot for it) */ zend_function *pt_native_closure_invoke_fn = nullptr; @@ -54,7 +54,7 @@ NativeClosureObject *allocateClosure(zend_class_entry *ce, pt_native_closure_fn closure->fn = fn; closure->count = count; zend_object_std_init(&closure->std, ce); - closure->std.handlers = &pt_native_closure_handlers; + closure->std.handlers = pt_native_closure_handlers; return closure; } @@ -195,13 +195,14 @@ zv::Val pt_native_closure_to_closure(zval *closure) PT_MINIT_REGISTRATION(pt_register_native_closure) { - memcpy(&pt_native_closure_handlers, zend_get_std_object_handlers(), sizeof(zend_object_handlers)); - pt_native_closure_handlers.offset = offsetof(NativeClosureObject, std); - pt_native_closure_handlers.free_obj = freeClosureObject; - pt_native_closure_handlers.get_gc = closureGc; - pt_native_closure_handlers.clone_obj = cloneClosureObject; - pt_native_closure_handlers.get_closure = getClosure; - pt_native_closure_handlers.compare = compareClosures; + pt_abi_handlers overrides = {}; + overrides.offset = offsetof(NativeClosureObject, std); + overrides.free_obj = freeClosureObject; + overrides.get_gc = closureGc; + overrides.clone_obj = cloneClosureObject; + overrides.get_closure = getClosure; + overrides.compare = compareClosures; + pt_native_closure_handlers = pt_abi_object_handlers(overrides); /* registered under a builder name other than `cls` on purpose — the * side-by-side parity scan pairs `cls.method(...)` lines with the @@ -210,8 +211,8 @@ PT_MINIT_REGISTRATION(pt_register_native_closure) holder.method("__invoke", reg::Public, 0, { reg::Arg{ "args", reg::detail::flagBits(false, true), nullptr } }, invokeNativeClosure); pt_ce_native_closure = holder.register_(); pt_ce_native_closure->ce_flags |= ZEND_ACC_FINAL | ZEND_ACC_NO_DYNAMIC_PROPERTIES | ZEND_ACC_NOT_SERIALIZABLE; - pt_ce_native_closure->create_object = createClosureObject; - pt_native_closure_invoke_fn = (zend_function *) zend_hash_str_find_ptr(&pt_ce_native_closure->function_table, PT_LC("__invoke")); + PT_CE(pt_ce_native_closure, create_object) = createClosureObject; + pt_native_closure_invoke_fn = (zend_function *) zend_hash_str_find_ptr(&PT_CE(pt_ce_native_closure, function_table), PT_LC("__invoke")); ZEND_ASSERT(pt_native_closure_invoke_fn != NULL); } @@ -463,11 +464,11 @@ bool pt_engine_call_node_callback(zval *callback, zval *node, zval *scope) bool handled; bool ok = pt_class_statements_gatherer_invoke(object, node, scope, handled); if (handled) return ok; - if (EXPECTED(object->handlers->get_closure != NULL)) { + if (EXPECTED(PT_OBJ_HANDLER(object, get_closure) != NULL)) { zend_class_entry *calledScope; zend_function *fn; zend_object *thisObject; - if (EXPECTED(object->handlers->get_closure(object, &calledScope, &fn, &thisObject, false) == SUCCESS)) { + if (EXPECTED(PT_OBJ_HANDLER(object, get_closure)(object, &calledScope, &fn, &thisObject, false) == SUCCESS)) { zval ret; zend_call_known_function(fn, thisObject, calledScope, &ret, 2, argv, NULL); zval_ptr_dtor(&ret); @@ -478,85 +479,11 @@ bool pt_engine_call_node_callback(zval *callback, zval *node, zval *scope) return !pt_type_call_callable(callback, 2, argv).isUndef(); } -namespace { - -/* one call continued on a fresh stack: the body, and the VM state it left - * behind — zend_fiber_switch_context() restores the caller's state from - * before the switch, which the body may legitimately have changed */ -struct FreshStackCall -{ - void (*body)(void *); - void *data; - bool bailout; - zend_vm_stack vmStack; - zval *vmStackTop; - zval *vmStackEnd; - size_t vmStackPageSize; - int errorReporting; -}; - -/* the call the coroutine starting next runs (read once, at its start) */ -FreshStackCall *pt_fresh_stack_call = nullptr; - -/* identifies the contexts in fiber observers */ -char pt_fresh_stack_kind; - -ZEND_STACK_ALIGNED void freshStackCoroutine(zend_fiber_transfer *transfer) -{ - (void) transfer; /* already addresses the caller, which is where it returns */ - FreshStackCall *call = pt_fresh_stack_call; - zend_fiber_context *context = EG(current_fiber_context); -#ifdef ZEND_CHECK_STACK_LIMIT - EG(stack_base) = zend_fiber_stack_base(context->stack); - EG(stack_limit) = zend_fiber_stack_limit(context->stack); -#endif - zend_try { - call->body(call->data); - } zend_catch { - call->bailout = true; - } zend_end_try(); - call->vmStack = EG(vm_stack); - call->vmStackTop = EG(vm_stack_top); - call->vmStackEnd = EG(vm_stack_end); - call->vmStackPageSize = EG(vm_stack_page_size); - call->errorReporting = EG(error_reporting); -} - -} // namespace - void pt_engine_run_on_fresh_stack(void (*body)(void *), void *data) { - zend_fiber_context context; - memset(&context, 0, sizeof(context)); - if (UNEXPECTED(zend_fiber_init_context(&context, &pt_fresh_stack_kind, freshStackCoroutine, PT_ENGINE_FRESH_STACK_SIZE_LIMIT) == FAILURE)) { - /* no stack to be had: continue here, where the engine's own stack - * limit decides */ - if (EG(exception) != NULL) { - zend_clear_exception(); - } - body(data); - return; - } - - FreshStackCall call = { body, data, false, NULL, NULL, NULL, 0, 0 }; - FreshStackCall *previous = pt_fresh_stack_call; - pt_fresh_stack_call = &call; - zend_fiber_transfer transfer; - transfer.context = &context; - transfer.flags = 0; - ZVAL_NULL(&transfer.value); - /* returns once the body finished; the dead context is destroyed then */ - zend_fiber_switch_context(&transfer); - pt_fresh_stack_call = previous; - - EG(vm_stack) = call.vmStack; - EG(vm_stack_top) = call.vmStackTop; - EG(vm_stack_end) = call.vmStackEnd; - EG(vm_stack_page_size) = call.vmStackPageSize; - EG(error_reporting) = call.errorReporting; - if (UNEXPECTED(call.bailout)) { - zend_bailout(); - } + /* the fiber context, its switch and the bailout around the body are + * engine internals of each minor (Abi.cpp) */ + pt_abi.run_on_fresh_stack(body, data); } /* }}} */ @@ -596,10 +523,10 @@ const NodeAbstractInfo *nodeAbstractInfo() if (EXPECTED(pt_node_abstract_info.generation == pt_engine_generation && pt_node_abstract_info.ce != NULL)) return &pt_node_abstract_info; zend_class_entry *ce = pt_class(PT_CLASS_NODE_ABSTRACT); if (UNEXPECTED(ce == NULL)) return NULL; - zend_property_info *attributes = (zend_property_info *) zend_hash_str_find_ptr(&ce->properties_info, PT_LC("attributes")); - zend_function *getAttribute = (zend_function *) zend_hash_str_find_ptr(&ce->function_table, PT_LC("getattribute")); - zend_function *setAttribute = (zend_function *) zend_hash_str_find_ptr(&ce->function_table, PT_LC("setattribute")); - zend_function *getComments = (zend_function *) zend_hash_str_find_ptr(&ce->function_table, PT_LC("getcomments")); + zend_property_info *attributes = (zend_property_info *) zend_hash_str_find_ptr(&PT_CE(ce, properties_info), PT_LC("attributes")); + zend_function *getAttribute = (zend_function *) zend_hash_str_find_ptr(&PT_CE(ce, function_table), PT_LC("getattribute")); + zend_function *setAttribute = (zend_function *) zend_hash_str_find_ptr(&PT_CE(ce, function_table), PT_LC("setattribute")); + zend_function *getComments = (zend_function *) zend_hash_str_find_ptr(&PT_CE(ce, function_table), PT_LC("getcomments")); if (UNEXPECTED(attributes == NULL || (attributes->flags & ZEND_ACC_STATIC) != 0 || getAttribute == NULL || setAttribute == NULL || getComments == NULL)) { zend_throw_error(NULL, "phpstan_turbo: %s does not declare the attributes it is expected to", ZSTR_VAL(ce->name)); return NULL; @@ -614,9 +541,9 @@ bool inheritsNodeAbstract(zend_class_entry *ce, const NodeAbstractInfo *info) NodeClassEntry &entry = pt_node_class_cache[(size_t) (h >> (sizeof(uintptr_t) * 8 - PT_NODE_CLASS_CACHE_BITS))]; if (EXPECTED(entry.ce == ce && entry.generation == pt_engine_generation)) return entry.inherits; bool inherits = instanceof_function(ce, info->ce) - && zend_hash_str_find_ptr(&ce->function_table, PT_LC("getattribute")) == info->getAttribute - && zend_hash_str_find_ptr(&ce->function_table, PT_LC("setattribute")) == info->setAttribute - && zend_hash_str_find_ptr(&ce->function_table, PT_LC("getcomments")) == info->getComments; + && zend_hash_str_find_ptr(&PT_CE(ce, function_table), PT_LC("getattribute")) == info->getAttribute + && zend_hash_str_find_ptr(&PT_CE(ce, function_table), PT_LC("setattribute")) == info->setAttribute + && zend_hash_str_find_ptr(&PT_CE(ce, function_table), PT_LC("getcomments")) == info->getComments; entry = { ce, pt_engine_generation, inherits }; return inherits; } diff --git a/turbo-ext/src/ExprHandlerRegistry.cpp b/turbo-ext/src/ExprHandlerRegistry.cpp index fdd2c4abde4..07d14005851 100644 --- a/turbo-ext/src/ExprHandlerRegistry.cpp +++ b/turbo-ext/src/ExprHandlerRegistry.cpp @@ -41,7 +41,7 @@ zval *handlersByClassSlot() if (CE_STATIC_MEMBERS(ce) == NULL) { zend_class_init_statics(ce); } - zend_property_info *info = (zend_property_info *) zend_hash_str_find_ptr(&ce->properties_info, PT_LC("exprHandlersByClass")); + zend_property_info *info = (zend_property_info *) zend_hash_str_find_ptr(&PT_CE(ce, properties_info), PT_LC("exprHandlersByClass")); ZEND_ASSERT(info != NULL && (info->flags & ZEND_ACC_STATIC) != 0); pt_ehr_statics_slot = CE_STATIC_MEMBERS(ce) + info->offset; pt_ehr_statics_ce = ce; diff --git a/turbo-ext/src/ExprPrinter.cpp b/turbo-ext/src/ExprPrinter.cpp index c8b20a5459e..655fec687ae 100644 --- a/turbo-ext/src/ExprPrinter.cpp +++ b/turbo-ext/src/ExprPrinter.cpp @@ -60,7 +60,7 @@ class ExprPrinter } zend_class_entry *ce = Z_OBJCE_P(printer); - zend_function *fn = (zend_function *) zend_hash_str_find_ptr(&ce->function_table, "prettyprintexpr", sizeof("prettyprintexpr") - 1); + zend_function *fn = (zend_function *) zend_hash_str_find_ptr(&PT_CE(ce, function_table), "prettyprintexpr", sizeof("prettyprintexpr") - 1); if (UNEXPECTED(fn == NULL)) { zend_throw_error(NULL, "phpstan_turbo: prettyPrintExpr not found"); return NULL; diff --git a/turbo-ext/src/ExpressionResult.cpp b/turbo-ext/src/ExpressionResult.cpp index 142b860daa4..8705387f6e7 100644 --- a/turbo-ext/src/ExpressionResult.cpp +++ b/turbo-ext/src/ExpressionResult.cpp @@ -1366,13 +1366,13 @@ void rememberCollection(zend_object *factory, zval *collection, zval *defaultNar bool isGeneratedFactory(zend_object *factory) { zend_class_entry *ce = factory->ce; - if ((ce->ce_flags & ZEND_ACC_ANON_CLASS) == 0 || ce->type != ZEND_USER_CLASS || ce->default_properties_count != 1 || ce->info.user.filename == NULL) return false; - zend_property_info *info = (zend_property_info *) zend_hash_str_find_ptr(&ce->properties_info, PT_LC("container")); + if ((ce->ce_flags & ZEND_ACC_ANON_CLASS) == 0 || ce->type != ZEND_USER_CLASS || PT_CE(ce, default_properties_count) != 1 || PT_CE(ce, info).user.filename == NULL) return false; + zend_property_info *info = (zend_property_info *) zend_hash_str_find_ptr(&PT_CE(ce, properties_info), PT_LC("container")); if (info == NULL || info->offset != OBJ_PROP_TO_OFFSET(0)) return false; zval *container = OBJ_PROP_NUM(factory, 0); if (Z_TYPE_P(container) != IS_OBJECT) return false; zend_class_entry *containerCe = Z_OBJCE_P(container); - return containerCe->type == ZEND_USER_CLASS && containerCe->info.user.filename != NULL && zend_string_equals(containerCe->info.user.filename, ce->info.user.filename); + return containerCe->type == ZEND_USER_CLASS && PT_CE(containerCe, info).user.filename != NULL && zend_string_equals(PT_CE(containerCe, info).user.filename, PT_CE(ce, info).user.filename); } inline zval *nullable(zval *value) diff --git a/turbo-ext/src/ForeachHandler.cpp b/turbo-ext/src/ForeachHandler.cpp index ab3df39c44b..49ca2f9827f 100644 --- a/turbo-ext/src/ForeachHandler.cpp +++ b/turbo-ext/src/ForeachHandler.cpp @@ -117,8 +117,8 @@ DocCommentMethodCache pt_feh_doc_comment_cache; if (UNEXPECTED(cache.ce != ce || cache.generation != pt_engine_generation)) { zend_class_entry *nodeAbstract = pt_class(PT_CLASS_NODE_ABSTRACT); if (UNEXPECTED(nodeAbstract == NULL)) return false; - zend_function *base = (zend_function *) zend_hash_str_find_ptr(&nodeAbstract->function_table, PT_LC("getdoccomment")); - zend_function *own = (zend_function *) zend_hash_str_find_ptr(&ce->function_table, PT_LC("getdoccomment")); + zend_function *base = (zend_function *) zend_hash_str_find_ptr(&PT_CE(nodeAbstract, function_table), PT_LC("getdoccomment")); + zend_function *own = (zend_function *) zend_hash_str_find_ptr(&PT_CE(ce, function_table), PT_LC("getdoccomment")); cache = { ce, pt_engine_generation, base != NULL && own == base, base }; } if (EXPECTED(cache.inherits)) { diff --git a/turbo-ext/src/GenericObjectType.cpp b/turbo-ext/src/GenericObjectType.cpp index 4ead7c89c6a..ebf0fe393bb 100644 --- a/turbo-ext/src/GenericObjectType.cpp +++ b/turbo-ext/src/GenericObjectType.cpp @@ -38,13 +38,7 @@ zend_class_entry *pt_ce_generic_object_type = nullptr; { #ifdef ZEND_CHECK_STACK_LIMIT if (UNEXPECTED(zend_call_stack_overflowed(EG(stack_limit)))) { -#if PHP_VERSION_ID >= 80400 - zend_call_stack_size_error(); -#else - /* static in PHP 8.3; the same message */ - zend_throw_error(nullptr, "Maximum call stack size of %zu bytes (zend.max_allowed_stack_size - zend.reserved_stack_size) reached. Infinite recursion?", - (size_t) ((uintptr_t) EG(stack_base) - (uintptr_t) EG(stack_limit))); -#endif + pt_abi_call_stack_size_error(); return true; } #endif diff --git a/turbo-ext/src/GetTemplateTypeType.cpp b/turbo-ext/src/GetTemplateTypeType.cpp index d1120deb987..1bef313a6f9 100644 --- a/turbo-ext/src/GetTemplateTypeType.cpp +++ b/turbo-ext/src/GetTemplateTypeType.cpp @@ -248,7 +248,7 @@ class GetTemplateTypeType * pending when missing */ [[nodiscard]] static zval *classConstant(zend_class_entry *ce, const char *name, size_t len) { - zend_class_constant *constant = (zend_class_constant *) zend_hash_str_find_ptr(&ce->constants_table, name, len); + zend_class_constant *constant = (zend_class_constant *) zend_hash_str_find_ptr(&PT_CE(ce, constants_table), name, len); if (UNEXPECTED(constant == NULL)) { zend_throw_error(NULL, "phpstan_turbo: %s::%s not found", ZSTR_VAL(ce->name), name); return NULL; diff --git a/turbo-ext/src/IntersectionType.cpp b/turbo-ext/src/IntersectionType.cpp index 68a3b5684be..bdeb4a1755b 100644 --- a/turbo-ext/src/IntersectionType.cpp +++ b/turbo-ext/src/IntersectionType.cpp @@ -137,7 +137,7 @@ static bool collectCallableAcceptorCombination(zval *combination, void *opaque) { zend_class_entry *ce = pt_class(classIdx); if (UNEXPECTED(ce == NULL)) return NULL; - zend_class_constant *constant = (zend_class_constant *) zend_hash_str_find_ptr(&ce->constants_table, name, len); + zend_class_constant *constant = (zend_class_constant *) zend_hash_str_find_ptr(&PT_CE(ce, constants_table), name, len); if (UNEXPECTED(constant == NULL)) { zend_throw_error(NULL, "phpstan_turbo: %s::%s not found", ZSTR_VAL(ce->name), name); return NULL; diff --git a/turbo-ext/src/IsSuperTypeOfResult.cpp b/turbo-ext/src/IsSuperTypeOfResult.cpp index 862fcc99a49..76b682a3d45 100644 --- a/turbo-ext/src/IsSuperTypeOfResult.cpp +++ b/turbo-ext/src/IsSuperTypeOfResult.cpp @@ -434,12 +434,7 @@ class IsSuperTypeOfResult holderRef.propAtWrite(slots::result, zv::Val::copyOf(cb)); holderRef.propAtWrite(slots::reasons, zv::Val::copyOf(entry.value().deref())); zval closure; -#if PHP_VERSION_ID >= 80600 - /* php-src fbb2e1f23d6: $this is passed as zend_object* from 8.6 on */ - zend_create_closure(&closure, pt_decorated_lazy_reason_invoke, pt_ce_decorated_lazy_reason, pt_ce_decorated_lazy_reason, Z_OBJ(holder)); -#else - zend_create_closure(&closure, pt_decorated_lazy_reason_invoke, pt_ce_decorated_lazy_reason, pt_ce_decorated_lazy_reason, &holder); -#endif + pt_abi_create_closure(&closure, pt_decorated_lazy_reason_invoke, pt_ce_decorated_lazy_reason, pt_ce_decorated_lazy_reason, Z_OBJ(holder)); zval_ptr_dtor(&holder); /* the closure holds its own reference */ wrapped.push(zv::Val::adopt(closure)); } @@ -724,7 +719,7 @@ PT_MINIT_REGISTRATION(pt_register_is_super_type_of_result) holder.method("__invoke", reg::Public, 0, {}, IsSuperTypeOfResult::invokeDecoratedLazyReason); pt_ce_decorated_lazy_reason = holder.register_(); pt_ce_decorated_lazy_reason->ce_flags |= ZEND_ACC_FINAL; - pt_decorated_lazy_reason_invoke = (zend_function *) zend_hash_str_find_ptr(&pt_ce_decorated_lazy_reason->function_table, "__invoke", sizeof("__invoke") - 1); + pt_decorated_lazy_reason_invoke = (zend_function *) zend_hash_str_find_ptr(&PT_CE(pt_ce_decorated_lazy_reason, function_table), "__invoke", sizeof("__invoke") - 1); ZEND_ASSERT(pt_decorated_lazy_reason_invoke != NULL); } diff --git a/turbo-ext/src/LateResolvableArrayShapeType.cpp b/turbo-ext/src/LateResolvableArrayShapeType.cpp index fe60326c987..5cb2e1cf68a 100644 --- a/turbo-ext/src/LateResolvableArrayShapeType.cpp +++ b/turbo-ext/src/LateResolvableArrayShapeType.cpp @@ -680,7 +680,7 @@ class LateResolvableArrayShapeType if (UNEXPECTED(k == NULL)) return false; zend_class_entry *ce = pt_class(PT_CLASS_ARRAY_SHAPE_NODE); if (UNEXPECTED(ce == NULL)) return false; - zend_class_constant *constant = (zend_class_constant *) zend_hash_str_find_ptr(&ce->constants_table, constantName, strlen(constantName)); + zend_class_constant *constant = (zend_class_constant *) zend_hash_str_find_ptr(&PT_CE(ce, constants_table), constantName, strlen(constantName)); if (UNEXPECTED(constant == NULL)) { zend_throw_error(NULL, "phpstan_turbo: %s::%s not found", ZSTR_VAL(ce->name), constantName); return false; diff --git a/turbo-ext/src/MixedType.cpp b/turbo-ext/src/MixedType.cpp index 18cb068f50f..6e0bd470e5b 100644 --- a/turbo-ext/src/MixedType.cpp +++ b/turbo-ext/src/MixedType.cpp @@ -1289,7 +1289,7 @@ class MixedType static zv::Val identityCallback() { zval closure; - zend_create_closure(&closure, pt_identity_callback_fn, pt_ce_identity_callback, pt_ce_identity_callback, NULL); + pt_abi_create_closure(&closure, pt_identity_callback_fn, pt_ce_identity_callback, pt_ce_identity_callback, NULL); return zv::Val::adopt(closure); } }; @@ -1301,7 +1301,7 @@ using phpstanturbo::MixedType; zv::Val pt_type_identity_callback() { zval closure; - zend_create_closure(&closure, pt_identity_callback_fn, pt_ce_identity_callback, pt_ce_identity_callback, NULL); + pt_abi_create_closure(&closure, pt_identity_callback_fn, pt_ce_identity_callback, pt_ce_identity_callback, NULL); return zv::Val::adopt(closure); } @@ -1339,7 +1339,7 @@ bool pt_identity_callback_direct_invoke(const zend_function *fn, zend_object *ob { (void) object; handled = false; - if (fn->internal_function.handler != identityCallbackIdentity && !(fn->common.scope == pt_ce_identity_callback && zend_string_equals_literal(fn->common.function_name, "identity"))) { + if (PT_INTERNAL_HANDLER(fn) != identityCallbackIdentity && !(fn->common.scope == pt_ce_identity_callback && zend_string_equals_literal(fn->common.function_name, "identity"))) { return false; } if (argc != 1 || Z_TYPE_P(argv) != IS_OBJECT) return false; @@ -1500,7 +1500,7 @@ PT_MINIT_REGISTRATION(pt_register_mixed_type) holder.method("identity", reg::PublicStatic, 1, { reg::obj("type", ptcls::type) }, identityCallbackIdentity, &ptret::type); pt_ce_identity_callback = holder.register_(); pt_ce_identity_callback->ce_flags |= ZEND_ACC_FINAL; - pt_identity_callback_fn = (zend_function *) zend_hash_str_find_ptr(&pt_ce_identity_callback->function_table, PT_LC("identity")); + pt_identity_callback_fn = (zend_function *) zend_hash_str_find_ptr(&PT_CE(pt_ce_identity_callback, function_table), PT_LC("identity")); ZEND_ASSERT(pt_identity_callback_fn != NULL); reg::Class cls("PHPStan\\Type\\MixedType"); diff --git a/turbo-ext/src/MutatingScope.cpp b/turbo-ext/src/MutatingScope.cpp index 6b5567f58a8..66e26133896 100644 --- a/turbo-ext/src/MutatingScope.cpp +++ b/turbo-ext/src/MutatingScope.cpp @@ -2636,7 +2636,14 @@ class MutatingScope PHP_MD5Update(&context, (const unsigned char *) ZSTR_VAL(parts.s), ZSTR_LEN(parts.s)); } PHP_MD5Final(digest, &context); - make_digest_ex(hex, digest, sizeof(digest)); + /* make_digest_ex()'s lowercase hex, spelled out: its length parameter + * became a size_t in 8.5, so a call compiles differently per version */ + static const char hexDigits[] = "0123456789abcdef"; + for (size_t i = 0; i < sizeof(digest); i++) { + hex[2 * i] = hexDigits[digest[i] >> 4]; + hex[2 * i + 1] = hexDigits[digest[i] & 0x0f]; + } + hex[32] = '\0'; smart_str_free(&parts); return zv::Val::string(hex, 32); } diff --git a/turbo-ext/src/NameNodeAccess.cpp b/turbo-ext/src/NameNodeAccess.cpp index 9a2c59812ef..c0a0d444987 100644 --- a/turbo-ext/src/NameNodeAccess.cpp +++ b/turbo-ext/src/NameNodeAccess.cpp @@ -192,9 +192,9 @@ bool pt_name_node_is_fully_qualified(zval *node, bool &out) zv::Val pt_name_node_new(int classIdx, zval *name) { /* Name: final public function __construct($name, array $attributes = []) { - * $this->attributes = $attributes; $this->name = self::prepareName($name); } + * $PT_CE(this, attributes) = $attributes; $this->name = self::prepareName($name); } * Identifier: public function __construct(string $name, array $attributes = []) { - * if ($name === '') throw ...; $this->attributes = $attributes; $this->name = $name; } + * if ($name === '') throw ...; $PT_CE(this, attributes) = $attributes; $this->name = $name; } * — for a non-empty string both write it as is */ if (EXPECTED(Z_TYPE_P(name) == IS_STRING && Z_STRLEN_P(name) != 0)) { zend_class_entry *ce = pt_class(classIdx); diff --git a/turbo-ext/src/NodeCallbackScope.cpp b/turbo-ext/src/NodeCallbackScope.cpp index 0d15ef5c188..9155fa4f341 100644 --- a/turbo-ext/src/NodeCallbackScope.cpp +++ b/turbo-ext/src/NodeCallbackScope.cpp @@ -58,7 +58,7 @@ zend_function *pt_ncs_weakref_get_fn = nullptr; zv::Val weakReferenceCall(zend_function *&fn, const char *lcname, size_t len, zend_object *object, uint32_t argc, zval *argv) { if (UNEXPECTED(fn == nullptr)) { - fn = (zend_function *) zend_hash_str_find_ptr(&zend_ce_weakref->function_table, lcname, len); + fn = (zend_function *) zend_hash_str_find_ptr(&PT_CE(zend_ce_weakref, function_table), lcname, len); if (UNEXPECTED(fn == nullptr)) { zend_throw_error(NULL, "phpstan_turbo: WeakReference::%s() does not exist", lcname); return zv::Val(); diff --git a/turbo-ext/src/NodeScopeResolver.cpp b/turbo-ext/src/NodeScopeResolver.cpp index c5c7e190178..4c78ea9b6ba 100644 --- a/turbo-ext/src/NodeScopeResolver.cpp +++ b/turbo-ext/src/NodeScopeResolver.cpp @@ -70,7 +70,7 @@ NsrStatics pt_nsr_statics; zval *staticSlotOf(zend_class_entry *ce, const char *name, size_t len) { - zend_property_info *info = (zend_property_info *) zend_hash_str_find_ptr(&ce->properties_info, name, len); + zend_property_info *info = (zend_property_info *) zend_hash_str_find_ptr(&PT_CE(ce, properties_info), name, len); ZEND_ASSERT(info != NULL && (info->flags & ZEND_ACC_STATIC) != 0); return CE_STATIC_MEMBERS(ce) + info->offset; } diff --git a/turbo-ext/src/NodeTraverser.cpp b/turbo-ext/src/NodeTraverser.cpp index bdcae25b397..1b963bed3ac 100644 --- a/turbo-ext/src/NodeTraverser.cpp +++ b/turbo-ext/src/NodeTraverser.cpp @@ -170,7 +170,7 @@ static pt_trav_class_info *pt_trav_class_info_for(zend_object *obj) info = (pt_trav_class_info *) ecalloc(1, sizeof(pt_trav_class_info)); - fn = (zend_function *) zend_hash_str_find_ptr(&ce->function_table, "getsubnodenames", sizeof("getsubnodenames") - 1); + fn = (zend_function *) zend_hash_str_find_ptr(&PT_CE(ce, function_table), "getsubnodenames", sizeof("getsubnodenames") - 1); if (fn != NULL && (fn->common.fn_flags & ZEND_ACC_ABSTRACT) == 0) { zend_call_known_function(fn, obj, ce, &names, 0, NULL, NULL); if (!EG(exception) && Z_TYPE(names) == IS_ARRAY) { @@ -181,7 +181,7 @@ static pt_trav_class_info *pt_trav_class_info_for(zend_object *obj) ZEND_HASH_FOREACH_VAL(Z_ARRVAL(names), name_zv) { zend_property_info *prop; if (Z_TYPE_P(name_zv) != IS_STRING) continue; - prop = (zend_property_info *) zend_hash_find_ptr(&ce->properties_info, Z_STR_P(name_zv)); + prop = (zend_property_info *) zend_hash_find_ptr(&PT_CE(ce, properties_info), Z_STR_P(name_zv)); if (prop == NULL || (prop->flags & ZEND_ACC_STATIC) != 0) continue; info->offsets[info->count] = (uint32_t) prop->offset; info->names[info->count] = zend_string_copy(Z_STR_P(name_zv)); @@ -905,7 +905,7 @@ class NodeTraverser /* hook lookup by lowercased name; NULL when the class lacks the method */ static zend_function *findHook(zend_class_entry *ce, const char *lcname, size_t len) { - return (zend_function *) zend_hash_str_find_ptr(&ce->function_table, lcname, len); + return (zend_function *) zend_hash_str_find_ptr(&PT_CE(ce, function_table), lcname, len); } /* calls one visitor hook through its cached zend_function; diff --git a/turbo-ext/src/ObjectShapeType.cpp b/turbo-ext/src/ObjectShapeType.cpp index 35ff40f9ef1..97c48c5b96f 100644 --- a/turbo-ext/src/ObjectShapeType.cpp +++ b/turbo-ext/src/ObjectShapeType.cpp @@ -1255,7 +1255,7 @@ PT_MINIT_REGISTRATION(pt_register_object_shape_type) holder.method("__invoke", reg::Public, 1, { reg::stringArg("reason") }, invokeReasonDecorator, &ptret::string); pt_ce_object_shape_reason_decorator = holder.register_(); pt_ce_object_shape_reason_decorator->ce_flags |= ZEND_ACC_FINAL; - pt_object_shape_reason_decorator_invoke = (zend_function *) zend_hash_str_find_ptr(&pt_ce_object_shape_reason_decorator->function_table, PT_LC("__invoke")); + pt_object_shape_reason_decorator_invoke = (zend_function *) zend_hash_str_find_ptr(&PT_CE(pt_ce_object_shape_reason_decorator, function_table), PT_LC("__invoke")); ZEND_ASSERT(pt_object_shape_reason_decorator_invoke != NULL); reg::Class cls("PHPStan\\Type\\ObjectShapeType"); diff --git a/turbo-ext/src/ObjectType.cpp b/turbo-ext/src/ObjectType.cpp index 224940abe45..7202f46f893 100644 --- a/turbo-ext/src/ObjectType.cpp +++ b/turbo-ext/src/ObjectType.cpp @@ -137,7 +137,7 @@ static zval *pt_ot_static(pt_ot_static_kind kind) zend_class_init_statics(ce); } for (int i = 0; i < PT_OT_STATIC_COUNT; i++) { - zend_property_info *info = (zend_property_info *) zend_hash_str_find_ptr(&ce->properties_info, pt_ot_static_names[i], strlen(pt_ot_static_names[i])); + zend_property_info *info = (zend_property_info *) zend_hash_str_find_ptr(&PT_CE(ce, properties_info), pt_ot_static_names[i], strlen(pt_ot_static_names[i])); ZEND_ASSERT(info != NULL && (info->flags & ZEND_ACC_STATIC) != 0); pt_ot_statics[i] = CE_STATIC_MEMBERS(ce) + info->offset; } @@ -291,12 +291,7 @@ static zv::Val pt_ot_callback(pt_ot_callback_kind kind, zval *object, zval *arg, ref.propAtWrite(slots::cachedInterfaces, arg != NULL ? zv::Val::copyOf(zv::Ref(arg)) : zv::Val::null()); ref.propAtWrite(slots::currentAncestors, scope != NULL ? zv::Val::copyOf(zv::Ref(scope)) : zv::Val::null()); zval closure; -#if PHP_VERSION_ID >= 80600 - /* php-src fbb2e1f23d6: $this is passed as zend_object* from 8.6 on */ - zend_create_closure(&closure, pt_object_type_callback_invoke_fn, pt_ce_object_type_callback, pt_ce_object_type_callback, Z_OBJ(holder)); -#else - zend_create_closure(&closure, pt_object_type_callback_invoke_fn, pt_ce_object_type_callback, pt_ce_object_type_callback, &holder); -#endif + pt_abi_create_closure(&closure, pt_object_type_callback_invoke_fn, pt_ce_object_type_callback, pt_ce_object_type_callback, Z_OBJ(holder)); if (holderOut != NULL) { ZVAL_COPY_VALUE(holderOut, &holder); /* the closure holds its own reference */ } else { @@ -309,7 +304,7 @@ static zv::Val pt_ot_callback(pt_ot_callback_kind kind, zval *object, zval *arg, static zv::Val pt_ot_identity_callback() { zval closure; - zend_create_closure(&closure, pt_object_type_callback_identity, pt_ce_object_type_callback, pt_ce_object_type_callback, NULL); + pt_abi_create_closure(&closure, pt_object_type_callback_identity, pt_ce_object_type_callback, pt_ce_object_type_callback, NULL); return zv::Val::adopt(closure); } @@ -2714,7 +2709,7 @@ class ObjectType if (zv::Ref(typeToRemove).instanceOf(pt_ce_object_type)) { zend_class_entry *unionCe = pt_ce_union_type; if (UNEXPECTED(unionCe == NULL)) return zv::Val(); - zend_class_constant *constant = (zend_class_constant *) zend_hash_str_find_ptr(&unionCe->constants_table, PT_LC("EQUAL_UNION_CLASSES")); + zend_class_constant *constant = (zend_class_constant *) zend_hash_str_find_ptr(&PT_CE(unionCe, constants_table), PT_LC("EQUAL_UNION_CLASSES")); if (UNEXPECTED(constant == NULL)) { zend_throw_error(NULL, "phpstan_turbo: %s::EQUAL_UNION_CLASSES not found", ZSTR_VAL(unionCe->name)); return zv::Val(); @@ -3599,8 +3594,8 @@ PT_MINIT_REGISTRATION(pt_register_object_type) holder.method("identity", reg::PublicStatic, 1, { reg::obj("type", ptcls::type) }, objectTypeCallbackIdentity, &ptret::type); pt_ce_object_type_callback = holder.register_(); pt_ce_object_type_callback->ce_flags |= ZEND_ACC_FINAL; - pt_object_type_callback_invoke_fn = (zend_function *) zend_hash_str_find_ptr(&pt_ce_object_type_callback->function_table, PT_LC("__invoke")); - pt_object_type_callback_identity = (zend_function *) zend_hash_str_find_ptr(&pt_ce_object_type_callback->function_table, PT_LC("identity")); + pt_object_type_callback_invoke_fn = (zend_function *) zend_hash_str_find_ptr(&PT_CE(pt_ce_object_type_callback, function_table), PT_LC("__invoke")); + pt_object_type_callback_identity = (zend_function *) zend_hash_str_find_ptr(&PT_CE(pt_ce_object_type_callback, function_table), PT_LC("identity")); ZEND_ASSERT(pt_object_type_callback_invoke_fn != NULL && pt_object_type_callback_identity != NULL); reg::Class cls("PHPStan\\Type\\ObjectType"); diff --git a/turbo-ext/src/PassedByReference.cpp b/turbo-ext/src/PassedByReference.cpp index 182091604b8..b455bcf14bb 100644 --- a/turbo-ext/src/PassedByReference.cpp +++ b/turbo-ext/src/PassedByReference.cpp @@ -33,7 +33,7 @@ static zval *pt_pbr_registry() if (CE_STATIC_MEMBERS(ce) == NULL) { zend_class_init_statics(ce); } - zend_property_info *info = (zend_property_info *) zend_hash_str_find_ptr(&ce->properties_info, PT_LC("registry")); + zend_property_info *info = (zend_property_info *) zend_hash_str_find_ptr(&PT_CE(ce, properties_info), PT_LC("registry")); ZEND_ASSERT(info != NULL && (info->flags & ZEND_ACC_STATIC) != 0); pt_pbr_registry_slot = CE_STATIC_MEMBERS(ce) + info->offset; pt_pbr_registry_ce = ce; diff --git a/turbo-ext/src/PhpClassReflectionExtension.cpp b/turbo-ext/src/PhpClassReflectionExtension.cpp index 9f2d92b8451..b883e2ca518 100644 --- a/turbo-ext/src/PhpClassReflectionExtension.cpp +++ b/turbo-ext/src/PhpClassReflectionExtension.cpp @@ -509,7 +509,7 @@ class PhpClassReflectionExtension } return zv::Val(); } - zend_function *fn = (zend_function *) zend_hash_str_find_ptr(&ce->function_table, lcmethod, methodLen); + zend_function *fn = (zend_function *) zend_hash_str_find_ptr(&PT_CE(ce, function_table), lcmethod, methodLen); if (UNEXPECTED(fn == NULL)) { zend_throw_error(NULL, "phpstan_turbo: method %s::%s not found", className, lcmethod); return zv::Val(); @@ -545,8 +545,8 @@ class PhpClassReflectionExtension } zval object; if (UNEXPECTED(object_init_ex(&object, ce) != SUCCESS)) return zv::Val(); - if (ce->constructor != NULL) { - zend_call_known_instance_method(ce->constructor, Z_OBJ(object), NULL, argc, argv); + if (PT_CE(ce, constructor) != NULL) { + zend_call_known_instance_method(PT_CE(ce, constructor), Z_OBJ(object), NULL, argc, argv); if (UNEXPECTED(EG(exception))) { zval_ptr_dtor(&object); return zv::Val(); diff --git a/turbo-ext/src/PhpDocsResolver.cpp b/turbo-ext/src/PhpDocsResolver.cpp index 40f47ae8d06..61d43a60333 100644 --- a/turbo-ext/src/PhpDocsResolver.cpp +++ b/turbo-ext/src/PhpDocsResolver.cpp @@ -115,7 +115,7 @@ zend_never_inline ZEND_COLD void memberCallOnNonObject(const char *method, zval zend_function *resolveMethod(pt_method_site &site, zend_class_entry *ce, const char *lcname, size_t len) { if (EXPECTED(site.ce == ce && site.generation == pt_engine_generation && site.fn != NULL)) return site.fn; - zend_function *fn = (zend_function *) zend_hash_str_find_ptr(&ce->function_table, lcname, len); + zend_function *fn = (zend_function *) zend_hash_str_find_ptr(&PT_CE(ce, function_table), lcname, len); if (fn != NULL) site = { ce, fn, pt_engine_generation }; return fn; } diff --git a/turbo-ext/src/PropertyHandler.cpp b/turbo-ext/src/PropertyHandler.cpp index 7d98507652c..37579852108 100644 --- a/turbo-ext/src/PropertyHandler.cpp +++ b/turbo-ext/src/PropertyHandler.cpp @@ -74,7 +74,7 @@ bool inheritsNodeAbstractMethod(pt_method_site &site, zend_object *node, const c return false; } if (EXPECTED(site.ce == node->ce && site.generation == pt_engine_generation && site.fn != NULL)) return site.fn->common.scope == nodeAbstractCe; - zend_function *fn = (zend_function *) zend_hash_str_find_ptr(&node->ce->function_table, lcname, len); + zend_function *fn = (zend_function *) zend_hash_str_find_ptr(&PT_CE(node->ce, function_table), lcname, len); if (fn == NULL) return false; site = { node->ce, fn, pt_engine_generation }; return fn->common.scope == nodeAbstractCe; diff --git a/turbo-ext/src/RecursionGuard.cpp b/turbo-ext/src/RecursionGuard.cpp index 3cc011338ac..69ba98ac663 100644 --- a/turbo-ext/src/RecursionGuard.cpp +++ b/turbo-ext/src/RecursionGuard.cpp @@ -31,7 +31,7 @@ static zval *pt_rg_context() if (CE_STATIC_MEMBERS(ce) == NULL) { zend_class_init_statics(ce); } - zend_property_info *info = (zend_property_info *) zend_hash_str_find_ptr(&ce->properties_info, PT_LC("context")); + zend_property_info *info = (zend_property_info *) zend_hash_str_find_ptr(&PT_CE(ce, properties_info), PT_LC("context")); ZEND_ASSERT(info != NULL && (info->flags & ZEND_ACC_STATIC) != 0); pt_rg_context_slot = CE_STATIC_MEMBERS(ce) + info->offset; pt_rg_context_ce = ce; diff --git a/turbo-ext/src/ReflectionAccess.cpp b/turbo-ext/src/ReflectionAccess.cpp index 77d1d281886..04e8ce871f1 100644 --- a/turbo-ext/src/ReflectionAccess.cpp +++ b/turbo-ext/src/ReflectionAccess.cpp @@ -102,7 +102,7 @@ const RegistryMemberNames pt_registry_member_names[PT_REGISTRY_MEMBER_COUNT] = { zval *staticSlot(zend_class_entry *ce, const char *name, size_t len) { if (UNEXPECTED(!(ce->ce_flags & ZEND_ACC_CONSTANTS_UPDATED)) || UNEXPECTED(CE_STATIC_MEMBERS(ce) == NULL)) return NULL; - zend_property_info *info = (zend_property_info *) zend_hash_str_find_ptr(&ce->properties_info, name, len); + zend_property_info *info = (zend_property_info *) zend_hash_str_find_ptr(&PT_CE(ce, properties_info), name, len); if (UNEXPECTED(info == NULL || (info->flags & ZEND_ACC_STATIC) == 0)) return NULL; return CE_STATIC_MEMBERS(ce) + info->offset; } diff --git a/turbo-ext/src/ResolvedFunctionVariantWithOriginal.cpp b/turbo-ext/src/ResolvedFunctionVariantWithOriginal.cpp index 920f4d630f7..1bffe1006fa 100644 --- a/turbo-ext/src/ResolvedFunctionVariantWithOriginal.cpp +++ b/turbo-ext/src/ResolvedFunctionVariantWithOriginal.cpp @@ -103,7 +103,7 @@ zv::Val weakReferenceCreate(zval *object) { static zend_function *create = NULL; if (UNEXPECTED(create == NULL)) { - create = (zend_function *) zend_hash_str_find_ptr(&zend_ce_weakref->function_table, ZEND_STRL("create")); + create = (zend_function *) zend_hash_str_find_ptr(&PT_CE(zend_ce_weakref, function_table), ZEND_STRL("create")); ZEND_ASSERT(create != NULL); } zval result; @@ -131,12 +131,8 @@ zv::Val weakReferenceCreate(zval *object) /* the twin's closure names, for the engine's messages */ #define PT_RFV_CLASS "PHPStan\\Reflection\\ResolvedFunctionVariantWithOriginal" -#if PHP_VERSION_ID >= 80400 -#define PT_RFV_CLOSURE(method, line) PT_RFV_CLASS "::{closure:" PT_RFV_CLASS "::" method "():" line "}" -#else -/* PHP 8.3 names a closure by its namespace alone */ -#define PT_RFV_CLOSURE(method, line) PT_RFV_CLASS "::PHPStan\\Reflection\\{closure}" -#endif +/* the closure names differ: PHP 8.3 names a closure by its namespace alone */ +#define PT_RFV_CLOSURE(method, line) PT_ABI_SINCE(80400, PT_RFV_CLASS "::{closure:" PT_RFV_CLASS "::" method "():" line "}", PT_RFV_CLASS "::PHPStan\\Reflection\\{closure}") #define PT_RFV_PARAMETERS_CLOSURE PT_RFV_CLOSURE("getParameters", "90") /* }}} */ @@ -547,7 +543,7 @@ class ResolvedFunctionVariantWithOriginal zend_class_entry *extendedParameterCe = pt_class(PT_CLASS_EXTENDED_PARAMETER_REFLECTION); if (UNEXPECTED(extendedParameterCe == NULL)) return zv::Val(); if (UNEXPECTED(Z_TYPE_P(param) != IS_OBJECT || !instanceof_function(Z_OBJCE_P(param), extendedParameterCe))) { - zend_type_error(PT_RFV_PARAMETERS_CLOSURE "(): Argument #1 ($param) must be of type PHPStan\\Reflection\\ExtendedParameterReflection, %s given", zend_zval_value_name(param)); + zend_type_error("%s(): Argument #1 ($param) must be of type PHPStan\\Reflection\\ExtendedParameterReflection, %s given", PT_RFV_PARAMETERS_CLOSURE, zend_zval_value_name(param)); return zv::Val(); } @@ -687,7 +683,7 @@ class ResolvedFunctionVariantWithOriginal static void typeCallbackBody(zval *captures, uint32_t argc, zval *argv, zval *return_value) { if (UNEXPECTED(argc < 2)) { - zend_throw_error(zend_ce_argument_count_error, "Too few arguments to function " PT_RFV_CLOSURE("resolveResolvableTemplateTypes", "338") "(), %u passed and exactly 2 expected", argc); + zend_throw_error(zend_ce_argument_count_error, "Too few arguments to function %s(), %u passed and exactly 2 expected", PT_RFV_CLOSURE("resolveResolvableTemplateTypes", "338"), argc); return; } zend_object *self = Z_OBJ(captures[0]); @@ -750,7 +746,7 @@ class ResolvedFunctionVariantWithOriginal static void objectCallbackBody(zval *captures, uint32_t argc, zval *argv, zval *return_value) { if (UNEXPECTED(argc < 2)) { - zend_throw_error(zend_ce_argument_count_error, "Too few arguments to function " PT_RFV_CLOSURE("resolveResolvableTemplateTypes", "292") "(), %u passed and exactly 2 expected", argc); + zend_throw_error(zend_ce_argument_count_error, "Too few arguments to function %s(), %u passed and exactly 2 expected", PT_RFV_CLOSURE("resolveResolvableTemplateTypes", "292"), argc); return; } zend_object *self = Z_OBJ(captures[0]); @@ -811,7 +807,7 @@ class ResolvedFunctionVariantWithOriginal static void narrowCallbackBody(zval *captures, uint32_t argc, zval *argv, zval *return_value) { if (UNEXPECTED(argc < 2)) { - zend_throw_error(zend_ce_argument_count_error, "Too few arguments to function " PT_RFV_CLOSURE("narrowTemplateTypesInConditionalTypesForParameter", "427") "(), %u passed and exactly 2 expected", argc); + zend_throw_error(zend_ce_argument_count_error, "Too few arguments to function %s(), %u passed and exactly 2 expected", PT_RFV_CLOSURE("narrowTemplateTypesInConditionalTypesForParameter", "427"), argc); return; } zend_object *self = Z_OBJ(captures[0]); @@ -868,7 +864,7 @@ class ResolvedFunctionVariantWithOriginal static void referencesCallbackBody(zval *references, zval *templateType, uint32_t argc, zval *argv, zval *return_value) { if (UNEXPECTED(argc < 2)) { - zend_throw_error(zend_ce_argument_count_error, "Too few arguments to function " PT_RFV_CLOSURE("referencesTemplateType", "490") "(), %u passed and exactly 2 expected", argc); + zend_throw_error(zend_ce_argument_count_error, "Too few arguments to function %s(), %u passed and exactly 2 expected", PT_RFV_CLOSURE("referencesTemplateType", "490"), argc); return; } zval *type = &argv[0]; diff --git a/turbo-ext/src/ScopeContext.cpp b/turbo-ext/src/ScopeContext.cpp index b60df5b5019..b12db5fc177 100644 --- a/turbo-ext/src/ScopeContext.cpp +++ b/turbo-ext/src/ScopeContext.cpp @@ -325,7 +325,7 @@ bool inheritsScopeGetters(zend_class_entry *ce, zend_class_entry *mutatingScope) { static const struct { const char *name; size_t len; } methods[] = { { "isinclass", sizeof("isinclass") - 1 }, { "getclassreflection", sizeof("getclassreflection") - 1 } }; for (const auto &method : methods) { - zend_function *fn = (zend_function *) zend_hash_str_find_ptr(&ce->function_table, method.name, method.len); + zend_function *fn = (zend_function *) zend_hash_str_find_ptr(&PT_CE(ce, function_table), method.name, method.len); if (fn == NULL || fn->common.scope != mutatingScope) return false; } return true; diff --git a/turbo-ext/src/ScopeOps.cpp b/turbo-ext/src/ScopeOps.cpp index 1006768628d..090c1786847 100644 --- a/turbo-ext/src/ScopeOps.cpp +++ b/turbo-ext/src/ScopeOps.cpp @@ -69,11 +69,11 @@ static void pt_scope_offsets_free(zval *zv) static void pt_scope_offsets_collect_memos(zend_class_entry *ce, pt_scope_offsets *off) { off->memo_count = 0; - off->memo_offsets = ce->default_properties_count > 0 - ? (uint32_t *) safe_emalloc((size_t) ce->default_properties_count, sizeof(uint32_t), 0) + off->memo_offsets = PT_CE(ce, default_properties_count) > 0 + ? (uint32_t *) safe_emalloc((size_t) PT_CE(ce, default_properties_count), sizeof(uint32_t), 0) : NULL; for (zend_class_entry *declaring = ce; declaring != NULL; declaring = declaring->parent) { - for (auto entry : zv::TableRef(&declaring->properties_info)) { + for (auto entry : zv::TableRef(&PT_CE(declaring, properties_info))) { zend_property_info *info = (zend_property_info *) Z_PTR_P(entry.value().raw()); if (info->ce != declaring || (info->flags & ZEND_ACC_STATIC) != 0 || !ZEND_TYPE_IS_SET(info->type)) continue; if (Z_TYPE(CE_DEFAULT_PROPERTIES_TABLE(ce)[OBJ_PROP_TO_NUM(info->offset)]) == IS_UNDEF) continue; @@ -84,7 +84,7 @@ static void pt_scope_offsets_collect_memos(zend_class_entry *ce, pt_scope_offset break; } } - if (!seen && off->memo_count < (uint32_t) ce->default_properties_count) off->memo_offsets[off->memo_count++] = info->offset; + if (!seen && off->memo_count < (uint32_t) PT_CE(ce, default_properties_count)) off->memo_offsets[off->memo_count++] = info->offset; } } } @@ -1162,7 +1162,7 @@ class ScopeOps /* Scope property slot by name, deref'd; NULL + throw when missing. */ static zval *scopeProp(zval *scope, const char *name, size_t len) { - zend_property_info *info = (zend_property_info *) zend_hash_str_find_ptr(&Z_OBJCE_P(scope)->properties_info, name, len); + zend_property_info *info = (zend_property_info *) zend_hash_str_find_ptr(&PT_CE(Z_OBJCE_P(scope), properties_info), name, len); if (UNEXPECTED(info == NULL || (info->flags & ZEND_ACC_STATIC) != 0)) { zend_throw_error(NULL, "phpstan_turbo: %s property not found", name); return NULL; @@ -1504,7 +1504,7 @@ class ScopeOps } if (instanceof_function(node->ce, nameCe)) { /* toLowerString() in [self, static, parent]? */ - zend_function *toLower = (zend_function *) zend_hash_str_find_ptr(&node->ce->function_table, "tolowerstring", sizeof("tolowerstring") - 1); + zend_function *toLower = (zend_function *) zend_hash_str_find_ptr(&PT_CE(node->ce, function_table), "tolowerstring", sizeof("tolowerstring") - 1); if (UNEXPECTED(toLower == NULL)) { ctx->failed = true; return false; @@ -1527,7 +1527,7 @@ class ScopeOps /* getClassReflection() !== null && getClassReflection()->is(resolveName($node)) */ if (!ctx->class_reflection_fetched) { zend_class_entry *scopeCe = Z_OBJCE_P(ctx->scope); - zend_function *getClassReflection = (zend_function *) zend_hash_str_find_ptr(&scopeCe->function_table, "getclassreflection", sizeof("getclassreflection") - 1); + zend_function *getClassReflection = (zend_function *) zend_hash_str_find_ptr(&PT_CE(scopeCe, function_table), "getclassreflection", sizeof("getclassreflection") - 1); if (UNEXPECTED(getClassReflection == NULL)) { ctx->failed = true; return false; @@ -1541,7 +1541,7 @@ class ScopeOps } if (Z_TYPE(ctx->class_reflection) == IS_OBJECT) { zend_class_entry *scopeCe = Z_OBJCE_P(ctx->scope); - zend_function *resolveName = (zend_function *) zend_hash_str_find_ptr(&scopeCe->function_table, "resolvename", sizeof("resolvename") - 1); + zend_function *resolveName = (zend_function *) zend_hash_str_find_ptr(&PT_CE(scopeCe, function_table), "resolvename", sizeof("resolvename") - 1); if (UNEXPECTED(resolveName == NULL)) { ctx->failed = true; return false; @@ -1556,7 +1556,7 @@ class ScopeOps zv::Val resolved = zv::Val::adopt(resolvedRaw); zend_class_entry *reflectionCe = Z_OBJCE(ctx->class_reflection); - zend_function *isFn = (zend_function *) zend_hash_str_find_ptr(&reflectionCe->function_table, "is", sizeof("is") - 1); + zend_function *isFn = (zend_function *) zend_hash_str_find_ptr(&PT_CE(reflectionCe, function_table), "is", sizeof("is") - 1); if (UNEXPECTED(isFn == NULL)) { ctx->failed = true; return false; diff --git a/turbo-ext/src/Shadow.cpp b/turbo-ext/src/Shadow.cpp index 3e990233dd7..74f673092e3 100644 --- a/turbo-ext/src/Shadow.cpp +++ b/turbo-ext/src/Shadow.cpp @@ -28,6 +28,8 @@ * native classes next to the PHP twins in one process. */ +/* version-specific: builds zend_class_entry / zend_op_array by hand */ +#define PHPSTANTURBO_ABI_IMPL #include "reg.h" #include "zend_inheritance.h" @@ -41,34 +43,13 @@ extern "C" { extern zend_module_entry phpstan_turbo_module_entry; } -static std::vector &pt_shadow_plans() -{ - static std::vector plans; - return plans; -} - static bool pt_shadow_active = false; -void pt_shadow_plan_add(reg::ShadowPlan &&plan) -{ - pt_shadow_plans().push_back(std::move(plan)); -} - bool pt_shadow_is_active() { return pt_shadow_active; } -bool pt_shadow_instanceof(zend_class_entry *ce, zend_class_entry *nativeCe, const char *realName, size_t realNameLen) -{ - if (EXPECTED(nativeCe != NULL && instanceof_function(ce, nativeCe))) return true; - if (nativeCe == NULL || zend_string_equals_cstr(nativeCe->name, realName, realNameLen)) return false; - zend_string *name = zend_string_init(realName, realNameLen, 0); - zend_class_entry *twin = zend_lookup_class_ex(name, NULL, ZEND_FETCH_CLASS_NO_AUTOLOAD); - zend_string_release(name); - return twin != NULL && instanceof_function(ce, twin); -} - static const char *pt_short_name(const char *fqcn) { const char *slash = strrchr(fqcn, '\\'); @@ -97,7 +78,7 @@ static bool pt_shadow_materialize(reg::ShadowPlan &plan, HashTable *twinFiles, z * function table */ static bool pt_shadow_plan_declares(const reg::ShadowPlan &plan, const char *lcname) { - for (const zend_function_entry &entry : plan.entries) { + for (const reg::FunctionEntry &entry : plan.entries) { if (entry.fname != NULL && strcasecmp(entry.fname, lcname) == 0) return true; } return false; @@ -155,7 +136,9 @@ static bool pt_shadow_materialize(reg::ShadowPlan &plan, HashTable *twinFiles, z * module is set the way module startup sets it */ zend_module_entry *previousModule = EG(current_module); EG(current_module) = &phpstan_turbo_module_entry; - zend_result registered = zend_register_functions(ce, plan.entries.data(), &ce->function_table, MODULE_PERSISTENT); + zend_function_entry *functions = pt_abi_function_entries(plan.entries.data(), plan.entries.size()); + zend_result registered = zend_register_functions(ce, functions, &ce->function_table, MODULE_PERSISTENT); + efree(functions); EG(current_module) = previousModule; if (registered != SUCCESS) { zend_throw_error(NULL, "phpstan_turbo: registering the methods of %s failed", declared.c_str()); diff --git a/turbo-ext/src/SpecifiedTypes.cpp b/turbo-ext/src/SpecifiedTypes.cpp index 2c1c096ad11..7855d917bb0 100644 --- a/turbo-ext/src/SpecifiedTypes.cpp +++ b/turbo-ext/src/SpecifiedTypes.cpp @@ -53,7 +53,7 @@ zval *emptySpecifyCallbackSlot() if (CE_STATIC_MEMBERS(ce) == NULL) { zend_class_init_statics(ce); } - zend_property_info *info = (zend_property_info *) zend_hash_str_find_ptr(&ce->properties_info, PT_LC("emptySpecifyCallback")); + zend_property_info *info = (zend_property_info *) zend_hash_str_find_ptr(&PT_CE(ce, properties_info), PT_LC("emptySpecifyCallback")); ZEND_ASSERT(info != NULL && (info->flags & ZEND_ACC_STATIC) != 0); pt_st_callback_slot = CE_STATIC_MEMBERS(ce) + info->offset; pt_st_callback_ce = ce; diff --git a/turbo-ext/src/StaticType.cpp b/turbo-ext/src/StaticType.cpp index fde999cdc92..dfad58674f2 100644 --- a/turbo-ext/src/StaticType.cpp +++ b/turbo-ext/src/StaticType.cpp @@ -855,7 +855,7 @@ static void ZEND_FASTCALL callbackToArgument(INTERNAL_FUNCTION_PARAMETERS) * its type, is left to the engine path */ bool pt_static_type_callbacks_direct_invoke(const zend_function *fn, zend_object *object, uint32_t argc, zval *argv, zval *retval, bool &handled) { - zif_handler handler = fn->internal_function.handler; + zif_handler handler = PT_INTERNAL_HANDLER(fn); bool inHolder = fn->common.scope == pt_ce_static_type_callbacks; zend_string *name = fn->common.function_name; handled = false; @@ -1035,10 +1035,10 @@ PT_MINIT_REGISTRATION(pt_register_static_type) holder.method("toArgument", reg::PublicStatic, 2, { reg::stringArg("name"), reg::obj("type", ptcls::type) }, callbackToArgument, &ptret::type); pt_ce_static_type_callbacks = holder.register_(); pt_ce_static_type_callbacks->ce_flags |= ZEND_ACC_FINAL; - pt_static_type_callbacks_transform = (zend_function *) zend_hash_str_find_ptr(&pt_ce_static_type_callbacks->function_table, PT_LC("transform")); - pt_static_type_callbacks_map = (zend_function *) zend_hash_str_find_ptr(&pt_ce_static_type_callbacks->function_table, PT_LC("map")); - pt_static_type_callbacks_guard = (zend_function *) zend_hash_str_find_ptr(&pt_ce_static_type_callbacks->function_table, PT_LC("guard")); - pt_static_type_callbacks_to_argument = (zend_function *) zend_hash_str_find_ptr(&pt_ce_static_type_callbacks->function_table, PT_LC("toargument")); + pt_static_type_callbacks_transform = (zend_function *) zend_hash_str_find_ptr(&PT_CE(pt_ce_static_type_callbacks, function_table), PT_LC("transform")); + pt_static_type_callbacks_map = (zend_function *) zend_hash_str_find_ptr(&PT_CE(pt_ce_static_type_callbacks, function_table), PT_LC("map")); + pt_static_type_callbacks_guard = (zend_function *) zend_hash_str_find_ptr(&PT_CE(pt_ce_static_type_callbacks, function_table), PT_LC("guard")); + pt_static_type_callbacks_to_argument = (zend_function *) zend_hash_str_find_ptr(&PT_CE(pt_ce_static_type_callbacks, function_table), PT_LC("toargument")); ZEND_ASSERT(pt_static_type_callbacks_transform != NULL && pt_static_type_callbacks_map != NULL && pt_static_type_callbacks_guard != NULL && pt_static_type_callbacks_to_argument != NULL); reg::Class cls("PHPStan\\Type\\StaticType"); diff --git a/turbo-ext/src/StmtHandlerRegistry.cpp b/turbo-ext/src/StmtHandlerRegistry.cpp index c6f9f2d278e..88444c811cd 100644 --- a/turbo-ext/src/StmtHandlerRegistry.cpp +++ b/turbo-ext/src/StmtHandlerRegistry.cpp @@ -35,7 +35,7 @@ zval *handlersByClassSlot() if (CE_STATIC_MEMBERS(ce) == NULL) { zend_class_init_statics(ce); } - zend_property_info *info = (zend_property_info *) zend_hash_str_find_ptr(&ce->properties_info, PT_LC("stmtHandlersByClass")); + zend_property_info *info = (zend_property_info *) zend_hash_str_find_ptr(&PT_CE(ce, properties_info), PT_LC("stmtHandlersByClass")); ZEND_ASSERT(info != NULL && (info->flags & ZEND_ACC_STATIC) != 0); pt_shr_statics_slot = CE_STATIC_MEMBERS(ce) + info->offset; pt_shr_statics_ce = ce; diff --git a/turbo-ext/src/StringType.cpp b/turbo-ext/src/StringType.cpp index 2c9fa9d223e..ec3d8d66e2a 100644 --- a/turbo-ext/src/StringType.cpp +++ b/turbo-ext/src/StringType.cpp @@ -360,7 +360,7 @@ class StringType { zend_class_entry *ce = pt_class(PT_CLASS_REPORT_UNSAFE_ARRAY_STRING_KEY_CASTING_TOGGLE); if (UNEXPECTED(ce == NULL)) return NULL; - zend_class_constant *constant = (zend_class_constant *) zend_hash_str_find_ptr(&ce->constants_table, name, len); + zend_class_constant *constant = (zend_class_constant *) zend_hash_str_find_ptr(&PT_CE(ce, constants_table), name, len); if (UNEXPECTED(constant == NULL)) { zend_throw_error(NULL, "phpstan_turbo: %s::%s not found", ZSTR_VAL(ce->name), name); return NULL; diff --git a/turbo-ext/src/SymbolScan.h b/turbo-ext/src/SymbolScan.h index d434a412151..e0d0accfc88 100644 --- a/turbo-ext/src/SymbolScan.h +++ b/turbo-ext/src/SymbolScan.h @@ -791,10 +791,9 @@ inline size_t CommentStripper::castLength(size_t at) const { static const char *const casts[] = { "int", "integer", "float", "double", "real", "string", "binary", "array", "object", "bool", "boolean", "unset", -#if PHP_VERSION_ID >= 80500 - "void", -#endif + "void", /* a cast since PHP 8.5 — the last entry, left out before */ }; + const size_t castCount = sizeof(casts) / sizeof(casts[0]) - PT_ABI_SINCE(80500, 0, 1); size_t p = at + 1; while (this->at(p) == ' ' || this->at(p) == '\t') { p++; @@ -805,7 +804,8 @@ inline size_t CommentStripper::castLength(size_t at) const } size_t wordLength = p - word; bool known = false; - for (const char *cast : casts) { + for (size_t i = 0; i < castCount; i++) { + const char *cast = casts[i]; if (strlen(cast) == wordLength && equalsIgnoreCase(contents + word, cast, wordLength)) { known = true; break; @@ -1103,11 +1103,8 @@ inline CommentStripper::Kind CommentStripper::lexScripting() index += c1 == '&' || c1 == '=' ? 2 : 1; return OTHER; case '|': -#if PHP_VERSION_ID >= 80500 - index += c1 == '|' || c1 == '=' || c1 == '>' ? 2 : 1; -#else - index += c1 == '|' || c1 == '=' ? 2 : 1; -#endif + /* |> is the pipe operator since PHP 8.5 */ + index += c1 == '|' || c1 == '=' || (c1 == '>' && PT_ABI_SINCE(80500, true, false)) ? 2 : 1; return OTHER; case ':': index += c1 == ':' ? 2 : 1; diff --git a/turbo-ext/src/TemplateTypeMap.cpp b/turbo-ext/src/TemplateTypeMap.cpp index 008bb4b84a0..8ef88f7b4b6 100644 --- a/turbo-ext/src/TemplateTypeMap.cpp +++ b/turbo-ext/src/TemplateTypeMap.cpp @@ -35,7 +35,7 @@ static zval *pt_ttm_empty() if (CE_STATIC_MEMBERS(ce) == NULL) { zend_class_init_statics(ce); } - zend_property_info *info = (zend_property_info *) zend_hash_str_find_ptr(&ce->properties_info, PT_LC("empty")); + zend_property_info *info = (zend_property_info *) zend_hash_str_find_ptr(&PT_CE(ce, properties_info), PT_LC("empty")); ZEND_ASSERT(info != NULL && (info->flags & ZEND_ACC_STATIC) != 0); pt_ttm_empty_slot = CE_STATIC_MEMBERS(ce) + info->offset; pt_ttm_empty_ce = ce; diff --git a/turbo-ext/src/TemplateTypeVariance.cpp b/turbo-ext/src/TemplateTypeVariance.cpp index 15a8b47d37e..3029efe8c59 100644 --- a/turbo-ext/src/TemplateTypeVariance.cpp +++ b/turbo-ext/src/TemplateTypeVariance.cpp @@ -33,7 +33,7 @@ static zval *pt_ttv_registry() if (CE_STATIC_MEMBERS(ce) == NULL) { zend_class_init_statics(ce); } - zend_property_info *info = (zend_property_info *) zend_hash_str_find_ptr(&ce->properties_info, PT_LC("registry")); + zend_property_info *info = (zend_property_info *) zend_hash_str_find_ptr(&PT_CE(ce, properties_info), PT_LC("registry")); ZEND_ASSERT(info != NULL && (info->flags & ZEND_ACC_STATIC) != 0); pt_ttv_registry_slot = CE_STATIC_MEMBERS(ce) + info->offset; pt_ttv_registry_ce = ce; @@ -45,7 +45,7 @@ static zval *pt_ttv_registry() * exception */ static zval *pt_ttv_class_constant(zend_class_entry *ce, const char *name, size_t len) { - zend_class_constant *constant = (zend_class_constant *) zend_hash_str_find_ptr(&ce->constants_table, name, len); + zend_class_constant *constant = (zend_class_constant *) zend_hash_str_find_ptr(&PT_CE(ce, constants_table), name, len); if (UNEXPECTED(constant == NULL)) { zend_throw_error(NULL, "phpstan_turbo: %s::%s not found", ZSTR_VAL(ce->name), name); return NULL; diff --git a/turbo-ext/src/TemplateTypeVarianceMap.cpp b/turbo-ext/src/TemplateTypeVarianceMap.cpp index 1298c8df6a0..129508335a9 100644 --- a/turbo-ext/src/TemplateTypeVarianceMap.cpp +++ b/turbo-ext/src/TemplateTypeVarianceMap.cpp @@ -32,7 +32,7 @@ static zval *pt_ttvm_empty() if (CE_STATIC_MEMBERS(ce) == NULL) { zend_class_init_statics(ce); } - zend_property_info *info = (zend_property_info *) zend_hash_str_find_ptr(&ce->properties_info, PT_LC("empty")); + zend_property_info *info = (zend_property_info *) zend_hash_str_find_ptr(&PT_CE(ce, properties_info), PT_LC("empty")); ZEND_ASSERT(info != NULL && (info->flags & ZEND_ACC_STATIC) != 0); pt_ttvm_empty_slot = CE_STATIC_MEMBERS(ce) + info->offset; pt_ttvm_empty_ce = ce; diff --git a/turbo-ext/src/TerminateHandler.cpp b/turbo-ext/src/TerminateHandler.cpp new file mode 100644 index 00000000000..c13c6f33d8d --- /dev/null +++ b/turbo-ext/src/TerminateHandler.cpp @@ -0,0 +1,25 @@ +/* + * Linked into the shared core and into each thin per-version extension + * alike: both fold libstdc++ in statically on Linux. + */ + +/* The Makefile's Linux build folds libstdc++ in statically, and with it the + * default terminate handler, which demangles the type of the exception that + * escaped — 45 KB of demangler for a path this code reaches only when a + * standard container fails an internal check (it never throws or catches + * itself). This definition keeps the archive's handler, and so the + * demangler, out of the link. Only for the static link: against a shared + * libstdc++ (the phpize build) the handler is the process's, not ours. The + * signature must match 's declaration exactly — GCC rejects a + * redeclaration adding [[noreturn]] or hidden visibility. */ +#ifdef PHPSTANTURBO_STATIC_LIBSTDCXX +#include +#include +namespace __gnu_cxx { +void __verbose_terminate_handler() +{ + fputs("phpstan_turbo: terminate called (a C++ standard library check failed)\n", stderr); + abort(); +} +} // namespace __gnu_cxx +#endif diff --git a/turbo-ext/src/TraitUseHandler.cpp b/turbo-ext/src/TraitUseHandler.cpp index 8ebfd2ab8e7..b1f34409a46 100644 --- a/turbo-ext/src/TraitUseHandler.cpp +++ b/turbo-ext/src/TraitUseHandler.cpp @@ -104,7 +104,7 @@ zv::Val cloneNode(zval *node) zv::Val readProperty(zval *node, zend_string *name) { zval rv; - zval *value = Z_OBJ_P(node)->handlers->read_property(Z_OBJ_P(node), name, BP_VAR_R, NULL, &rv); + zval *value = PT_OBJ_HANDLER(Z_OBJ_P(node), read_property)(Z_OBJ_P(node), name, BP_VAR_R, NULL, &rv); if (UNEXPECTED(EG(exception))) { if (value == &rv) zval_ptr_dtor(&rv); return zv::Val(); diff --git a/turbo-ext/src/TrustedTypes.cpp b/turbo-ext/src/TrustedTypes.cpp index 3355fb6eea3..68bf597f4d5 100644 --- a/turbo-ext/src/TrustedTypes.cpp +++ b/turbo-ext/src/TrustedTypes.cpp @@ -48,6 +48,8 @@ * error. */ +/* version-specific: walks zend_op_array and the property hooks of 8.4 */ +#define PHPSTANTURBO_ABI_IMPL #include "support.h" #pragma GCC diagnostic push diff --git a/turbo-ext/src/TypeCombinator.cpp b/turbo-ext/src/TypeCombinator.cpp index c1b052719ff..07b1b3b6dbd 100644 --- a/turbo-ext/src/TypeCombinator.cpp +++ b/turbo-ext/src/TypeCombinator.cpp @@ -541,7 +541,7 @@ class TypeCombinator return true; } if (UNEXPECTED(ce != cacheEnabledCe)) { - zend_property_info *info = (zend_property_info *) zend_hash_str_find_ptr(&ce->properties_info, PT_LC("cacheEnabled")); + zend_property_info *info = (zend_property_info *) zend_hash_str_find_ptr(&PT_CE(ce, properties_info), PT_LC("cacheEnabled")); if (UNEXPECTED(info == NULL || (info->flags & ZEND_ACC_STATIC) == 0)) { zend_throw_error(NULL, "phpstan_turbo: %s::$cacheEnabled not found", ZSTR_VAL(ce->name)); return false; diff --git a/turbo-ext/src/TypeCombinatorCache.cpp b/turbo-ext/src/TypeCombinatorCache.cpp index 73d39b69e67..e2268bfc7b8 100644 --- a/turbo-ext/src/TypeCombinatorCache.cpp +++ b/turbo-ext/src/TypeCombinatorCache.cpp @@ -78,27 +78,6 @@ namespace sigs = ptdecl::TypeCombinatorCache::sig; #include "zv.h" #include -#if PHP_VERSION_ID >= 80400 -#include -#endif - -/* zend_weakrefs_hash_clean()/_destroy() only exist since PHP 8.5; on 8.4 the - * same unregister-then-destroy is spelled out with the 8.4-available API. */ -#if PHP_VERSION_ID < 80500 -static zend_always_inline void pt_weakrefs_hash_destroy(HashTable *ht) -{ - zend_ulong objKey; - ZEND_HASH_MAP_FOREACH_NUM_KEY(ht, objKey) { - zend_weakrefs_hash_del(ht, zend_weakref_key_to_object(objKey)); - } ZEND_HASH_FOREACH_END(); - zend_hash_destroy(ht); -} -#else -static zend_always_inline void pt_weakrefs_hash_destroy(HashTable *ht) -{ - zend_weakrefs_hash_destroy(ht); -} -#endif namespace phpstanturbo { @@ -196,13 +175,12 @@ struct TypeObjectHeader zend_object std; }; -static zend_object_handlers pt_type_object_handlers; -static bool pt_type_object_handlers_inited = false; +static const zend_object_handlers *pt_type_object_handlers = nullptr; static uint32_t pt_memo_generation = 1; static zend_always_inline TypeObjectHeader *typeObjectHeader(zend_object *obj) { - if (obj->handlers != &pt_type_object_handlers) return NULL; + if (obj->handlers != pt_type_object_handlers) return NULL; return (TypeObjectHeader *) ((char *) obj - offsetof(TypeObjectHeader, std)); } @@ -271,7 +249,7 @@ static CePlan cePlan(zend_class_entry *ce) kind = CE_STRUCTURAL; } - CePlan plan = { kind, (uint32_t) ce->default_properties_count }; + CePlan plan = { kind, (uint32_t) PT_CE(ce, default_properties_count) }; zval packed; ZVAL_LONG(&packed, ((zend_long) plan.slots << 1) | (zend_long) plan.kind); zend_hash_index_add(&pt_ce_kinds, (zend_ulong) (uintptr_t) ce, &packed); @@ -387,7 +365,7 @@ static bool hashZval(zval *value, Hash128 &h, uint32_t depth) * guarantee the object is CE_STRUCTURAL, so the plan does not need consulting. */ static zend_always_inline bool hashZeroSlotObject(zend_object *obj, Hash128 &out) { - if (obj->ce->default_properties_count != 0) return false; + if (PT_CE(obj->ce, default_properties_count) != 0) return false; Hash128 h = { FNV_OFFSET_A, FNV_OFFSET_B }; mixU64(h, (uint64_t) (uintptr_t) obj->ce); out = h; @@ -722,14 +700,7 @@ static void memoTrackResult(zend_object *obj, Hash128 key) static void memoResultsClean() { pt_invalidate_active = false; -#if PHP_VERSION_ID < 80500 - zend_ulong objKey; - ZEND_HASH_MAP_FOREACH_NUM_KEY(&pt_memo_results, objKey) { - zend_weakrefs_hash_del(&pt_memo_results, zend_weakref_key_to_object(objKey)); - } ZEND_HASH_FOREACH_END(); -#else - zend_weakrefs_hash_clean(&pt_memo_results); -#endif + pt_abi_weakrefs_hash_clean(&pt_memo_results); pt_invalidate_active = true; } @@ -745,7 +716,7 @@ static zend_object *typeObjectCreate(zend_class_entry *ce) header->memoGeneration = 0; zend_object_std_init(&header->std, ce); object_properties_init(&header->std, ce); - header->std.handlers = &pt_type_object_handlers; + header->std.handlers = pt_type_object_handlers; return &header->std; } @@ -769,20 +740,18 @@ static void typeObjectFree(zend_object *obj) * exported); that clone has no header and keeps to the weak maps. */ static zend_object *typeObjectClone(zend_object *old) { -#if PHP_VERSION_ID >= 80400 - if (UNEXPECTED(zend_object_is_lazy(old))) { + if (UNEXPECTED(pt_abi_object_is_lazy(old))) { return zend_objects_clone_obj(old); } -#endif TypeObjectHeader *header = (TypeObjectHeader *) zend_object_alloc(sizeof(TypeObjectHeader), old->ce); header->hashComputed = 0; header->memoKeys = NULL; header->memoGeneration = 0; zend_object_std_init(&header->std, old->ce); - header->std.handlers = &pt_type_object_handlers; - if (old->ce->default_properties_count) { + header->std.handlers = pt_type_object_handlers; + if (PT_CE(old->ce, default_properties_count)) { zval *p = header->std.properties_table; - zval *end = p + old->ce->default_properties_count; + zval *end = p + PT_CE(old->ce, default_properties_count); do { ZVAL_UNDEF(p); p++; @@ -795,12 +764,12 @@ static zend_object *typeObjectClone(zend_object *old) static void typeObjectHandlersInit() { - if (pt_type_object_handlers_inited) return; - memcpy(&pt_type_object_handlers, &std_object_handlers, sizeof(zend_object_handlers)); - pt_type_object_handlers.offset = offsetof(TypeObjectHeader, std); - pt_type_object_handlers.free_obj = typeObjectFree; - pt_type_object_handlers.clone_obj = typeObjectClone; - pt_type_object_handlers_inited = true; + if (pt_type_object_handlers != nullptr) return; + pt_abi_handlers overrides = {}; + overrides.offset = offsetof(TypeObjectHeader, std); + overrides.free_obj = typeObjectFree; + overrides.clone_obj = typeObjectClone; + pt_type_object_handlers = pt_abi_object_handlers(overrides); } /* }}} */ @@ -987,14 +956,14 @@ void pt_type_combinator_cache_rshutdown() if (!pt_cache_inited) return; TypeCombinatorCache::clear(); phpstanturbo::pt_invalidate_active = false; - pt_weakrefs_hash_destroy(&pt_memo_results); + pt_abi_weakrefs_hash_destroy(&pt_memo_results); efree(pt_memo_slots); pt_memo_slots = NULL; pt_memo_mask = 0; pt_memo_count = 0; phpstanturbo::pt_memo_tombstones = 0; - pt_weakrefs_hash_destroy(&pt_type_hashes); - pt_weakrefs_hash_destroy(&pt_obj_serials); + pt_abi_weakrefs_hash_destroy(&pt_type_hashes); + pt_abi_weakrefs_hash_destroy(&pt_obj_serials); zend_hash_destroy(&pt_ce_kinds); pt_cache_inited = false; } @@ -1027,14 +996,14 @@ zv::Val pt_type_combinator_cache_remove(zval *fromType, zval *typeToRemove) * of its own is left alone and keeps the weak maps. */ void pt_type_combinator_cache_adopt_class(zend_class_entry *ce, const char *realName) { - if (ce->create_object != NULL) return; + if (PT_CE(ce, create_object) != NULL) return; size_t len = strlen(realName); bool structural = (len > sizeof("PHPStan\\Type\\") - 1 && memcmp(realName, "PHPStan\\Type\\", sizeof("PHPStan\\Type\\") - 1) == 0) || (len > sizeof("PHPStan\\Php\\") - 1 && memcmp(realName, "PHPStan\\Php\\", sizeof("PHPStan\\Php\\") - 1) == 0) || strcmp(realName, "PHPStan\\TrinaryLogic") == 0; if (!structural) return; phpstanturbo::typeObjectHandlersInit(); - ce->create_object = phpstanturbo::typeObjectCreate; + PT_CE(ce, create_object) = phpstanturbo::typeObjectCreate; } void pt_type_combinator_cache_clear() diff --git a/turbo-ext/src/TypeOps.h b/turbo-ext/src/TypeOps.h index 6db27c5b260..d88663b50e9 100644 --- a/turbo-ext/src/TypeOps.h +++ b/turbo-ext/src/TypeOps.h @@ -182,6 +182,10 @@ struct pt_type_op_info }; extern const pt_type_op_info pt_type_op_infos[PT_OP_COUNT]; +PT_CORE_API const pt_type_op_info *pt_core_type_op_infos(); +#ifdef PHPSTANTURBO_ABI_IMPL +#define pt_type_op_infos (pt_core_type_op_infos()) +#endif constexpr uint8_t pt_type_op_kinds(pt_type_op_arg a0 = PT_OPARG_ANY, pt_type_op_arg a1 = PT_OPARG_ANY) { @@ -234,7 +238,7 @@ static zend_always_inline const pt_type_ops *pt_type_ops_of(const zend_class_ent /* Shadow.cpp: records a class's ops once its class entry is linked (fatal * on a full table — cannot happen for the registered class count) */ -void pt_type_ops_attach(zend_class_entry *ce, const pt_type_ops *ops); +PT_CORE_API void pt_type_ops_attach(zend_class_entry *ce, const pt_type_ops *ops); /* the op behind a lowercase method name, PT_OP_COUNT for any other name — * for the by-name call helpers (pt_type_call() and the per-file wrappers diff --git a/turbo-ext/src/TypeSpecifierContext.cpp b/turbo-ext/src/TypeSpecifierContext.cpp index 9e3067d7946..ae84b8af94b 100644 --- a/turbo-ext/src/TypeSpecifierContext.cpp +++ b/turbo-ext/src/TypeSpecifierContext.cpp @@ -35,7 +35,7 @@ static zval *pt_tsc_registry() if (CE_STATIC_MEMBERS(ce) == NULL) { zend_class_init_statics(ce); } - zend_property_info *info = (zend_property_info *) zend_hash_str_find_ptr(&ce->properties_info, PT_LC("registry")); + zend_property_info *info = (zend_property_info *) zend_hash_str_find_ptr(&PT_CE(ce, properties_info), PT_LC("registry")); ZEND_ASSERT(info != NULL && (info->flags & ZEND_ACC_STATIC) != 0); pt_tsc_registry_slot = CE_STATIC_MEMBERS(ce) + info->offset; pt_tsc_registry_ce = ce; diff --git a/turbo-ext/src/TypeTraits.cpp b/turbo-ext/src/TypeTraits.cpp index 4a331146d7e..35b6db5ab3a 100644 --- a/turbo-ext/src/TypeTraits.cpp +++ b/turbo-ext/src/TypeTraits.cpp @@ -66,8 +66,8 @@ bool pt_type_method_is(zend_object *object, const char *lcname, size_t len, zif_handler handler) { - zend_function *fn = (zend_function *) zend_hash_str_find_ptr(&object->ce->function_table, lcname, len); - return fn != NULL && fn->type == ZEND_INTERNAL_FUNCTION && fn->internal_function.handler == handler; + zend_function *fn = (zend_function *) zend_hash_str_find_ptr(&PT_CE(object->ce, function_table), lcname, len); + return fn != NULL && fn->type == ZEND_INTERNAL_FUNCTION && PT_INTERNAL_HANDLER(fn) == handler; } bool pt_type_method_is_resolve(pt_method_is_site &site, zend_object *object, const char *lcname, size_t len, zif_handler handler) @@ -81,8 +81,8 @@ bool pt_type_method_inherited_resolve(pt_method_is_site &site, zend_object *obje { /* not activated yet: nothing to remember */ if (UNEXPECTED(base == NULL)) return false; - zend_function *own = (zend_function *) zend_hash_str_find_ptr(&base->function_table, lcname, len); - bool is = own != NULL && own->type == ZEND_INTERNAL_FUNCTION && pt_type_method_is(object, lcname, len, own->internal_function.handler); + zend_function *own = (zend_function *) zend_hash_str_find_ptr(&PT_CE(base, function_table), lcname, len); + bool is = own != NULL && own->type == ZEND_INTERNAL_FUNCTION && pt_type_method_is(object, lcname, len, PT_INTERNAL_HANDLER(own)); site = { object->ce, lcname, (uintptr_t) base, pt_engine_generation, is }; return is; } @@ -145,7 +145,7 @@ zend_long pt_type_trinary_value(zval *trinary) if (EXPECTED(object->ce == pt_ce_trinary)) return pt_trinary_value(object); /* the PHP twin declared next to the native class in the differential * tests: the same private int $value, found by name */ - zend_property_info *info = (zend_property_info *) zend_hash_str_find_ptr(&object->ce->properties_info, PT_LC("value")); + zend_property_info *info = (zend_property_info *) zend_hash_str_find_ptr(&PT_CE(object->ce, properties_info), PT_LC("value")); if (info != NULL && (info->flags & ZEND_ACC_STATIC) == 0) { zval *slot = OBJ_PROP(object, info->offset); if (Z_TYPE_P(slot) == IS_LONG) return Z_LVAL_P(slot); @@ -170,8 +170,8 @@ zv::Val pt_type_new(int classIdx, uint32_t argc, zval *argv) if (UNEXPECTED(ce == NULL)) return zv::Val(); zval object; if (UNEXPECTED(object_init_ex(&object, ce) != SUCCESS)) return zv::Val(); - if (ce->constructor != NULL) { - zend_call_known_instance_method(ce->constructor, Z_OBJ(object), NULL, argc, argv); + if (PT_CE(ce, constructor) != NULL) { + zend_call_known_instance_method(PT_CE(ce, constructor), Z_OBJ(object), NULL, argc, argv); if (UNEXPECTED(EG(exception))) { zval_ptr_dtor(&object); return zv::Val(); @@ -937,12 +937,7 @@ void pt_type_trait_non_generalizable(reg::Class &cls) object_init_ex(&holder, pt_ce_generalize_callback); zv::ObjRef(&holder).propAtWrite(PT_GC_PROP_PRECISION, zv::Val::copyOf(zv::Ref(precision))); zval closure; -#if PHP_VERSION_ID >= 80600 - /* php-src fbb2e1f23d6: $this is passed as zend_object* from 8.6 on */ - zend_create_closure(&closure, pt_generalize_callback_invoke, pt_ce_generalize_callback, pt_ce_generalize_callback, Z_OBJ(holder)); -#else - zend_create_closure(&closure, pt_generalize_callback_invoke, pt_ce_generalize_callback, pt_ce_generalize_callback, &holder); -#endif + pt_abi_create_closure(&closure, pt_generalize_callback_invoke, pt_ce_generalize_callback, pt_ce_generalize_callback, Z_OBJ(holder)); zval_ptr_dtor(&holder); /* the closure holds its own reference */ zv::Val result = pt_type_op(PT_THIS_OBJ, PT_OP_TRAVERSE, 1, &closure); zval_ptr_dtor(&closure); @@ -960,7 +955,7 @@ void pt_type_trait_non_generalizable(reg::Class &cls) * twin's typed-property read raises */ [[nodiscard]] zval *pt_type_constant_scalar_value(zend_object *object, zend_class_entry *scope) { - zend_property_info *info = (zend_property_info *) zend_hash_str_find_ptr(&scope->properties_info, PT_LC("value")); + zend_property_info *info = (zend_property_info *) zend_hash_str_find_ptr(&PT_CE(scope, properties_info), PT_LC("value")); if (UNEXPECTED(info == NULL || (info->flags & ZEND_ACC_STATIC) != 0)) { zend_throw_error(NULL, "phpstan_turbo: %s declares no $value property for ConstantScalarTypeTrait", ZSTR_VAL(scope->name)); return NULL; @@ -1363,7 +1358,7 @@ PT_MINIT_REGISTRATION(pt_register_type_traits) holder.method("__invoke", reg::Public, 1, { reg::obj("type", ptcls::type) }, invokeGeneralizeCallback); pt_ce_generalize_callback = holder.register_(); pt_ce_generalize_callback->ce_flags |= ZEND_ACC_FINAL; - pt_generalize_callback_invoke = (zend_function *) zend_hash_str_find_ptr(&pt_ce_generalize_callback->function_table, PT_LC("__invoke")); + pt_generalize_callback_invoke = (zend_function *) zend_hash_str_find_ptr(&PT_CE(pt_ce_generalize_callback, function_table), PT_LC("__invoke")); ZEND_ASSERT(pt_generalize_callback_invoke != NULL); pt_register_native_callback(); } @@ -1929,8 +1924,8 @@ zv::Val pt_type_new_ce(zend_class_entry *ce, uint32_t argc, zval *argv) { zval object; if (UNEXPECTED(object_init_ex(&object, ce) != SUCCESS)) return zv::Val(); - if (ce->constructor != NULL) { - zend_call_known_instance_method(ce->constructor, Z_OBJ(object), NULL, argc, argv); + if (PT_CE(ce, constructor) != NULL) { + zend_call_known_instance_method(PT_CE(ce, constructor), Z_OBJ(object), NULL, argc, argv); if (UNEXPECTED(EG(exception))) { zval_ptr_dtor(&object); return zv::Val(); @@ -1942,16 +1937,7 @@ zv::Val pt_type_new_ce(zend_class_entry *ce, uint32_t argc, zval *argv) zv::Val pt_type_closure_over(zend_function *fn, zend_class_entry *ce, zend_object *holder) { zval closure; -#if PHP_VERSION_ID >= 80600 - /* php-src fbb2e1f23d6: $this is passed as zend_object* from 8.6 on */ - zend_create_closure(&closure, fn, ce, ce, holder); -#else - zval holderZv; - if (holder != NULL) { - ZVAL_OBJ(&holderZv, holder); - } - zend_create_closure(&closure, fn, ce, ce, holder != NULL ? &holderZv : NULL); -#endif + pt_abi_create_closure(&closure, fn, ce, ce, holder); return zv::Val::adopt(closure); } @@ -1991,13 +1977,7 @@ zv::Val pt_type_call_callable(zval *callable, uint32_t argc, zval *argv) if (object->ce == zend_ce_closure) { const zend_function *fn = zend_get_closure_method_def(object); if (fn->type == ZEND_INTERNAL_FUNCTION) { -#if PHP_VERSION_ID >= 80600 - /* PHP 8.6 holds the bound $this as a zend_object (NULL when unbound) */ - zend_object *bound = zend_get_closure_this_ptr(target); -#else - zval *thisZv = zend_get_closure_this_ptr(target); - zend_object *bound = thisZv != NULL && Z_TYPE_P(thisZv) == IS_OBJECT ? Z_OBJ_P(thisZv) : NULL; -#endif + zend_object *bound = pt_abi_closure_this(target); zval ret; bool handled; bool ok = pt_direct_invoke(fn, bound, argc, argv, &ret, handled); @@ -2096,7 +2076,7 @@ zend_class_entry *pt_native_callback_ce() bool pt_direct_invoke(const zend_function *fn, zend_object *object, uint32_t argc, zval *argv, zval *retval, bool &handled) { handled = true; - zif_handler handler = fn->internal_function.handler; + zif_handler handler = PT_INTERNAL_HANDLER(fn); zend_class_entry *scope = fn->common.scope; if (handler == pt_native_closure_invoke_handler() || (scope == pt_ce_native_closure && zend_string_equals_literal(fn->common.function_name, "__invoke"))) { if (EXPECTED(object != NULL && object->ce == pt_ce_native_closure)) return pt_native_closure_invoke(object, argc, argv, retval); @@ -2670,7 +2650,7 @@ bool pt_type_unsafe_array_string_key_casting_not_prevented(bool &out) if (UNEXPECTED(level.isUndef())) return false; zend_class_entry *ce = pt_class(PT_CLASS_REPORT_UNSAFE_ARRAY_STRING_KEY_CASTING_TOGGLE); if (UNEXPECTED(ce == NULL)) return false; - zend_class_constant *constant = (zend_class_constant *) zend_hash_str_find_ptr(&ce->constants_table, PT_LC("PREVENT")); + zend_class_constant *constant = (zend_class_constant *) zend_hash_str_find_ptr(&PT_CE(ce, constants_table), PT_LC("PREVENT")); if (UNEXPECTED(constant == NULL)) { zend_throw_error(NULL, "phpstan_turbo: %s::PREVENT not found", ZSTR_VAL(ce->name)); return false; @@ -3394,7 +3374,7 @@ zv::Val pt_carr_native_closure(pt_native_callback fn, zval *state0, zval *state1 { zv::Val holder = pt_type_native_callback(fn, state0, state1); if (UNEXPECTED(holder.isUndef())) return zv::Val(); - zend_function *invoke = (zend_function *) zend_hash_str_find_ptr(&pt_ce_native_callback->function_table, PT_LC("__invoke")); + zend_function *invoke = (zend_function *) zend_hash_str_find_ptr(&PT_CE(pt_ce_native_callback, function_table), PT_LC("__invoke")); ZEND_ASSERT(invoke != NULL); return pt_type_closure_over(invoke, pt_ce_native_callback, Z_OBJ_P(holder.raw())); } @@ -3445,7 +3425,7 @@ class LateResolvable * an Error pending when scope declares none */ zval *resultSlot() const { - zend_property_info *info = (zend_property_info *) zend_hash_str_find_ptr(&scope->properties_info, PT_LC("result")); + zend_property_info *info = (zend_property_info *) zend_hash_str_find_ptr(&PT_CE(scope, properties_info), PT_LC("result")); if (UNEXPECTED(info == NULL || (info->flags & ZEND_ACC_STATIC) != 0)) { zend_throw_error(NULL, "phpstan_turbo: %s declares no $result property for LateResolvableTypeTrait", ZSTR_VAL(scope->name)); return NULL; @@ -3514,7 +3494,7 @@ class LateResolvable zv::Val result = resolved(); if (UNEXPECTED(result.isUndef())) return zv::Val(); zend_object *target = Z_OBJ_P(result.raw()); - zend_function *fn = (zend_function *) zend_hash_find_ptr_lc(&target->ce->function_table, name); + zend_function *fn = (zend_function *) zend_hash_find_ptr_lc(&PT_CE(target->ce, function_table), name); if (UNEXPECTED(fn == NULL)) { zend_throw_error(NULL, "Call to undefined method %s::%s()", ZSTR_VAL(target->ce->name), ZSTR_VAL(name)); return zv::Val(); @@ -3930,7 +3910,7 @@ class TemplateTypeTrait static zval *slotOf(zend_object *object, zend_class_entry *scope, int index) { - return OBJ_PROP_NUM(object, (uint32_t) scope->default_properties_count - PT_TT_PROP_COUNT + (uint32_t) index); + return OBJ_PROP_NUM(object, (uint32_t) PT_CE(scope, default_properties_count) - PT_TT_PROP_COUNT + (uint32_t) index); } /* a slot read as the twin's typed-property read: NULL with an Error @@ -3956,7 +3936,7 @@ class TemplateTypeTrait void init(zval *templateScope, zval *strategy, zval *variance, zend_string *name, zval *bound, zval *defaultType) const { zv::ObjRef object(self); - uint32_t base = (uint32_t) scope->default_properties_count - PT_TT_PROP_COUNT; + uint32_t base = (uint32_t) PT_CE(scope, default_properties_count) - PT_TT_PROP_COUNT; object.propAtWrite(base + PT_TT_PROP_SCOPE, zv::Val::copyOf(zv::Ref(templateScope))); object.propAtWrite(base + PT_TT_PROP_STRATEGY, zv::Val::copyOf(zv::Ref(strategy))); object.propAtWrite(base + PT_TT_PROP_VARIANCE, zv::Val::copyOf(zv::Ref(variance))); diff --git a/turbo-ext/src/UnionType.cpp b/turbo-ext/src/UnionType.cpp index 81ffd3dd3eb..ac355c53dfc 100644 --- a/turbo-ext/src/UnionType.cpp +++ b/turbo-ext/src/UnionType.cpp @@ -137,7 +137,7 @@ static void throwMapped(int classIdx, uint32_t argc, zval *argv) { zend_class_entry *ce = pt_class(classIdx); if (UNEXPECTED(ce == NULL)) return NULL; - zend_class_constant *constant = (zend_class_constant *) zend_hash_str_find_ptr(&ce->constants_table, name, len); + zend_class_constant *constant = (zend_class_constant *) zend_hash_str_find_ptr(&PT_CE(ce, constants_table), name, len); if (UNEXPECTED(constant == NULL)) { zend_throw_error(NULL, "phpstan_turbo: %s::%s not found", ZSTR_VAL(ce->name), name); return NULL; @@ -616,7 +616,7 @@ class UnionType } /* foreach (self::EQUAL_UNION_CLASSES as $baseClass => $classes) */ - zend_class_constant *equalUnionClasses = (zend_class_constant *) zend_hash_str_find_ptr(&pt_ce_union_type->constants_table, PT_LC("EQUAL_UNION_CLASSES")); + zend_class_constant *equalUnionClasses = (zend_class_constant *) zend_hash_str_find_ptr(&PT_CE(pt_ce_union_type, constants_table), PT_LC("EQUAL_UNION_CLASSES")); if (UNEXPECTED(equalUnionClasses == NULL || Z_TYPE(equalUnionClasses->value) != IS_ARRAY)) { zend_throw_error(NULL, "phpstan_turbo: %s::EQUAL_UNION_CLASSES not found", ZSTR_VAL(pt_ce_union_type->name)); return zv::Val(); diff --git a/turbo-ext/src/UnresolvedTemplateArgumentType.cpp b/turbo-ext/src/UnresolvedTemplateArgumentType.cpp index f45eb277e9b..93a99c4da1b 100644 --- a/turbo-ext/src/UnresolvedTemplateArgumentType.cpp +++ b/turbo-ext/src/UnresolvedTemplateArgumentType.cpp @@ -280,7 +280,7 @@ class UnresolvedTemplateArgumentType zv::Val target = getDelegate(); if (UNEXPECTED(target.isUndef())) return zv::Val(); zend_object *object = Z_OBJ_P(target.raw()); - zend_function *fn = (zend_function *) zend_hash_find_ptr_lc(&object->ce->function_table, name); + zend_function *fn = (zend_function *) zend_hash_find_ptr_lc(&PT_CE(object->ce, function_table), name); if (UNEXPECTED(fn == NULL)) { zend_throw_error(NULL, "Call to undefined method %s::%s()", ZSTR_VAL(object->ce->name), ZSTR_VAL(name)); return zv::Val(); diff --git a/turbo-ext/src/VerbosityLevel.cpp b/turbo-ext/src/VerbosityLevel.cpp index cec0ef50ded..84a0ce33e0e 100644 --- a/turbo-ext/src/VerbosityLevel.cpp +++ b/turbo-ext/src/VerbosityLevel.cpp @@ -53,7 +53,7 @@ static zval *pt_vl_static(pt_vl_static_kind kind) zend_class_init_statics(ce); } for (int i = 0; i < PT_VL_STATIC_COUNT; i++) { - zend_property_info *info = (zend_property_info *) zend_hash_str_find_ptr(&ce->properties_info, pt_vl_static_names[i], strlen(pt_vl_static_names[i])); + zend_property_info *info = (zend_property_info *) zend_hash_str_find_ptr(&PT_CE(ce, properties_info), pt_vl_static_names[i], strlen(pt_vl_static_names[i])); ZEND_ASSERT(info != NULL && (info->flags & ZEND_ACC_STATIC) != 0); pt_vl_statics[i] = CE_STATIC_MEMBERS(ce) + info->offset; } diff --git a/turbo-ext/src/abi.h b/turbo-ext/src/abi.h new file mode 100644 index 00000000000..615a8e04325 --- /dev/null +++ b/turbo-ext/src/abi.h @@ -0,0 +1,605 @@ +/* + * Engine globals through pointers instead of fixed offsets. + * + * EG() and CG() compile to a field offset inside zend_executor_globals / + * zend_compiler_globals, and both structures change layout in every PHP + * minor (EG(exception) sits at 0x360 in 8.3, 0x3a8 in 8.4, 0x3c0 in 8.5). + * Redefined here to read through pointers that Abi.cpp fills in once at + * module startup, every use compiles to the same instructions against each + * supported PHP's headers — the same number of loads as before, the GOT + * entry of the globals symbol traded for the pointer. + * + * A field missing from the lists below fails to compile ("no member named + * eg_"): add it to PT_ABI_EG_FIELDS / PT_ABI_CG_FIELDS. + */ + +#ifndef PHPSTANTURBO_ABI_H +#define PHPSTANTURBO_ABI_H + +#include + +#define PT_ABI_EG_FIELDS(X) \ + X(exception) \ + X(uninitialized_zval) \ + X(class_table) \ + X(function_table) \ + X(fake_scope) \ + X(stack_limit) \ + X(stack_base) \ + X(current_module) \ + X(weakrefs) \ + X(vm_stack) \ + X(vm_stack_top) \ + X(vm_stack_end) \ + X(vm_stack_page_size) \ + X(error_reporting) \ + X(exit_status) \ + X(current_fiber_context) \ + X(assertions) \ + X(bailout) + +#define PT_ABI_CG_FIELDS(X) \ + X(skip_shebang) \ + X(short_tags) \ + X(function_table) \ + X(arena) \ + X(map_ptr_base) + +/* the zend_known_strings the extension reads (ZSTR_KNOWN): the enum is + * renumbered whenever a minor adds a string */ +#define PT_ABI_KNOWN_STRINGS(X) \ + X(ZEND_STR_MESSAGE) \ + X(ZEND_STR_NAME) \ + X(ZEND_STR_THIS) + +/* the zend_object_handlers members read through Z_OBJ_HANDLER(): 8.5 + * inserted a handler before `compare`, moving everything after it */ +#define PT_ABI_OBJECT_HANDLERS(X) \ + X(compare) \ + X(clone_obj) \ + X(read_property) \ + X(get_properties) \ + X(get_closure) + +/* The shared core's API for the version-specific library (main.cpp, + * Abi.cpp, Shadow.cpp, TrustedTypes.cpp): everything else in the core stays + * hidden. The dependency only runs that way — the core never references a + * symbol of the version-specific library. */ +#if defined(_WIN32) +#ifdef PHPSTANTURBO_CORE_BUILD +#define PT_CORE_API __declspec(dllexport) +#else +#define PT_CORE_API +#endif +#else +#define PT_CORE_API __attribute__((visibility("default"))) +#endif + +namespace reg { +struct FunctionEntry; +} + +/* what a class's zend_object_handlers override over the standard ones */ +struct pt_abi_handlers +{ + int offset; + zend_object_free_obj_t free_obj; + zend_object_get_gc_t get_gc; + zend_object_clone_obj_t clone_obj; + zend_object_get_closure_t get_closure; + zend_object_compare_t compare; +}; + +/* the zend_class_entry members read through PT_CE(): 8.6 inserted + * ce_flags2 after ce_flags, moving every member after it (the ones before — + * type, name, parent, refcount, ce_flags — stay put and are read directly) */ +#define PT_ABI_CLASS_ENTRY_MEMBERS(X) \ + X(default_properties_count) \ + X(default_properties_table) \ + X(static_members_table__ptr) \ + X(mutable_data__ptr) \ + X(function_table) \ + X(properties_info) \ + X(constants_table) \ + X(constructor) \ + X(create_object) \ + X(interfaces) \ + X(num_interfaces) \ + X(interface_names) \ + X(attributes) \ + X(info) \ + X(__tostring) \ + X(__serialize) \ + X(__unserialize) + +struct pt_abi_globals +{ +#define PT_ABI_EG_FIELD(field) decltype(((zend_executor_globals *) nullptr)->field) *eg_##field; +#define PT_ABI_CG_FIELD(field) decltype(((zend_compiler_globals *) nullptr)->field) *cg_##field; +#define PT_ABI_KNOWN_STRING(id) zend_string *known_##id; +#define PT_ABI_OBJECT_HANDLER(member) size_t handler_offset_##member; +#define PT_ABI_CLASS_ENTRY_MEMBER(member) size_t ce_offset_##member; + PT_ABI_EG_FIELDS(PT_ABI_EG_FIELD) + PT_ABI_CG_FIELDS(PT_ABI_CG_FIELD) + PT_ABI_KNOWN_STRINGS(PT_ABI_KNOWN_STRING) + PT_ABI_OBJECT_HANDLERS(PT_ABI_OBJECT_HANDLER) + PT_ABI_CLASS_ENTRY_MEMBERS(PT_ABI_CLASS_ENTRY_MEMBER) +#undef PT_ABI_EG_FIELD +#undef PT_ABI_CLASS_ENTRY_MEMBER +#undef PT_ABI_CG_FIELD +#undef PT_ABI_KNOWN_STRING +#undef PT_ABI_OBJECT_HANDLER + + /* member offsets that moved between minors */ + size_t offset_internal_function_handler; /* 8.4 inserted frameless_function_infos and doc_comment before it */ + size_t sizeof_arg_info; /* zend_arg_info: 8.6 appended doc_comment */ + + /* constants whose value differs between minors */ + uint32_t php_version_id; + uint32_t object_lazy_flags; /* IS_OBJ_LAZY_* (8.4+), 0 before */ + uint32_t is_reference_ex; /* 8.5 made references collectable */ + uint32_t acc_use_guards; /* (1 << 11) up to 8.4, (1 << 30) from 8.5 */ + + /* engine inline functions whose behaviour differs between minors: the + * running PHP's own, compiled by Abi.cpp */ + zend_long (*dval_to_lval)(double d); /* 8.5 warns out of range */ + + /* engine API that differs between minors, implemented once per minor by + * Abi.cpp — function pointers rather than symbols, so the shared core + * never links against the version-specific library (a Windows DLL + * cannot leave a symbol for its loader to resolve) */ + zend_function_entry *(*function_entries)(const reg::FunctionEntry *entries, size_t count); + zend_class_entry *(*register_internal_class)(const char *name, const reg::FunctionEntry *entries, size_t count); + const zend_object_handlers *(*object_handlers)(const pt_abi_handlers &overrides); + void (*create_closure)(zval *out, zend_function *fn, zend_class_entry *scope, zend_class_entry *calledScope, zend_object *thisObject); + zend_object *(*closure_this)(zval *closure); + void (*weakrefs_hash_clean)(HashTable *ht); + void (*weakrefs_hash_destroy)(HashTable *ht); + void (*call_stack_size_error)(); + void (*try_assign_ref_str)(zval *zv, zend_string *str); + void (*try_assign_ref_true)(zval *zv); + /* zend_wrong_parameter_error() with the neutral ZPP numbering (below) */ + void (*wrong_parameter_error)(int errorCode, uint32_t num, char *name, int expectedType, zval *arg); + /* the slow paths of the bool/double/string parameter parsing: 8.6 + * changed them to return the value instead of writing through dest */ + bool (*parse_arg_bool_slow)(const zval *arg, bool *dest, uint32_t num); + bool (*parse_arg_double_slow)(const zval *arg, double *dest, uint32_t num); + bool (*parse_arg_str_slow)(zval *arg, zend_string **dest, uint32_t num); + /* exported up to 8.5, inline wrappers over renamed functions from 8.6: + * a core referencing the old symbols would not load on 8.6 */ + void (*call_known_function)(zend_function *fn, zend_object *object, zend_class_entry *calledScope, zval *retval, uint32_t paramCount, zval *params, HashTable *namedParams); + int (*stream_free)(php_stream *stream, int closeOptions); + ssize_t (*stream_read)(php_stream *stream, char *buf, size_t count); + /* pt_engine_run_on_fresh_stack(): a fiber context, its switch and a + * zend_try around the body (8.6 moved SETJMP to sigsetjmp) */ + void (*run_on_fresh_stack)(void (*body)(void *), void *data); + /* zend_is_true() where the declaration cannot be renamed (MSVC, below) */ + bool (*is_true)(const zval *op); +}; + +/* Defined by the shared core, filled by the version-specific library, which + * reaches it through pt_core_abi() (abi.h's PHPSTANTURBO_ABI_IMPL block) */ +extern pt_abi_globals pt_abi; +PT_CORE_API pt_abi_globals *pt_core_abi(); +PT_CORE_API const char *pt_core_version(); +#ifdef PHPSTANTURBO_ABI_IMPL +#define pt_abi (*pt_core_abi()) +#endif + +/* fills pt_abi from the running engine; first thing MINIT (and, in ZTS + * builds, RINIT) does — version-specific (Abi.cpp) */ +void pt_abi_init(); + +/* the engine's function table for the builder's entries: an emalloc()ed + * array terminated by the sentinel the engine expects; the engine copies the + * entries and keeps referencing their arg_info */ +inline zend_function_entry *pt_abi_function_entries(const reg::FunctionEntry *entries, size_t count) { return pt_abi.function_entries(entries, count); } +/* zend_register_internal_class() for a class whose methods are the entries */ +inline zend_class_entry *pt_abi_register_internal_class(const char *name, const reg::FunctionEntry *entries, size_t count) { return pt_abi.register_internal_class(name, entries, count); } +/* a persistent copy of the standard handlers with the given overrides (the + * null members keep the standard handler) */ +inline const zend_object_handlers *pt_abi_object_handlers(const pt_abi_handlers &overrides) { return pt_abi.object_handlers(overrides); } +inline void pt_abi_create_closure(zval *out, zend_function *fn, zend_class_entry *scope, zend_class_entry *calledScope, zend_object *thisObject) { pt_abi.create_closure(out, fn, scope, calledScope, thisObject); } +inline zend_object *pt_abi_closure_this(zval *closure) { return pt_abi.closure_this(closure); } +inline void pt_abi_weakrefs_hash_clean(HashTable *ht) { pt_abi.weakrefs_hash_clean(ht); } +inline void pt_abi_weakrefs_hash_destroy(HashTable *ht) { pt_abi.weakrefs_hash_destroy(ht); } +inline void pt_abi_call_stack_size_error() { pt_abi.call_stack_size_error(); } +inline void pt_abi_try_assign_ref_str(zval *zv, zend_string *str) { pt_abi.try_assign_ref_str(zv, str); } +inline void pt_abi_try_assign_ref_true(zval *zv) { pt_abi.try_assign_ref_true(zv); } + +/* `then` from the given PHP_VERSION_ID on, `otherwise` before it — the + * runtime form of an #if PHP_VERSION_ID gate */ +#define PT_ABI_SINCE(version, then, otherwise) (pt_abi.php_version_id >= (version) ? (then) : (otherwise)) + +/* ce->member for the members in PT_ABI_CLASS_ENTRY_MEMBERS — an lvalue; + * refuses anything but a zend_class_entry pointer at compile time */ +template +static zend_always_inline zend_class_entry *pt_abi_class_entry(T *ce) +{ + static_assert(std::is_same::type, zend_class_entry>::value, "PT_CE() takes a zend_class_entry *"); + return (zend_class_entry *) ce; +} +#define PT_CE(ce, member) \ + (*(decltype(((zend_class_entry *) nullptr)->member) *) ((char *) pt_abi_class_entry(ce) + pt_abi.ce_offset_##member)) + +#ifndef PHPSTANTURBO_ABI_IMPL +/* Not ABI, but codegen: before 8.6 the headers enable ZEND_COLD for GCC + * only (clang identifies as GCC 4.2), from 8.6 for clang too. Pinned so + * the shared code is compiled alike against every header set. */ +#if defined(__GNUC__) || defined(__clang__) +#undef ZEND_COLD +#define ZEND_COLD __attribute__((cold)) +#endif + +/* Codegen pins: helpers whose result is the same in every minor but whose + * spelling changed (8.6 turned macros into inline functions and back), so + * the shared code would compile to different instructions per header set. + * One definition for all — 8.6's. */ +static zend_always_inline bool pt_abi_string_equals_cstr_ci(const zend_string *s1, const char *s2, size_t s2_length) +{ + return ZSTR_LEN(s1) == s2_length && !zend_binary_strcasecmp(ZSTR_VAL(s1), ZSTR_LEN(s1), s2, s2_length); +} +static zend_always_inline bool pt_abi_string_is_interned(const zend_string *s) +{ + return GC_FLAGS(s) & IS_STR_INTERNED; +} +#undef zend_string_equals_literal_ci +#define zend_string_equals_literal_ci(str, literal) pt_abi_string_equals_cstr_ci(str, "" literal, sizeof(literal) - 1) +#undef zend_string_starts_with_literal +#define zend_string_starts_with_literal(str, prefix) zend_string_starts_with_cstr(str, "" prefix, sizeof(prefix) - 1) +#undef zend_string_starts_with_literal_ci +#define zend_string_starts_with_literal_ci(str, prefix) zend_string_starts_with_cstr_ci(str, "" prefix, sizeof(prefix) - 1) +#undef ZSTR_IS_INTERNED +#define ZSTR_IS_INTERNED(s) pt_abi_string_is_interned(s) +#undef zend_string_equals_ci +#define zend_string_equals_ci(s1, s2) pt_abi_string_equals_cstr_ci(s1, ZSTR_VAL(s2), ZSTR_LEN(s2)) +/* array_init(): a macro up to 8.5, an inline function from 8.6 */ +static zend_always_inline void pt_abi_array_init_size(zval *arg, uint32_t size) +{ + ZVAL_ARR(arg, zend_new_array(size)); +} +#undef array_init +#define array_init(arg) pt_abi_array_init_size((arg), 0) +#undef array_init_size +#define array_init_size(arg, size) pt_abi_array_init_size((arg), (size)) +/* 8.5 evaluates the argument once */ +#undef Z_TRY_ADDREF_P +#define Z_TRY_ADDREF_P(pz) do { \ + zval *_pz = (pz); \ + if (Z_REFCOUNTED_P(_pz)) { \ + Z_ADDREF_P(_pz); \ + } \ + } while (0) +#undef Z_TRY_DELREF_P +#define Z_TRY_DELREF_P(pz) do { \ + zval *_pz = (pz); \ + if (Z_REFCOUNTED_P(_pz)) { \ + Z_DELREF_P(_pz); \ + } \ + } while (0) + +#undef EG +#undef CG +#define EG(field) (*pt_abi.eg_##field) +#define CG(field) (*pt_abi.cg_##field) + +#undef ZSTR_KNOWN +#define ZSTR_KNOWN(id) (pt_abi.known_##id) + +/* obj->handlers->member */ +#define PT_OBJ_HANDLER(obj, member) \ + (*(decltype(((zend_object_handlers *) nullptr)->member) *) ((const char *) (obj)->handlers + pt_abi.handler_offset_##member)) +#undef Z_OBJ_HANDLER +#define Z_OBJ_HANDLER(zval, member) PT_OBJ_HANDLER(Z_OBJ(zval), member) +#undef ZEND_COMPARE_OBJECTS_FALLBACK +#define ZEND_COMPARE_OBJECTS_FALLBACK(op1, op2) \ + if (Z_TYPE_P(op1) != IS_OBJECT || \ + Z_TYPE_P(op2) != IS_OBJECT || \ + Z_OBJ_HANDLER_P(op1, compare) != Z_OBJ_HANDLER_P(op2, compare)) { \ + return zend_std_compare_objects(op1, op2); \ + } + +/* Argument parsing. The ZEND_PARSE_PARAMETERS / Z_PARAM_* macros expand in + * our code with two numberings that move between minors: 8.6 turned the + * ZPP_ERROR_* codes into a uint8_t enum without ZPP_ERROR_WRONG_COUNT, and + * inserted Z_EXPECTED_CLASS_NAME(_OR_NULL) in the middle of the expected + * types. The shared code carries the neutral numbering below (8.6's lists), + * and the error report goes through pt_abi, which translates it for the + * running engine. */ +enum pt_abi_zpp_error : int { + PT_ABI_ZPP_ERROR_OK, + PT_ABI_ZPP_ERROR_FAILURE, + PT_ABI_ZPP_ERROR_WRONG_CALLBACK, + PT_ABI_ZPP_ERROR_WRONG_CLASS, + PT_ABI_ZPP_ERROR_WRONG_CLASS_OR_NULL, + PT_ABI_ZPP_ERROR_WRONG_CLASS_OR_STRING, + PT_ABI_ZPP_ERROR_WRONG_CLASS_OR_STRING_OR_NULL, + PT_ABI_ZPP_ERROR_WRONG_CLASS_OR_LONG, + PT_ABI_ZPP_ERROR_WRONG_CLASS_OR_LONG_OR_NULL, + PT_ABI_ZPP_ERROR_WRONG_ARG, + PT_ABI_ZPP_ERROR_UNEXPECTED_EXTRA_NAMED, + PT_ABI_ZPP_ERROR_WRONG_CALLBACK_OR_NULL, +}; +enum pt_abi_expected_type : int { + PT_ABI_Z_EXPECTED_LONG, + PT_ABI_Z_EXPECTED_LONG_OR_NULL, + PT_ABI_Z_EXPECTED_BOOL, + PT_ABI_Z_EXPECTED_BOOL_OR_NULL, + PT_ABI_Z_EXPECTED_STRING, + PT_ABI_Z_EXPECTED_STRING_OR_NULL, + PT_ABI_Z_EXPECTED_ARRAY, + PT_ABI_Z_EXPECTED_ARRAY_OR_NULL, + PT_ABI_Z_EXPECTED_ARRAY_OR_LONG, + PT_ABI_Z_EXPECTED_ARRAY_OR_LONG_OR_NULL, + PT_ABI_Z_EXPECTED_ITERABLE, + PT_ABI_Z_EXPECTED_ITERABLE_OR_NULL, + PT_ABI_Z_EXPECTED_FUNC, + PT_ABI_Z_EXPECTED_FUNC_OR_NULL, + PT_ABI_Z_EXPECTED_RESOURCE, + PT_ABI_Z_EXPECTED_RESOURCE_OR_NULL, + PT_ABI_Z_EXPECTED_PATH, + PT_ABI_Z_EXPECTED_PATH_OR_NULL, + PT_ABI_Z_EXPECTED_OBJECT, + PT_ABI_Z_EXPECTED_OBJECT_OR_NULL, + PT_ABI_Z_EXPECTED_DOUBLE, + PT_ABI_Z_EXPECTED_DOUBLE_OR_NULL, + PT_ABI_Z_EXPECTED_NUMBER, + PT_ABI_Z_EXPECTED_NUMBER_OR_NULL, + PT_ABI_Z_EXPECTED_NUMBER_OR_STRING, + PT_ABI_Z_EXPECTED_NUMBER_OR_STRING_OR_NULL, + PT_ABI_Z_EXPECTED_ARRAY_OR_STRING, + PT_ABI_Z_EXPECTED_ARRAY_OR_STRING_OR_NULL, + PT_ABI_Z_EXPECTED_STRING_OR_LONG, + PT_ABI_Z_EXPECTED_STRING_OR_LONG_OR_NULL, + PT_ABI_Z_EXPECTED_CLASS_NAME, + PT_ABI_Z_EXPECTED_CLASS_NAME_OR_NULL, + PT_ABI_Z_EXPECTED_OBJECT_OR_CLASS_NAME, + PT_ABI_Z_EXPECTED_OBJECT_OR_CLASS_NAME_OR_NULL, + PT_ABI_Z_EXPECTED_OBJECT_OR_STRING, + PT_ABI_Z_EXPECTED_OBJECT_OR_STRING_OR_NULL, +}; +#undef ZPP_ERROR_OK +#define ZPP_ERROR_OK PT_ABI_ZPP_ERROR_OK +#undef ZPP_ERROR_FAILURE +#define ZPP_ERROR_FAILURE PT_ABI_ZPP_ERROR_FAILURE +#undef ZPP_ERROR_WRONG_CALLBACK +#define ZPP_ERROR_WRONG_CALLBACK PT_ABI_ZPP_ERROR_WRONG_CALLBACK +#undef ZPP_ERROR_WRONG_CLASS +#define ZPP_ERROR_WRONG_CLASS PT_ABI_ZPP_ERROR_WRONG_CLASS +#undef ZPP_ERROR_WRONG_CLASS_OR_NULL +#define ZPP_ERROR_WRONG_CLASS_OR_NULL PT_ABI_ZPP_ERROR_WRONG_CLASS_OR_NULL +#undef ZPP_ERROR_WRONG_CLASS_OR_STRING +#define ZPP_ERROR_WRONG_CLASS_OR_STRING PT_ABI_ZPP_ERROR_WRONG_CLASS_OR_STRING +#undef ZPP_ERROR_WRONG_CLASS_OR_STRING_OR_NULL +#define ZPP_ERROR_WRONG_CLASS_OR_STRING_OR_NULL PT_ABI_ZPP_ERROR_WRONG_CLASS_OR_STRING_OR_NULL +#undef ZPP_ERROR_WRONG_CLASS_OR_LONG +#define ZPP_ERROR_WRONG_CLASS_OR_LONG PT_ABI_ZPP_ERROR_WRONG_CLASS_OR_LONG +#undef ZPP_ERROR_WRONG_CLASS_OR_LONG_OR_NULL +#define ZPP_ERROR_WRONG_CLASS_OR_LONG_OR_NULL PT_ABI_ZPP_ERROR_WRONG_CLASS_OR_LONG_OR_NULL +#undef ZPP_ERROR_WRONG_ARG +#define ZPP_ERROR_WRONG_ARG PT_ABI_ZPP_ERROR_WRONG_ARG +#undef ZPP_ERROR_UNEXPECTED_EXTRA_NAMED +#define ZPP_ERROR_UNEXPECTED_EXTRA_NAMED PT_ABI_ZPP_ERROR_UNEXPECTED_EXTRA_NAMED +#undef ZPP_ERROR_WRONG_CALLBACK_OR_NULL +#define ZPP_ERROR_WRONG_CALLBACK_OR_NULL PT_ABI_ZPP_ERROR_WRONG_CALLBACK_OR_NULL +#define Z_EXPECTED_LONG PT_ABI_Z_EXPECTED_LONG +#define Z_EXPECTED_LONG_OR_NULL PT_ABI_Z_EXPECTED_LONG_OR_NULL +#define Z_EXPECTED_BOOL PT_ABI_Z_EXPECTED_BOOL +#define Z_EXPECTED_BOOL_OR_NULL PT_ABI_Z_EXPECTED_BOOL_OR_NULL +#define Z_EXPECTED_STRING PT_ABI_Z_EXPECTED_STRING +#define Z_EXPECTED_STRING_OR_NULL PT_ABI_Z_EXPECTED_STRING_OR_NULL +#define Z_EXPECTED_ARRAY PT_ABI_Z_EXPECTED_ARRAY +#define Z_EXPECTED_ARRAY_OR_NULL PT_ABI_Z_EXPECTED_ARRAY_OR_NULL +#define Z_EXPECTED_ARRAY_OR_LONG PT_ABI_Z_EXPECTED_ARRAY_OR_LONG +#define Z_EXPECTED_ARRAY_OR_LONG_OR_NULL PT_ABI_Z_EXPECTED_ARRAY_OR_LONG_OR_NULL +#define Z_EXPECTED_ITERABLE PT_ABI_Z_EXPECTED_ITERABLE +#define Z_EXPECTED_ITERABLE_OR_NULL PT_ABI_Z_EXPECTED_ITERABLE_OR_NULL +#define Z_EXPECTED_FUNC PT_ABI_Z_EXPECTED_FUNC +#define Z_EXPECTED_FUNC_OR_NULL PT_ABI_Z_EXPECTED_FUNC_OR_NULL +#define Z_EXPECTED_RESOURCE PT_ABI_Z_EXPECTED_RESOURCE +#define Z_EXPECTED_RESOURCE_OR_NULL PT_ABI_Z_EXPECTED_RESOURCE_OR_NULL +#define Z_EXPECTED_PATH PT_ABI_Z_EXPECTED_PATH +#define Z_EXPECTED_PATH_OR_NULL PT_ABI_Z_EXPECTED_PATH_OR_NULL +#define Z_EXPECTED_OBJECT PT_ABI_Z_EXPECTED_OBJECT +#define Z_EXPECTED_OBJECT_OR_NULL PT_ABI_Z_EXPECTED_OBJECT_OR_NULL +#define Z_EXPECTED_DOUBLE PT_ABI_Z_EXPECTED_DOUBLE +#define Z_EXPECTED_DOUBLE_OR_NULL PT_ABI_Z_EXPECTED_DOUBLE_OR_NULL +#define Z_EXPECTED_NUMBER PT_ABI_Z_EXPECTED_NUMBER +#define Z_EXPECTED_NUMBER_OR_NULL PT_ABI_Z_EXPECTED_NUMBER_OR_NULL +#define Z_EXPECTED_NUMBER_OR_STRING PT_ABI_Z_EXPECTED_NUMBER_OR_STRING +#define Z_EXPECTED_NUMBER_OR_STRING_OR_NULL PT_ABI_Z_EXPECTED_NUMBER_OR_STRING_OR_NULL +#define Z_EXPECTED_ARRAY_OR_STRING PT_ABI_Z_EXPECTED_ARRAY_OR_STRING +#define Z_EXPECTED_ARRAY_OR_STRING_OR_NULL PT_ABI_Z_EXPECTED_ARRAY_OR_STRING_OR_NULL +#define Z_EXPECTED_STRING_OR_LONG PT_ABI_Z_EXPECTED_STRING_OR_LONG +#define Z_EXPECTED_STRING_OR_LONG_OR_NULL PT_ABI_Z_EXPECTED_STRING_OR_LONG_OR_NULL +#define Z_EXPECTED_CLASS_NAME PT_ABI_Z_EXPECTED_CLASS_NAME +#define Z_EXPECTED_CLASS_NAME_OR_NULL PT_ABI_Z_EXPECTED_CLASS_NAME_OR_NULL +#define Z_EXPECTED_OBJECT_OR_CLASS_NAME PT_ABI_Z_EXPECTED_OBJECT_OR_CLASS_NAME +#define Z_EXPECTED_OBJECT_OR_CLASS_NAME_OR_NULL PT_ABI_Z_EXPECTED_OBJECT_OR_CLASS_NAME_OR_NULL +#define Z_EXPECTED_OBJECT_OR_STRING PT_ABI_Z_EXPECTED_OBJECT_OR_STRING +#define Z_EXPECTED_OBJECT_OR_STRING_OR_NULL PT_ABI_Z_EXPECTED_OBJECT_OR_STRING_OR_NULL + +#undef ZEND_PARSE_PARAMETERS_START_EX +#define ZEND_PARSE_PARAMETERS_START_EX(flags, min_num_args, max_num_args) do { \ + const int _flags = (flags); \ + uint32_t _min_num_args = (min_num_args); \ + uint32_t _max_num_args = (uint32_t) (max_num_args); \ + uint32_t _num_args = EX_NUM_ARGS(); \ + uint32_t _i = 0; \ + zval *_real_arg, *_arg = NULL; \ + int _expected_type = Z_EXPECTED_LONG; \ + char *_error = NULL; \ + bool _dummy = 0; \ + bool _optional = 0; \ + int _error_code = ZPP_ERROR_OK; \ + ((void)_i); \ + ((void)_real_arg); \ + ((void)_arg); \ + ((void)_expected_type); \ + ((void)_error); \ + ((void)_optional); \ + ((void)_dummy); \ + \ + do { \ + if (UNEXPECTED(_num_args < _min_num_args) || \ + UNEXPECTED(_num_args > _max_num_args)) { \ + if (!(_flags & ZEND_PARSE_PARAMS_QUIET)) { \ + zend_wrong_parameters_count_error(_min_num_args, _max_num_args); \ + } \ + _error_code = ZPP_ERROR_FAILURE; \ + break; \ + } \ + _real_arg = ZEND_CALL_ARG(execute_data, 0); + +#undef ZEND_PARSE_PARAMETERS_END_EX +#define ZEND_PARSE_PARAMETERS_END_EX(failure) \ + ZEND_ASSERT(_i == _max_num_args || _max_num_args == (uint32_t) -1); \ + } while (0); \ + if (UNEXPECTED(_error_code != ZPP_ERROR_OK)) { \ + if (!(_flags & ZEND_PARSE_PARAMS_QUIET)) { \ + pt_abi.wrong_parameter_error(_error_code, _i, _error, _expected_type, _arg); \ + } \ + failure; \ + } \ + } while (0) + +/* the fast paths of zend_parse_arg_bool/double/str() (identical in every + * minor), over the slow paths in pt_abi */ +static zend_always_inline bool pt_abi_parse_arg_bool(const zval *arg, bool *dest, bool *is_null, bool check_null, uint32_t arg_num) +{ + if (check_null) { + *is_null = 0; + } + if (EXPECTED(Z_TYPE_P(arg) == IS_TRUE)) { + *dest = 1; + } else if (EXPECTED(Z_TYPE_P(arg) == IS_FALSE)) { + *dest = 0; + } else if (check_null && Z_TYPE_P(arg) == IS_NULL) { + *is_null = 1; + *dest = 0; + } else { + return pt_abi.parse_arg_bool_slow(arg, dest, arg_num); + } + return 1; +} +static zend_always_inline bool pt_abi_parse_arg_double(const zval *arg, double *dest, bool *is_null, bool check_null, uint32_t arg_num) +{ + if (check_null) { + *is_null = 0; + } + if (EXPECTED(Z_TYPE_P(arg) == IS_DOUBLE)) { + *dest = Z_DVAL_P(arg); + } else if (check_null && Z_TYPE_P(arg) == IS_NULL) { + *is_null = 1; + *dest = 0.0; + } else { + return pt_abi.parse_arg_double_slow(arg, dest, arg_num); + } + return 1; +} +static zend_always_inline bool pt_abi_parse_arg_str(zval *arg, zend_string **dest, bool check_null, uint32_t arg_num) +{ + if (EXPECTED(Z_TYPE_P(arg) == IS_STRING)) { + *dest = Z_STR_P(arg); + } else if (check_null && Z_TYPE_P(arg) == IS_NULL) { + *dest = NULL; + } else { + return pt_abi.parse_arg_str_slow(arg, dest, arg_num); + } + return 1; +} +/* zend_is_callable() is an inline wrapper over zend_is_callable_ex() from + * 8.6 on, and no longer exported; _ex is exported by every minor */ +#define zend_is_callable(callable, checkFlags, callableName) zend_is_callable_ex((callable), NULL, (checkFlags), (callableName), NULL, NULL) +#define zend_call_known_function(fn, object, calledScope, retval, paramCount, params, namedParams) \ + pt_abi.call_known_function((fn), (object), (calledScope), (retval), (paramCount), (params), (namedParams)) +#define zend_call_known_instance_method(fn, object, retval, paramCount, params) \ + pt_abi.call_known_function((fn), (object), (object)->ce, (retval), (paramCount), (params), NULL) +/* 8.6 dropped the underscore of _php_stream_free() / _php_stream_read() */ +#undef php_stream_close +#define php_stream_close(stream) pt_abi.stream_free((stream), PHP_STREAM_FREE_CLOSE) +#undef php_stream_read +#define php_stream_read(stream, buf, count) pt_abi.stream_read((stream), (buf), (count)) + +#define zend_parse_arg_bool pt_abi_parse_arg_bool +#define zend_parse_arg_double pt_abi_parse_arg_double +#define zend_parse_arg_str pt_abi_parse_arg_str + +#undef IS_REFERENCE_EX +#define IS_REFERENCE_EX (pt_abi.is_reference_ex) +#undef ZEND_ACC_USE_GUARDS +#define ZEND_ACC_USE_GUARDS (pt_abi.acc_use_guards) + +/* 8.4 made map pointer offsets signed (static map pointers sit below the + * base); every offset 8.3 hands out is positive, so the signed form reads + * both */ +#undef ZEND_MAP_PTR_GET_IMM +#define ZEND_MAP_PTR_GET_IMM(ptr) (*ZEND_MAP_PTR_OFFSET2PTR((intptr_t) ZEND_MAP_PTR(ptr))) + +/* the engine's inline helpers below were compiled against their own + * header's layout — redefined over the neutral accessors above */ +static zend_always_inline void *pt_abi_map_ptr_get(void *ptr) +{ + return ((uintptr_t) ptr & 1) ? *(void **) ((char *) CG(map_ptr_base) + (intptr_t) ptr) : ptr; +} +static zend_always_inline zval *pt_abi_default_properties_table(zend_class_entry *ce) +{ + void *mutableData = PT_CE(ce, mutable_data__ptr); + if ((ce->ce_flags & ZEND_ACC_HAS_AST_PROPERTIES) && mutableData != NULL) { + return ((zend_class_mutable_data *) *(void **) ((char *) CG(map_ptr_base) + (intptr_t) mutableData))->default_properties_table; + } + return PT_CE(ce, default_properties_table); +} +#undef CE_DEFAULT_PROPERTIES_TABLE +#define CE_DEFAULT_PROPERTIES_TABLE(ce) pt_abi_default_properties_table(ce) +#undef CE_STATIC_MEMBERS +#define CE_STATIC_MEMBERS(ce) ((zval *) pt_abi_map_ptr_get(PT_CE(ce, static_members_table__ptr))) + +static zend_always_inline void *pt_abi_object_alloc(size_t obj_size, zend_class_entry *ce) +{ + size_t properties = sizeof(zval) * (PT_CE(ce, default_properties_count) - ((ce->ce_flags & ZEND_ACC_USE_GUARDS) ? 0 : 1)); + void *obj = emalloc(obj_size + properties); + memset(obj, 0, obj_size - sizeof(zend_object)); + return obj; +} +#define zend_object_alloc(obj_size, ce) pt_abi_object_alloc(obj_size, ce) + +#define zend_dval_to_lval(d) (pt_abi.dval_to_lval(d)) + +/* 8.5 destroys the previous value with zval_ptr_safe_dtor() */ +#undef ZEND_TRY_ASSIGN_REF_STR +#define ZEND_TRY_ASSIGN_REF_STR(zv, str) pt_abi_try_assign_ref_str(zv, str) +#undef ZEND_TRY_ASSIGN_REF_TRUE +#define ZEND_TRY_ASSIGN_REF_TRUE(zv) pt_abi_try_assign_ref_true(zv) + +/* arg_info[i] of a user function (zend_arg_info; name and type keep their + * offsets, the stride moved) */ +#define PT_ARG_INFO(argInfo, i) ((zend_arg_info *) ((char *) (argInfo) + (size_t) (i) * pt_abi.sizeof_arg_info)) + +/* fn->internal_function.handler */ +#define PT_INTERNAL_HANDLER(fn) (*(zif_handler *) ((const char *) (fn) + pt_abi.offset_internal_function_handler)) + +/* zend_object_is_lazy(): zend_object.extra_flags is 8.4's name for the + * padding after `handle`, which 8.3 leaves unset — the mask is 0 there */ +static zend_always_inline bool pt_abi_object_is_lazy(const zend_object *obj) +{ + uint32_t extraFlags; + memcpy(&extraFlags, (const char *) obj + offsetof(zend_object, handle) + sizeof(uint32_t), sizeof(extraFlags)); + return (extraFlags & pt_abi.object_lazy_flags) != 0; +} + +/* zend_is_true() returned int up to 8.3 and bool since; declared bool — + * the int it returned was always 0 or 1, so reading the low byte is exact */ +#if defined(__GNUC__) +#define PT_ABI_SYMBOL_STR2(prefix, name) #prefix #name +#define PT_ABI_SYMBOL_STR(prefix, name) PT_ABI_SYMBOL_STR2(prefix, name) +extern "C" ZEND_API bool ZEND_FASTCALL pt_abi_zend_is_true(const zval *op) __asm__(PT_ABI_SYMBOL_STR(__USER_LABEL_PREFIX__, zend_is_true)); +#define zend_is_true(op) pt_abi_zend_is_true(op) +#else +/* MSVC has no assembler names for declarations: through Abi.cpp, which + * calls the running engine's own */ +#define zend_is_true(op) pt_abi.is_true(op) +#endif +#endif + +#endif diff --git a/turbo-ext/src/main.cpp b/turbo-ext/src/main.cpp index 79f0aa1f17b..b12070c7271 100644 --- a/turbo-ext/src/main.cpp +++ b/turbo-ext/src/main.cpp @@ -8,9 +8,12 @@ * the boundary-economics rules in turbo-ext/README.md). */ +/* version-specific: the module entry carries the engine's API number */ +#define PHPSTANTURBO_ABI_IMPL #include "support.h" #include "reg.h" #include "Engine.h" +#include "version.h" #ifdef PHP_WIN32 #include @@ -18,41 +21,6 @@ #include #endif -/* The Makefile's Linux build folds libstdc++ in statically, and with it the - * default terminate handler, which demangles the type of the exception that - * escaped — 45 KB of demangler for a path this code reaches only when a - * standard container fails an internal check (it never throws or catches - * itself). This definition keeps the archive's handler, and so the - * demangler, out of the link. Only for the static link: against a shared - * libstdc++ (the phpize build) the handler is the process's, not ours. The - * signature must match 's declaration exactly — GCC rejects a - * redeclaration adding [[noreturn]] or hidden visibility. */ -#ifdef PHPSTANTURBO_STATIC_LIBSTDCXX -#include -#include -namespace __gnu_cxx { -void __verbose_terminate_handler() -{ - fputs("phpstan_turbo: terminate called (a C++ standard library check failed)\n", stderr); - abort(); -} -} // namespace __gnu_cxx -#endif - -/* The short SHA of the last commit touching the watched set: baked from git - * by the Makefile (quoted string passed directly), or from the VERSION.txt - * the subsplit workflow commits into phpstan/turbo-ext; "dev" with neither, - * which the enabler rejects. config.w32 and config.m4 pass the bare token as - * PHPSTANTURBO_VERSION_RAW — quote characters do not survive the Windows - * configure-to-nmake pipeline — and it is stringized here. */ -#ifdef PHPSTANTURBO_VERSION_RAW -#define PT_VERSION_STR2(x) #x -#define PT_VERSION_STR(x) PT_VERSION_STR2(x) -#define PHPSTANTURBO_VERSION PT_VERSION_STR(PHPSTANTURBO_VERSION_RAW) -#endif -#ifndef PHPSTANTURBO_VERSION -#define PHPSTANTURBO_VERSION "dev" -#endif /* PHPStanTurbo\Runtime::configure() — cold-path configuration entry point. * TurboExtensionEnabler passes the generated class map (derived from the @@ -159,34 +127,64 @@ static void ZEND_FASTCALL runtimeExitImmediately(INTERNAL_FUNCTION_PARAMETERS) _exit(EG(exit_status)); } -/* the PT_MINIT_REGISTRATION() functions of every file, in name order; a - * constant-initialized pointer, so it is null before any of the file-static - * registrations construct */ -static pt_minit_registration *pt_minit_registrations = nullptr; +/* Whether the shared core loaded next to this library carries the same + * version: a mismatched pair (a torn update of the distributed files) must + * never run together — the extension then registers nothing and reports + * the mismatch as its version, which TurboExtensionEnabler rejects, so + * PHPStan runs without it. */ +static bool pt_core_matches = false; -pt_minit_registration::pt_minit_registration(const char *name, void (*run)()) noexcept - : name(name), run(run), next(nullptr) -{ - pt_minit_registration **slot = &pt_minit_registrations; - while (*slot != nullptr && strcmp((*slot)->name, name) < 0) { - slot = &(*slot)->next; - } - next = *slot; - *slot = this; +extern "C" { +extern zend_module_entry phpstan_turbo_module_entry; } -void pt_minit_registrations_run() +#ifdef PHP_WIN32 +/* Windows searches the dependencies of a DLL loaded by full path in the + * application's directory, not the DLL's own, and PHP loads extensions with + * plain LoadLibrary(): the core is delay-loaded (config.w32) and loaded here + * by its full path, next to this DLL, before PHP calls anything that needs + * it — the delay-load helper then finds it already loaded by name. */ +#ifdef ZTS +#define PT_CORE_DLL L"phpstan_turbo_core-zts.dll" +#else +#define PT_CORE_DLL L"phpstan_turbo_core.dll" +#endif +static bool pt_load_core() { - for (const pt_minit_registration *registration = pt_minit_registrations; registration != nullptr; registration = registration->next) { - registration->run(); - } + HMODULE self = NULL; + if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, (LPCWSTR) &pt_load_core, &self)) return false; + wchar_t path[4096]; + DWORD length = GetModuleFileNameW(self, path, (DWORD) (sizeof(path) / sizeof(path[0]))); + if (length == 0 || length >= sizeof(path) / sizeof(path[0])) return false; + wchar_t *slash = wcsrchr(path, L'\\'); + size_t directory = slash != NULL ? (size_t) (slash - path) + 1 : 0; + size_t name = wcslen(PT_CORE_DLL); + if (directory + name + 1 > sizeof(path) / sizeof(path[0])) return false; + wmemcpy(path + directory, PT_CORE_DLL, name + 1); + return LoadLibraryExW(path, NULL, LOAD_WITH_ALTERED_SEARCH_PATH) != NULL; } +static bool pt_core_loaded = false; +#endif static PHP_MINIT_FUNCTION(phpstan_turbo) { #ifdef ZTS ZEND_TSRMLS_CACHE_UPDATE(); #endif +#ifdef PHPSTANTURBO_SPLIT +#ifdef PHP_WIN32 + if (!pt_core_loaded) { + phpstan_turbo_module_entry.version = "core-missing"; + return SUCCESS; + } +#endif + if (strcmp(pt_core_version(), PHPSTANTURBO_VERSION) != 0) { + phpstan_turbo_module_entry.version = "core-mismatch"; + return SUCCESS; + } +#endif + pt_core_matches = true; + pt_abi_init(); static const reg::Arg returnsBool = reg::boolArg(""); reg::Class runtime("PHPStanTurbo\\Runtime"); @@ -206,46 +204,29 @@ static PHP_MINIT_FUNCTION(phpstan_turbo) static PHP_MSHUTDOWN_FUNCTION(phpstan_turbo) { - pt_arena_mshutdown(); + if (!pt_core_matches) return SUCCESS; + pt_core_mshutdown(); return SUCCESS; } static PHP_RINIT_FUNCTION(phpstan_turbo) { + if (!pt_core_matches) return SUCCESS; #ifdef ZTS ZEND_TSRMLS_CACHE_UPDATE(); + pt_abi_init(); #endif - pt_support_rinit(); - pt_node_traverser_rinit(); - pt_scope_ops_rinit(); - pt_type_combinator_cache_rinit(); - pt_is_super_type_of_result_rinit(); - pt_accepts_result_rinit(); - pt_integer_range_type_rinit(); - pt_object_type_rinit(); - pt_static_type_factory_rinit(); - pt_scope_access_rinit(); - pt_reflection_access_rinit(); - pt_mutating_scope_rinit(); - pt_variable_flow_rinit(); - pt_engine_rinit(); + pt_core_rinit(); return SUCCESS; } static PHP_RSHUTDOWN_FUNCTION(phpstan_turbo) { - pt_scope_ops_rshutdown(); - pt_node_traverser_rshutdown(); - pt_type_combinator_cache_rshutdown(); - pt_is_super_type_of_result_rshutdown(); - pt_accepts_result_rshutdown(); - pt_support_rshutdown(); - pt_object_type_rshutdown(); - pt_static_type_factory_rshutdown(); - pt_engine_rshutdown(); + if (!pt_core_matches) return SUCCESS; + pt_core_rshutdown(); return SUCCESS; } @@ -275,6 +256,14 @@ zend_module_entry phpstan_turbo_module_entry = { STANDARD_MODULE_PROPERTIES, }; +#if defined(PHP_WIN32) && defined(PHPSTANTURBO_SPLIT) +ZEND_DLEXPORT zend_module_entry *get_module(void) +{ + pt_core_loaded = pt_core_loaded || pt_load_core(); + return &phpstan_turbo_module_entry; +} +#else ZEND_GET_MODULE(phpstan_turbo) +#endif } diff --git a/turbo-ext/src/parser/ParserRunner.cpp b/turbo-ext/src/parser/ParserRunner.cpp index a34f49efefb..fb03d0a004f 100644 --- a/turbo-ext/src/parser/ParserRunner.cpp +++ b/turbo-ext/src/parser/ParserRunner.cpp @@ -295,13 +295,13 @@ NodeClassInfo *ParserEngine::resolveNodeClass(const char *alias, bool useCtor) * names — lazily, so a useCtor=true resolve (isInstanceOf) seeing the * class first cannot deny later slot-write callers the plan */ cls->planState = NodeClassInfo::PLAN_FAILED; - zend_function *ctor = cls->ce->constructor; + zend_function *ctor = PT_CE(cls->ce, constructor); if (ctor != NULL && ctor->type == ZEND_USER_FUNCTION && cls->attrsSlot != UINT32_MAX) { uint32_t numArgs = ctor->op_array.num_args; int nprops = 0; bool ok = true; for (uint32_t i = 0; i < numArgs; i++) { - zend_string *argName = ctor->op_array.arg_info[i].name; + zend_string *argName = PT_ARG_INFO(ctor->op_array.arg_info, i)->name; if (zend_string_equals_literal(argName, "attributes")) continue; if (nprops >= 16) { ok = false; @@ -365,7 +365,7 @@ zv::Val ParserEngine::createNode(const char *alias, bool useCtor, zv::Val attrib } } ZVAL_COPY_VALUE(&args[argc++], &attrs); - zend_call_known_function(cls->ce->constructor, Z_OBJ(node), cls->ce, NULL, argc, args, NULL); + zend_call_known_function(PT_CE(cls->ce, constructor), Z_OBJ(node), cls->ce, NULL, argc, args, NULL); zval_ptr_dtor(&attrs); if (EG(exception) != NULL) { abortForPendingException(); @@ -426,7 +426,7 @@ static zval makeErrorObject(zend_string *msg, zval *attrsBorrowed) zval error; object_init_ex(&error, g_errorCe); zv::Args args{msg, attrsBorrowed}; - zend_call_known_function(g_errorCe->constructor, Z_OBJ(error), g_errorCe, NULL, 2, args, NULL); + zend_call_known_function(PT_CE(g_errorCe, constructor), Z_OBJ(error), g_errorCe, NULL, 2, args, NULL); return error; } @@ -973,7 +973,7 @@ zv::Val ParserEngine::makeComment(const Token *tok, int tokenPos) ZVAL_LONG(&args[4], tok->line + countNewlines(tok->text)); ZVAL_LONG(&args[5], tok->pos + (int) ZSTR_LEN(tok->text) - 1); ZVAL_LONG(&args[6], tokenPos); - zend_call_known_function(cls->ce->constructor, Z_OBJ(comment), cls->ce, NULL, 7, args, NULL); + zend_call_known_function(PT_CE(cls->ce, constructor), Z_OBJ(comment), cls->ce, NULL, 7, args, NULL); zval_ptr_dtor(&args[0]); return zv::Val::adopt(comment); } diff --git a/turbo-ext/src/parser/ParserRunnerHelpers.cpp b/turbo-ext/src/parser/ParserRunnerHelpers.cpp index 34d28af6cf1..fb24bccfbd8 100644 --- a/turbo-ext/src/parser/ParserRunnerHelpers.cpp +++ b/turbo-ext/src/parser/ParserRunnerHelpers.cpp @@ -437,7 +437,7 @@ void ParserEngine::parseEscapeSequencesInPart(zv::Ref partNode, const char *quot NodeClassInfo *cls = resolveNodeClass("Scalar\\String_", true); if (cls == NULL || cls->ce == NULL) return; zend_function *fn = (zend_function *) zend_hash_str_find_ptr( - &cls->ce->function_table, "parseescapesequences", sizeof("parseescapesequences") - 1); + &PT_CE(cls->ce, function_table), "parseescapesequences", sizeof("parseescapesequences") - 1); if (fn == NULL) return; zv::Ref value = prop(partNode, "value"); if (value.raw() == NULL) return; diff --git a/turbo-ext/src/reg.h b/turbo-ext/src/reg.h index 537863b9ed1..5dce507f7bc 100644 --- a/turbo-ext/src/reg.h +++ b/turbo-ext/src/reg.h @@ -458,12 +458,27 @@ struct Constant void (*buildValue)(zval *out) = nullptr; /* a non-int constant (an array): fills *out with a persistent, immutable value the engine references for the process lifetime */ }; +/* + * A method as the builder records it. zend_function_entry is not used here + * because its layout differs between PHP minors (8.4 appended + * frameless_function_infos and doc_comment, growing it from 32 to 48 + * bytes); Abi.cpp and Shadow.cpp — the version-specific translation units — + * turn these into the engine's table, sentinel included. + */ +struct FunctionEntry +{ + const char *fname; + zif_handler handler; + zend_internal_arg_info *argInfo; + uint32_t numArgs; + uint32_t flags; +}; + /* * Everything a shadowing class needs to be declared at activation time: the * PHP twin's real name (the class is registered under it, or under * "" in the differential tests), its final flag, parent - * and interfaces, and the members. The entries vector ends with the sentinel - * zend_register_functions() expects. `out` receives the linked class entry. + * and interfaces, and the members. `out` receives the linked class entry. */ struct ShadowPlan { @@ -471,7 +486,7 @@ struct ShadowPlan uint32_t flags; const char *parentName; std::vector interfaces; - std::vector entries; + std::vector entries; std::vector properties; std::vector constants; zend_class_entry **out; @@ -637,6 +652,8 @@ inline void declareMembers(zend_class_entry *ce, const std::vector &pr /* Shadow.cpp: the plan registry and Runtime::activateShadowing() */ void pt_shadow_plan_add(reg::ShadowPlan &&plan); +/* every recorded plan, for the activation (Shadow.cpp) */ +PT_CORE_API std::vector &pt_shadow_plans(); namespace reg { @@ -954,7 +971,7 @@ class Class * (case-insensitive, like the engine's function table) */ bool hasMethod(const char *methodName) const { - for (const zend_function_entry &entry : entries) { + for (const FunctionEntry &entry : entries) { if (strcasecmp(entry.fname, methodName) == 0) return true; } return false; @@ -1266,15 +1283,7 @@ class Class * classes with no PHP twin) */ zend_class_entry *register_() { - zend_function_entry sentinel; - memset(&sentinel, 0, sizeof(sentinel)); - entries.push_back(sentinel); - - /* the engine copies fentry contents but references arg_info forever; - * the entries vector lives only through this call, argInfo persists */ - zend_class_entry ce; - INIT_CLASS_ENTRY_EX(ce, name, strlen(name), entries.data()); - zend_class_entry *registered = zend_register_internal_class(&ce); + zend_class_entry *registered = pt_abi_register_internal_class(name, entries.data(), entries.size()); registered->ce_flags |= flags; declareMembers(registered, properties, constants); return registered; @@ -1284,10 +1293,6 @@ class Class * the twin's name by Runtime::activateShadowing(); *out is set then */ void shadow(zend_class_entry **out) { - zend_function_entry sentinel; - memset(&sentinel, 0, sizeof(sentinel)); - entries.push_back(sentinel); - reg::ShadowPlan plan; plan.name = name; plan.flags = flags; @@ -1333,14 +1338,7 @@ class Class private: Class &addEntry(const char *methodName, uint32_t methodFlags, zend_internal_arg_info *argInfo, size_t argc, zif_handler handler) { - zend_function_entry entry; - memset(&entry, 0, sizeof(entry)); - entry.fname = methodName; - entry.handler = handler; - entry.arg_info = argInfo; - entry.num_args = (uint32_t) argc; - entry.flags = methodFlags; - entries.push_back(entry); + entries.push_back({ methodName, handler, argInfo, (uint32_t) argc, methodFlags }); return *this; } @@ -1348,7 +1346,7 @@ class Class uint32_t flags = 0; const char *parentName = nullptr; std::vector interfaces; - std::vector entries; + std::vector entries; std::vector properties; std::vector constants; pt_type_op_fn opFns[PT_OP_COUNT] = {}; diff --git a/turbo-ext/src/support.cpp b/turbo-ext/src/support.cpp index 5c3f8d28f58..f3eaca418cd 100644 --- a/turbo-ext/src/support.cpp +++ b/turbo-ext/src/support.cpp @@ -613,7 +613,7 @@ ZEND_COLD void pt_throw_undefined_method(zend_class_entry *ce, const char *lcnam const char *name = lcname; zend_class_entry *typeCe = pt_class_loaded(PT_CLASS_TYPE); if (typeCe != NULL) { - zend_function *declared = (zend_function *) zend_hash_str_find_ptr(&typeCe->function_table, lcname, len); + zend_function *declared = (zend_function *) zend_hash_str_find_ptr(&PT_CE(typeCe, function_table), lcname, len); if (declared != NULL) name = ZSTR_VAL(declared->common.function_name); } zend_throw_error(NULL, "Call to undefined method %s::%s()", ZSTR_VAL(ce->name), name); @@ -621,7 +621,7 @@ ZEND_COLD void pt_throw_undefined_method(zend_class_entry *ce, const char *lcnam zend_function *pt_find_method(zend_class_entry *ce, const char *lcname, size_t len) { - zend_function *fn = (zend_function *) zend_hash_str_find_ptr(&ce->function_table, lcname, len); + zend_function *fn = (zend_function *) zend_hash_str_find_ptr(&PT_CE(ce, function_table), lcname, len); if (UNEXPECTED(fn == NULL)) { pt_throw_undefined_method(ce, lcname, len); } @@ -677,7 +677,7 @@ void pt_throw_should_not_happen() bool pt_call_scope_bool(zval *scope, const char *lcname, size_t len, uint32_t argc, zval *argv, bool *out) { zend_class_entry *ce = Z_OBJCE_P(scope); - zend_function *fn = (zend_function *) zend_hash_str_find_ptr(&ce->function_table, lcname, len); + zend_function *fn = (zend_function *) zend_hash_str_find_ptr(&PT_CE(ce, function_table), lcname, len); zval ret; if (UNEXPECTED(fn == NULL)) { @@ -706,7 +706,7 @@ static void pt_node_class_info_free(zval *zv) int32_t pt_instance_prop_offset(zend_class_entry *ce, const char *name, size_t len) { - zend_property_info *info = (zend_property_info *) zend_hash_str_find_ptr(&ce->properties_info, name, len); + zend_property_info *info = (zend_property_info *) zend_hash_str_find_ptr(&PT_CE(ce, properties_info), name, len); if (info == NULL || (info->flags & ZEND_ACC_STATIC) != 0) return -1; return (int32_t) info->offset; } @@ -746,7 +746,7 @@ uint32_t pt_node_print_kind(zend_object *node) if (UNEXPECTED(info == NULL)) return 0; if (EXPECTED(info->print_kind != 0)) return info->print_kind; - zend_function *fn = (zend_function *) zend_hash_str_find_ptr(&node->ce->function_table, "gettype", sizeof("gettype") - 1); + zend_function *fn = (zend_function *) zend_hash_str_find_ptr(&PT_CE(node->ce, function_table), "gettype", sizeof("gettype") - 1); if (UNEXPECTED(fn == NULL)) { zend_throw_error(NULL, "phpstan_turbo: %s has no getType()", ZSTR_VAL(node->ce->name)); return 0; @@ -802,7 +802,7 @@ pt_node_class_info *pt_node_class_info_for_object(zend_object *obj) if (info == NULL) return NULL; if (info->subnode_offsets != NULL || info->subnode_count == UINT32_MAX) return info; - fn = (zend_function *) zend_hash_str_find_ptr(&ce->function_table, "getsubnodenames", sizeof("getsubnodenames") - 1); + fn = (zend_function *) zend_hash_str_find_ptr(&PT_CE(ce, function_table), "getsubnodenames", sizeof("getsubnodenames") - 1); if (fn == NULL || (fn->common.fn_flags & ZEND_ACC_ABSTRACT) != 0) { info->subnode_count = UINT32_MAX; return info; @@ -1139,7 +1139,7 @@ uint32_t pt_call_like_classes_next = 0; /* the method's declaring class is one of the class-map classes */ bool pt_call_like_declared_by(zend_class_entry *ce, const char *lcname, size_t len, std::initializer_list classIdxs) { - zend_function *fn = (zend_function *) zend_hash_str_find_ptr(&ce->function_table, lcname, len); + zend_function *fn = (zend_function *) zend_hash_str_find_ptr(&PT_CE(ce, function_table), lcname, len); if (fn == NULL) return false; for (int classIdx : classIdxs) { if (fn->common.scope == pt_class_loaded(classIdx)) return true; @@ -1153,7 +1153,7 @@ int32_t pt_call_like_resolve(zend_class_entry *ce) if (entry.ce == ce && entry.generation == pt_engine_generation) return entry.argsOffset; } int32_t argsOffset = -1; - zend_property_info *info = (zend_property_info *) zend_hash_str_find_ptr(&ce->properties_info, "args", sizeof("args") - 1); + zend_property_info *info = (zend_property_info *) zend_hash_str_find_ptr(&PT_CE(ce, properties_info), "args", sizeof("args") - 1); if (info != NULL && (info->flags & ZEND_ACC_STATIC) == 0 && pt_call_like_declared_by(ce, PT_LC("getrawargs"), { PT_CLASS_FUNC_CALL, PT_CLASS_METHOD_CALL, PT_CLASS_NULLSAFE_METHOD_CALL, PT_CLASS_STATIC_CALL, PT_CLASS_NEW }) && pt_call_like_declared_by(ce, PT_LC("isfirstclasscallable"), { PT_CLASS_CALL_LIKE }) diff --git a/turbo-ext/src/support.h b/turbo-ext/src/support.h index fc06388a608..6980dfcbf72 100644 --- a/turbo-ext/src/support.h +++ b/turbo-ext/src/support.h @@ -38,6 +38,8 @@ extern "C" { #pragma GCC diagnostic pop +#include "abi.h" + #include #ifdef _WIN32 @@ -493,12 +495,12 @@ bool pt_combinations_helper_for_each(zval *arrays, pt_combination_consumer consu /* Called by Runtime::configure() */ -void pt_class_map_configure(zend_string *key, zend_string *value); +PT_CORE_API void pt_class_map_configure(zend_string *key, zend_string *value); /* Fills return_value with key => default FQCN (or null) for every * pt_class_refs entry — Runtime::classRefs(), backing the smoke test's * structural checks against the generated class map. */ -void pt_class_refs_dump(zval *return_value); +PT_CORE_API void pt_class_refs_dump(zval *return_value); /* }}} */ @@ -517,6 +519,10 @@ struct pt_globals_t { }; extern pt_globals_t pt_globals; +PT_CORE_API pt_globals_t *pt_core_globals(); +#ifdef PHPSTANTURBO_ABI_IMPL +#define pt_globals (*pt_core_globals()) +#endif #define PT_G(v) (pt_globals.v) @@ -557,7 +563,12 @@ struct pt_minit_registration { pt_minit_registration(const char *name, void (*run)()) noexcept; }; -void pt_minit_registrations_run(); +PT_CORE_API void pt_minit_registrations_run(); + +/* the module hooks of the shared core, called by main.cpp */ +PT_CORE_API void pt_core_rinit(); +PT_CORE_API void pt_core_rshutdown(); +PT_CORE_API void pt_core_mshutdown(); #define PT_MINIT_REGISTRATION(fn) \ static ZEND_COLD void fn(); \ @@ -641,7 +652,7 @@ void pt_arena_mshutdown(); /* Runtime::enablePharForkGuard() — privatizes the phar archive's fd cursor * in pcntl_fork()ed children via pthread_atfork (see PharForkGuard.cpp); * a no-op on Windows */ -void pt_phar_fork_guard_register(zend_string *path); +PT_CORE_API void pt_phar_fork_guard_register(zend_string *path); /* Runtime::trustTypesUnder() — arms the optimizer pass that drops the * engine's argument and return type checks from scripts under the prefix @@ -918,7 +929,7 @@ void pt_native_visitor_register(const pt_native_visitor *entry); const pt_native_visitor *pt_native_visitor_for(zend_class_entry *ce); /* drops the class-entry index; called per request and after activation * declared the shadowing classes */ -void pt_native_visitor_index_reset(); +PT_CORE_API void pt_native_visitor_index_reset(); /* }}} */ @@ -1179,7 +1190,7 @@ zv::Val pt_type_combinator_cache_union(uint32_t argc, zval *argv); zv::Val pt_type_combinator_cache_intersect(uint32_t argc, zval *argv); zv::Val pt_type_combinator_cache_remove(zval *fromType, zval *typeToRemove); void pt_type_combinator_cache_clear(); -void pt_type_combinator_cache_adopt_class(zend_class_entry *ce, const char *realName); +PT_CORE_API void pt_type_combinator_cache_adopt_class(zend_class_entry *ce, const char *realName); /* merged from the parallel port branch */ /* merged from the parallel port branch */ diff --git a/turbo-ext/src/version.h b/turbo-ext/src/version.h new file mode 100644 index 00000000000..3f70aac85ea --- /dev/null +++ b/turbo-ext/src/version.h @@ -0,0 +1,19 @@ +#ifndef PHPSTANTURBO_VERSION_H +#define PHPSTANTURBO_VERSION_H + +/* The short SHA of the last commit touching the watched set: baked from git + * by the Makefile (quoted string passed directly), or from the VERSION.txt + * the subsplit workflow commits into phpstan/turbo-ext; "dev" with neither, + * which the enabler rejects. config.w32 and config.m4 pass the bare token as + * PHPSTANTURBO_VERSION_RAW — quote characters do not survive the Windows + * configure-to-nmake pipeline — and it is stringized here. */ +#ifdef PHPSTANTURBO_VERSION_RAW +#define PT_VERSION_STR2(x) #x +#define PT_VERSION_STR(x) PT_VERSION_STR2(x) +#define PHPSTANTURBO_VERSION PT_VERSION_STR(PHPSTANTURBO_VERSION_RAW) +#endif +#ifndef PHPSTANTURBO_VERSION +#define PHPSTANTURBO_VERSION "dev" +#endif + +#endif