Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
turbo-ext: shared core loads on PHP 8.3-8.6 on Linux too
The core no longer references anything in the version-specific library:
the pt_abi table, the module-startup registration list and the shadow
plan registry moved into the new Core.cpp, and the functions Abi.cpp
implements per minor became function pointers in pt_abi. The few core
functions the version-specific library calls are PT_CORE_API, everything
else stays hidden.

PHP 8.6 broke the remaining assumptions, now read through pt_abi too:
- zend_class_entry gained ce_flags2 after ce_flags, moving every later
  member (PT_CE(), type-checked to take a zend_class_entry only)
- the ZPP_ERROR_* codes became a uint8_t enum without WRONG_COUNT and two
  Z_EXPECTED_* types were inserted; the parse slow paths now return the
  value (neutral ZPP macros, translated by Abi.cpp)
- zend_is_callable, zend_call_known_function, _php_stream_free and
  _php_stream_read are no longer exported
- zend_arg_info grew a doc_comment (PT_ARG_INFO())
and ZEND_COLD is pinned, since the older headers enable it for GCC only.

bin/shared-core/gate-linux.sh builds in the CI images (GCC, or clang with
GATE_IMAGE/GATE_MAKE_ARGS), link-split-linux.sh links one core from the
8.4 objects plus a 68 KB extension per minor and runs smoke.php with each:
ALL OK on 8.3, 8.4, 8.5 and 8.6. The macOS scripts got a -mac suffix.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017h1GfB7vfJPgwcTHD9Fth6
  • Loading branch information
ondrejmirtes and claude committed Oct 8, 2026
commit ef180454a66fc6df92d50d2034bbd8f26bd2b354
5 changes: 5 additions & 0 deletions turbo-ext/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,11 @@ CXXFLAGS += $(PGO_FLAGS)
SOURCES := $(wildcard src/*.cpp) $(wildcard src/parser/*.cpp)
OBJECTS := $(SOURCES:.cpp=.o)

# the objects alone — what bin/shared-core/gate-*.sh compare between the PHP
# versions' headers
objects: $(OBJECTS)
.PHONY: objects

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
Expand Down
14 changes: 14 additions & 0 deletions turbo-ext/bin/shared-core/compare-data-linux.sh
Original file line number Diff line number Diff line change
@@ -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]+/.L/g'; }
data() { $O -s "$1" | awk '/^Contents of section / { keep = ($4 !~ /^\.text/ && $4 !~ /^\.comment/) } keep'; }
cd "$S/$A"
for o in $(find src -name '*.o' | sort); do
[ "$(data $o | md5)" != "$(data $S/$B/$o | md5)" ] && echo "DATA DIFF $o"
[ "$(norm_relocs $o | md5)" != "$(norm_relocs $S/$B/$o | md5)" ] && echo "RELOC DIFF $o"
done
true
11 changes: 8 additions & 3 deletions turbo-ext/bin/shared-core/compare-functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import hashlib, os, re, subprocess, sys
from concurrent.futures import ThreadPoolExecutor

OBJDUMP = '/opt/homebrew/opt/llvm/bin/llvm-objdump'
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]+ <')

Expand All @@ -29,7 +29,7 @@ def flush():
if cur is None:
continue
s = line.strip()
if not s:
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)
Expand All @@ -39,10 +39,15 @@ def flush():
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)
# GCC's local labels (.LC0, .LANCHOR1, .L42) and clone suffixes
s = re.sub(r'\.L[A-Za-z]*\d+', '.L', 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'):
if not s.startswith(('ARM64_RELOC', 'R_AARCH64', 'R_X86_64')):
n += 1
flush()
return funcs
Expand Down
24 changes: 24 additions & 0 deletions turbo-ext/bin/shared-core/gate-linux.sh
Original file line number Diff line number Diff line change
@@ -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<v>, 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)
38 changes: 38 additions & 0 deletions turbo-ext/bin/shared-core/link-split-linux.sh
Original file line number Diff line number Diff line change
@@ -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"
Loading