From 1f7643eff9666b97e3f15f7fb166b322986b137d Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 8 Oct 2026 08:33:52 +0200 Subject: [PATCH 1/6] turbo-ext: make everything outside four files PHP-version-neutral Spike towards one shared core library per platform with a thin per-PHP-version extension. The engine layout that moves between minors (EG()/CG() fields, zend_function_entry, internal_function.handler, zend_class_entry.info, object handler offsets, ZSTR_KNOWN ids, IS_REFERENCE_EX, ZEND_ACC_USE_GUARDS, lazy-object flags) is read through pt_abi, which Abi.cpp fills in at module startup; engine functions whose signature or behaviour differs (zend_create_closure, the weakref hash helpers, zend_dval_to_lval, ZEND_TRY_ASSIGN_REF_*, the call stack size error) are implemented once per minor in Abi.cpp; the PHP_VERSION_ID gates in shared code became runtime checks. With this, every object file except Abi.o, Shadow.o, TrustedTypes.o and main.o compiles to identical code, data and relocations against the PHP 8.3, 8.4 and 8.5 headers (bin/shared-core/gate.sh, compare-data.sh). A core dylib linked from the 8.4 objects plus three 76 KB thin extensions (bin/shared-core/link-split.sh) loads under all three, passes smoke.php on 8.4 and 8.5 (8.3 fails the same five deep-recursion checks a pristine 2.3.x build fails on Homebrew 8.3), produces byte-identical analysis output, and measured +0.18% (t=1.29, n=10, noise) against 2.3.x. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_017h1GfB7vfJPgwcTHD9Fth6 --- turbo-ext/bin/shared-core/compare-data.sh | 17 ++ .../bin/shared-core/compare-functions.py | 100 ++++++++ turbo-ext/bin/shared-core/gate.sh | 13 + turbo-ext/bin/shared-core/link-split.sh | 30 +++ turbo-ext/src/Abi.cpp | 148 ++++++++++++ turbo-ext/src/ArenaCache.cpp | 2 +- turbo-ext/src/ArgumentsHandler.cpp | 12 +- turbo-ext/src/Assertions.cpp | 8 +- turbo-ext/src/CalledMethodProcessor.cpp | 2 +- turbo-ext/src/ConditionalTypeForParameter.cpp | 17 +- turbo-ext/src/ConstantArrayType.cpp | 2 +- turbo-ext/src/Engine.cpp | 23 +- turbo-ext/src/ExpressionResult.cpp | 4 +- turbo-ext/src/GenericObjectType.cpp | 8 +- turbo-ext/src/IsSuperTypeOfResult.cpp | 7 +- turbo-ext/src/MixedType.cpp | 2 +- turbo-ext/src/ObjectType.cpp | 7 +- .../ResolvedFunctionVariantWithOriginal.cpp | 18 +- turbo-ext/src/Shadow.cpp | 8 +- turbo-ext/src/StaticType.cpp | 2 +- turbo-ext/src/SymbolScan.h | 15 +- turbo-ext/src/TraitUseHandler.cpp | 2 +- turbo-ext/src/TrustedTypes.cpp | 2 + turbo-ext/src/TypeCombinatorCache.cpp | 61 ++--- turbo-ext/src/TypeTraits.cpp | 32 +-- turbo-ext/src/abi.h | 225 ++++++++++++++++++ turbo-ext/src/main.cpp | 2 + turbo-ext/src/reg.h | 48 ++-- turbo-ext/src/support.h | 2 + 29 files changed, 636 insertions(+), 183 deletions(-) create mode 100755 turbo-ext/bin/shared-core/compare-data.sh create mode 100644 turbo-ext/bin/shared-core/compare-functions.py create mode 100755 turbo-ext/bin/shared-core/gate.sh create mode 100755 turbo-ext/bin/shared-core/link-split.sh create mode 100644 turbo-ext/src/Abi.cpp create mode 100644 turbo-ext/src/abi.h diff --git a/turbo-ext/bin/shared-core/compare-data.sh b/turbo-ext/bin/shared-core/compare-data.sh new file mode 100755 index 00000000000..df60424c0d1 --- /dev/null +++ b/turbo-ext/bin/shared-core/compare-data.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..144cc5ed112 --- /dev/null +++ b/turbo-ext/bin/shared-core/compare-functions.py @@ -0,0 +1,100 @@ +#!/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 = '/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), [], 0 + continue + if cur is None: + continue + s = line.strip() + if not s: + 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) + # 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'): + 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.sh b/turbo-ext/bin/shared-core/gate.sh new file mode 100755 index 00000000000..72c1bc5f9fb --- /dev/null +++ b/turbo-ext/bin/shared-core/gate.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.sh b/turbo-ext/bin/shared-core/link-split.sh new file mode 100755 index 00000000000..8439c408d77 --- /dev/null +++ b/turbo-ext/bin/shared-core/link-split.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/src/Abi.cpp b/turbo-ext/src/Abi.cpp new file mode 100644 index 00000000000..5da809f422a --- /dev/null +++ b/turbo-ext/src/Abi.cpp @@ -0,0 +1,148 @@ +/* + * 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 "zend_closures.h" +#include "zend_weakrefs.h" + +pt_abi_globals pt_abi; + +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); + 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) +#undef PT_ABI_EG_FIELD +#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.offset_class_entry_info = offsetof(zend_class_entry, 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); }; +} + +const zend_object_handlers *pt_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; +} + +zend_function_entry *pt_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; +} + +zend_class_entry *pt_abi_register_internal_class(const char *name, const reg::FunctionEntry *entries, size_t count) +{ + zend_function_entry *functions = pt_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; +} + +void pt_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 +} + +zend_object *pt_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 */ +void pt_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 +} + +void pt_abi_weakrefs_hash_destroy(HashTable *ht) +{ +#if PHP_VERSION_ID >= 80500 + zend_weakrefs_hash_destroy(ht); +#else + pt_abi_weakrefs_hash_clean(ht); + zend_hash_destroy(ht); +#endif +} + +void pt_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 +} + +void pt_abi_try_assign_ref_str(zval *zv, zend_string *str) +{ + ZEND_TRY_ASSIGN_REF_STR(zv, str); +} + +void pt_abi_try_assign_ref_true(zval *zv) +{ + ZEND_TRY_ASSIGN_REF_TRUE(zv); +} diff --git a/turbo-ext/src/ArenaCache.cpp b/turbo-ext/src/ArenaCache.cpp index 77d3dcac3d5..c179100470c 100644 --- a/turbo-ext/src/ArenaCache.cpp +++ b/turbo-ext/src/ArenaCache.cpp @@ -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..7a6730fedb2 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) 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/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..537f5c35e17 100644 --- a/turbo-ext/src/ConstantArrayType.cpp +++ b/turbo-ext/src/ConstantArrayType.cpp @@ -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/Engine.cpp b/turbo-ext/src/Engine.cpp index 2974361619c..426f0fa80ba 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 @@ -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); diff --git a/turbo-ext/src/ExpressionResult.cpp b/turbo-ext/src/ExpressionResult.cpp index 142b860daa4..a33330a44d0 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; + if ((ce->ce_flags & ZEND_ACC_ANON_CLASS) == 0 || ce->type != ZEND_USER_CLASS || ce->default_properties_count != 1 || PT_CE_INFO(ce).user.filename == NULL) return false; zend_property_info *info = (zend_property_info *) zend_hash_str_find_ptr(&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_INFO(containerCe).user.filename != NULL && zend_string_equals(PT_CE_INFO(containerCe).user.filename, PT_CE_INFO(ce).user.filename); } inline zval *nullable(zval *value) 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/IsSuperTypeOfResult.cpp b/turbo-ext/src/IsSuperTypeOfResult.cpp index 862fcc99a49..2d4ee60ee12 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)); } diff --git a/turbo-ext/src/MixedType.cpp b/turbo-ext/src/MixedType.cpp index 18cb068f50f..a320abf7979 100644 --- a/turbo-ext/src/MixedType.cpp +++ b/turbo-ext/src/MixedType.cpp @@ -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; diff --git a/turbo-ext/src/ObjectType.cpp b/turbo-ext/src/ObjectType.cpp index 224940abe45..63e64210d56 100644 --- a/turbo-ext/src/ObjectType.cpp +++ b/turbo-ext/src/ObjectType.cpp @@ -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 { diff --git a/turbo-ext/src/ResolvedFunctionVariantWithOriginal.cpp b/turbo-ext/src/ResolvedFunctionVariantWithOriginal.cpp index 920f4d630f7..c302c8039f7 100644 --- a/turbo-ext/src/ResolvedFunctionVariantWithOriginal.cpp +++ b/turbo-ext/src/ResolvedFunctionVariantWithOriginal.cpp @@ -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/Shadow.cpp b/turbo-ext/src/Shadow.cpp index 3e990233dd7..7157587a996 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" @@ -97,7 +99,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 +157,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/StaticType.cpp b/turbo-ext/src/StaticType.cpp index fde999cdc92..334f8268fe2 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; 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/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/TypeCombinatorCache.cpp b/turbo-ext/src/TypeCombinatorCache.cpp index 73d39b69e67..22499ee3441 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)); } @@ -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,17 +740,15 @@ 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; + header->std.handlers = pt_type_object_handlers; if (old->ce->default_properties_count) { zval *p = header->std.properties_table; zval *end = p + old->ce->default_properties_count; @@ -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; } diff --git a/turbo-ext/src/TypeTraits.cpp b/turbo-ext/src/TypeTraits.cpp index 4a331146d7e..14282d5bbbc 100644 --- a/turbo-ext/src/TypeTraits.cpp +++ b/turbo-ext/src/TypeTraits.cpp @@ -67,7 +67,7 @@ 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; + 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) @@ -82,7 +82,7 @@ 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); + 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; } @@ -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); @@ -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); diff --git a/turbo-ext/src/abi.h b/turbo-ext/src/abi.h new file mode 100644 index 00000000000..88dfc6c1060 --- /dev/null +++ b/turbo-ext/src/abi.h @@ -0,0 +1,225 @@ +/* + * 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 + +#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) + +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; + 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) +#undef PT_ABI_EG_FIELD +#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 offset_class_entry_info; /* 8.4 grew zend_class_entry before it */ + + /* 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 */ +}; + +/* 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; +}; + +/* a persistent copy of the standard handlers with the given overrides (the + * null members keep the standard handler) */ +const zend_object_handlers *pt_abi_object_handlers(const pt_abi_handlers &overrides); + +/* engine API that differs between minors, implemented once per minor */ +void pt_abi_create_closure(zval *out, zend_function *fn, zend_class_entry *scope, zend_class_entry *calledScope, zend_object *thisObject); +zend_object *pt_abi_closure_this(zval *closure); +void pt_abi_weakrefs_hash_clean(HashTable *ht); +void pt_abi_weakrefs_hash_destroy(HashTable *ht); +void pt_abi_call_stack_size_error(); +void pt_abi_try_assign_ref_str(zval *zv, zend_string *str); +void pt_abi_try_assign_ref_true(zval *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)) + +extern pt_abi_globals pt_abi; + +/* fills pt_abi from the running engine; first thing MINIT (and, in ZTS + * builds, RINIT) does */ +void pt_abi_init(); + +namespace reg { +struct FunctionEntry; +} + +/* 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 */ +zend_function_entry *pt_abi_function_entries(const reg::FunctionEntry *entries, size_t count); + +/* zend_register_internal_class() for a class whose methods are the entries */ +zend_class_entry *pt_abi_register_internal_class(const char *name, const reg::FunctionEntry *entries, size_t count); + +#ifndef PHPSTANTURBO_ABI_IMPL +#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); \ + } + +#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 zval *pt_abi_default_properties_table(zend_class_entry *ce) +{ + if ((ce->ce_flags & ZEND_ACC_HAS_AST_PROPERTIES) && ZEND_MAP_PTR(ce->mutable_data)) { + zend_class_mutable_data *mutable_data = (zend_class_mutable_data *) ZEND_MAP_PTR_GET_IMM(ce->mutable_data); + return mutable_data->default_properties_table; + } + return ce->default_properties_table; +} +#undef CE_DEFAULT_PROPERTIES_TABLE +#define CE_DEFAULT_PROPERTIES_TABLE(ce) pt_abi_default_properties_table(ce) + +static zend_always_inline void *pt_abi_object_alloc(size_t obj_size, zend_class_entry *ce) +{ + size_t properties = sizeof(zval) * (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) + +/* fn->internal_function.handler */ +#define PT_INTERNAL_HANDLER(fn) (*(zif_handler *) ((const char *) (fn) + pt_abi.offset_internal_function_handler)) +/* ce->info */ +#define PT_CE_INFO(ce) (*(decltype(((zend_class_entry *) nullptr)->info) *) ((const char *) (ce) + pt_abi.offset_class_entry_info)) + +/* 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.4 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) +#endif +#endif + +#endif diff --git a/turbo-ext/src/main.cpp b/turbo-ext/src/main.cpp index 79f0aa1f17b..e8da9a73974 100644 --- a/turbo-ext/src/main.cpp +++ b/turbo-ext/src/main.cpp @@ -187,6 +187,7 @@ static PHP_MINIT_FUNCTION(phpstan_turbo) #ifdef ZTS ZEND_TSRMLS_CACHE_UPDATE(); #endif + pt_abi_init(); static const reg::Arg returnsBool = reg::boolArg(""); reg::Class runtime("PHPStanTurbo\\Runtime"); @@ -215,6 +216,7 @@ static PHP_RINIT_FUNCTION(phpstan_turbo) { #ifdef ZTS ZEND_TSRMLS_CACHE_UPDATE(); + pt_abi_init(); #endif pt_support_rinit(); diff --git a/turbo-ext/src/reg.h b/turbo-ext/src/reg.h index 537863b9ed1..ec8df0f2dd3 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; @@ -954,7 +969,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 +1281,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 +1291,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 +1336,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 +1344,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.h b/turbo-ext/src/support.h index fc06388a608..19b51d324f3 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 From ef180454a66fc6df92d50d2034bbd8f26bd2b354 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 8 Oct 2026 11:05:38 +0200 Subject: [PATCH 2/6] 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) Claude-Session: https://claude.ai/code/session_017h1GfB7vfJPgwcTHD9Fth6 --- turbo-ext/Makefile | 5 + .../bin/shared-core/compare-data-linux.sh | 14 + .../{compare-data.sh => compare-data-mac.sh} | 0 .../bin/shared-core/compare-functions.py | 11 +- turbo-ext/bin/shared-core/gate-linux.sh | 24 ++ .../bin/shared-core/{gate.sh => gate-mac.sh} | 0 turbo-ext/bin/shared-core/link-split-linux.sh | 38 ++ .../{link-split.sh => link-split-mac.sh} | 0 turbo-ext/src/Abi.cpp | 223 ++++++++-- turbo-ext/src/ArenaCache.cpp | 8 +- turbo-ext/src/Assertions.cpp | 2 +- turbo-ext/src/BinaryOpHandler.cpp | 2 +- turbo-ext/src/ClassReflection.cpp | 4 +- turbo-ext/src/ConstantArrayType.cpp | 2 +- turbo-ext/src/ConstantStringType.cpp | 4 +- turbo-ext/src/Core.cpp | 98 +++++ turbo-ext/src/Engine.cpp | 18 +- turbo-ext/src/ExprHandlerRegistry.cpp | 2 +- turbo-ext/src/ExprPrinter.cpp | 2 +- turbo-ext/src/ExpressionResult.cpp | 6 +- turbo-ext/src/ForeachHandler.cpp | 4 +- turbo-ext/src/GetTemplateTypeType.cpp | 2 +- turbo-ext/src/IntersectionType.cpp | 2 +- turbo-ext/src/IsSuperTypeOfResult.cpp | 2 +- .../src/LateResolvableArrayShapeType.cpp | 2 +- turbo-ext/src/MixedType.cpp | 6 +- turbo-ext/src/NameNodeAccess.cpp | 4 +- turbo-ext/src/NodeCallbackScope.cpp | 2 +- turbo-ext/src/NodeScopeResolver.cpp | 2 +- turbo-ext/src/NodeTraverser.cpp | 6 +- turbo-ext/src/ObjectShapeType.cpp | 2 +- turbo-ext/src/ObjectType.cpp | 10 +- turbo-ext/src/PassedByReference.cpp | 2 +- turbo-ext/src/PhpClassReflectionExtension.cpp | 6 +- turbo-ext/src/PhpDocsResolver.cpp | 2 +- turbo-ext/src/PropertyHandler.cpp | 2 +- turbo-ext/src/RecursionGuard.cpp | 2 +- turbo-ext/src/ReflectionAccess.cpp | 2 +- .../ResolvedFunctionVariantWithOriginal.cpp | 2 +- turbo-ext/src/ScopeContext.cpp | 2 +- turbo-ext/src/ScopeOps.cpp | 18 +- turbo-ext/src/Shadow.cpp | 21 - turbo-ext/src/SpecifiedTypes.cpp | 2 +- turbo-ext/src/StaticType.cpp | 8 +- turbo-ext/src/StmtHandlerRegistry.cpp | 2 +- turbo-ext/src/StringType.cpp | 2 +- turbo-ext/src/TemplateTypeMap.cpp | 2 +- turbo-ext/src/TemplateTypeVariance.cpp | 4 +- turbo-ext/src/TemplateTypeVarianceMap.cpp | 2 +- turbo-ext/src/TerminateHandler.cpp | 25 ++ turbo-ext/src/TypeCombinator.cpp | 2 +- turbo-ext/src/TypeCombinatorCache.cpp | 12 +- turbo-ext/src/TypeOps.h | 4 +- turbo-ext/src/TypeSpecifierContext.cpp | 2 +- turbo-ext/src/TypeTraits.cpp | 30 +- turbo-ext/src/UnionType.cpp | 4 +- .../src/UnresolvedTemplateArgumentType.cpp | 2 +- turbo-ext/src/VerbosityLevel.cpp | 2 +- turbo-ext/src/abi.h | 404 ++++++++++++++++-- turbo-ext/src/main.cpp | 71 +-- turbo-ext/src/parser/ParserRunner.cpp | 10 +- turbo-ext/src/parser/ParserRunnerHelpers.cpp | 2 +- turbo-ext/src/reg.h | 2 + turbo-ext/src/support.cpp | 16 +- turbo-ext/src/support.h | 19 +- 65 files changed, 889 insertions(+), 306 deletions(-) create mode 100755 turbo-ext/bin/shared-core/compare-data-linux.sh rename turbo-ext/bin/shared-core/{compare-data.sh => compare-data-mac.sh} (100%) create mode 100755 turbo-ext/bin/shared-core/gate-linux.sh rename turbo-ext/bin/shared-core/{gate.sh => gate-mac.sh} (100%) create mode 100755 turbo-ext/bin/shared-core/link-split-linux.sh rename turbo-ext/bin/shared-core/{link-split.sh => link-split-mac.sh} (100%) create mode 100644 turbo-ext/src/Core.cpp create mode 100644 turbo-ext/src/TerminateHandler.cpp diff --git a/turbo-ext/Makefile b/turbo-ext/Makefile index a84a8e7fa4c..ad64c0f048f 100644 --- a/turbo-ext/Makefile +++ b/turbo-ext/Makefile @@ -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 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..52281b9f0bd --- /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]+/.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 diff --git a/turbo-ext/bin/shared-core/compare-data.sh b/turbo-ext/bin/shared-core/compare-data-mac.sh similarity index 100% rename from turbo-ext/bin/shared-core/compare-data.sh rename to turbo-ext/bin/shared-core/compare-data-mac.sh diff --git a/turbo-ext/bin/shared-core/compare-functions.py b/turbo-ext/bin/shared-core/compare-functions.py index 144cc5ed112..ff3ed01c602 100644 --- a/turbo-ext/bin/shared-core/compare-functions.py +++ b/turbo-ext/bin/shared-core/compare-functions.py @@ -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]+ <') @@ -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) @@ -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 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.sh b/turbo-ext/bin/shared-core/gate-mac.sh similarity index 100% rename from turbo-ext/bin/shared-core/gate.sh rename to turbo-ext/bin/shared-core/gate-mac.sh 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.sh b/turbo-ext/bin/shared-core/link-split-mac.sh similarity index 100% rename from turbo-ext/bin/shared-core/link-split.sh rename to turbo-ext/bin/shared-core/link-split-mac.sh diff --git a/turbo-ext/src/Abi.cpp b/turbo-ext/src/Abi.cpp index 5da809f422a..9969ffc5166 100644 --- a/turbo-ext/src/Abi.cpp +++ b/turbo-ext/src/Abi.cpp @@ -10,38 +10,8 @@ #include "zend_closures.h" #include "zend_weakrefs.h" -pt_abi_globals pt_abi; -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); - 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) -#undef PT_ABI_EG_FIELD -#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.offset_class_entry_info = offsetof(zend_class_entry, 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); }; -} - -const zend_object_handlers *pt_abi_object_handlers(const pt_abi_handlers &overrides) +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)); @@ -54,7 +24,7 @@ const zend_object_handlers *pt_abi_object_handlers(const pt_abi_handlers &overri return handlers; } -zend_function_entry *pt_abi_function_entries(const reg::FunctionEntry *entries, size_t count) +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++) { @@ -67,9 +37,9 @@ zend_function_entry *pt_abi_function_entries(const reg::FunctionEntry *entries, return functions; } -zend_class_entry *pt_abi_register_internal_class(const char *name, const reg::FunctionEntry *entries, size_t count) +static zend_class_entry *abi_register_internal_class(const char *name, const reg::FunctionEntry *entries, size_t count) { - zend_function_entry *functions = pt_abi_function_entries(entries, 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); @@ -77,7 +47,7 @@ zend_class_entry *pt_abi_register_internal_class(const char *name, const reg::Fu return registered; } -void pt_abi_create_closure(zval *out, zend_function *fn, zend_class_entry *scope, zend_class_entry *calledScope, zend_object *thisObject) +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 */ @@ -91,7 +61,7 @@ void pt_abi_create_closure(zval *out, zend_function *fn, zend_class_entry *scope #endif } -zend_object *pt_abi_closure_this(zval *closure) +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) */ @@ -104,7 +74,7 @@ zend_object *pt_abi_closure_this(zval *closure) /* 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 */ -void pt_abi_weakrefs_hash_clean(HashTable *ht) +static void abi_weakrefs_hash_clean(HashTable *ht) { #if PHP_VERSION_ID >= 80500 zend_weakrefs_hash_clean(ht); @@ -116,17 +86,17 @@ void pt_abi_weakrefs_hash_clean(HashTable *ht) #endif } -void pt_abi_weakrefs_hash_destroy(HashTable *ht) +static void abi_weakrefs_hash_destroy(HashTable *ht) { #if PHP_VERSION_ID >= 80500 zend_weakrefs_hash_destroy(ht); #else - pt_abi_weakrefs_hash_clean(ht); + abi_weakrefs_hash_clean(ht); zend_hash_destroy(ht); #endif } -void pt_abi_call_stack_size_error() +static void abi_call_stack_size_error() { #if PHP_VERSION_ID >= 80400 zend_call_stack_size_error(); @@ -137,12 +107,181 @@ void pt_abi_call_stack_size_error() #endif } -void pt_abi_try_assign_ref_str(zval *zv, zend_string *str) +static void abi_try_assign_ref_str(zval *zv, zend_string *str) { ZEND_TRY_ASSIGN_REF_STR(zv, str); } -void pt_abi_try_assign_ref_true(zval *zv) +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); +} + +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; +} diff --git a/turbo-ext/src/ArenaCache.cpp b/turbo-ext/src/ArenaCache.cpp index c179100470c..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; } diff --git a/turbo-ext/src/Assertions.cpp b/turbo-ext/src/Assertions.cpp index 7a6730fedb2..274862f834e 100644 --- a/turbo-ext/src/Assertions.cpp +++ b/turbo-ext/src/Assertions.cpp @@ -104,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/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/ConstantArrayType.cpp b/turbo-ext/src/ConstantArrayType.cpp index 537f5c35e17..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(); 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..cb5a962444c --- /dev/null +++ b/turbo-ext/src/Core.cpp @@ -0,0 +1,98 @@ +/* + * 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 + +PT_CORE_API pt_abi_globals pt_abi; + +/* 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 426f0fa80ba..8469c8cd86f 100644 --- a/turbo-ext/src/Engine.cpp +++ b/turbo-ext/src/Engine.cpp @@ -211,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); } @@ -597,10 +597,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; @@ -615,9 +615,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 a33330a44d0..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 || PT_CE_INFO(ce).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 && PT_CE_INFO(containerCe).user.filename != NULL && zend_string_equals(PT_CE_INFO(containerCe).user.filename, PT_CE_INFO(ce).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/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 2d4ee60ee12..76b682a3d45 100644 --- a/turbo-ext/src/IsSuperTypeOfResult.cpp +++ b/turbo-ext/src/IsSuperTypeOfResult.cpp @@ -719,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 a320abf7979..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); } @@ -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/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 63e64210d56..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; } @@ -304,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); } @@ -2709,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(); @@ -3594,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 c302c8039f7..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; 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 7157587a996..74f673092e3 100644 --- a/turbo-ext/src/Shadow.cpp +++ b/turbo-ext/src/Shadow.cpp @@ -43,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, '\\'); 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 334f8268fe2..dfad58674f2 100644 --- a/turbo-ext/src/StaticType.cpp +++ b/turbo-ext/src/StaticType.cpp @@ -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/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/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 22499ee3441..e2268bfc7b8 100644 --- a/turbo-ext/src/TypeCombinatorCache.cpp +++ b/turbo-ext/src/TypeCombinatorCache.cpp @@ -249,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); @@ -365,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; @@ -749,9 +749,9 @@ static zend_object *typeObjectClone(zend_object *old) header->memoGeneration = 0; zend_object_std_init(&header->std, old->ce); header->std.handlers = pt_type_object_handlers; - if (old->ce->default_properties_count) { + 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++; @@ -996,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..161ceee216f 100644 --- a/turbo-ext/src/TypeOps.h +++ b/turbo-ext/src/TypeOps.h @@ -181,7 +181,7 @@ struct pt_type_op_info uint8_t argKinds; /* two bits per argument, argument 0 in the low bits */ }; -extern const pt_type_op_info pt_type_op_infos[PT_OP_COUNT]; +PT_CORE_API extern const pt_type_op_info pt_type_op_infos[PT_OP_COUNT]; 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 +234,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 14282d5bbbc..35b6db5ab3a 100644 --- a/turbo-ext/src/TypeTraits.cpp +++ b/turbo-ext/src/TypeTraits.cpp @@ -66,7 +66,7 @@ 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); + 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; } @@ -81,7 +81,7 @@ 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); + 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(); @@ -955,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; @@ -1358,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(); } @@ -1924,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(); @@ -2650,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; @@ -3374,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())); } @@ -3425,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; @@ -3494,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(); @@ -3910,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 @@ -3936,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 index 88dfc6c1060..e188c2dbb7c 100644 --- a/turbo-ext/src/abi.h +++ b/turbo-ext/src/abi.h @@ -16,6 +16,8 @@ #ifndef PHPSTANTURBO_ABI_H #define PHPSTANTURBO_ABI_H +#include + #define PT_ABI_EG_FIELDS(X) \ X(exception) \ X(uninitialized_zval) \ @@ -59,24 +61,74 @@ 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) +#define PT_CORE_API +#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 offset_class_entry_info; /* 8.4 grew zend_class_entry 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; @@ -87,55 +139,83 @@ struct pt_abi_globals /* 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 */ -}; -/* 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; + /* 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); }; -/* a persistent copy of the standard handlers with the given overrides (the - * null members keep the standard handler) */ -const zend_object_handlers *pt_abi_object_handlers(const pt_abi_handlers &overrides); - -/* engine API that differs between minors, implemented once per minor */ -void pt_abi_create_closure(zval *out, zend_function *fn, zend_class_entry *scope, zend_class_entry *calledScope, zend_object *thisObject); -zend_object *pt_abi_closure_this(zval *closure); -void pt_abi_weakrefs_hash_clean(HashTable *ht); -void pt_abi_weakrefs_hash_destroy(HashTable *ht); -void pt_abi_call_stack_size_error(); -void pt_abi_try_assign_ref_str(zval *zv, zend_string *str); -void pt_abi_try_assign_ref_true(zval *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)) - -extern pt_abi_globals pt_abi; +/* Defined by the shared core, filled by the version-specific library. */ +PT_CORE_API extern pt_abi_globals pt_abi; /* fills pt_abi from the running engine; first thing MINIT (and, in ZTS - * builds, RINIT) does */ + * builds, RINIT) does — version-specific (Abi.cpp) */ void pt_abi_init(); -namespace reg { -struct FunctionEntry; -} - /* 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 */ -zend_function_entry *pt_abi_function_entries(const reg::FunctionEntry *entries, size_t count); - +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 */ -zend_class_entry *pt_abi_register_internal_class(const char *name, const reg::FunctionEntry *entries, size_t count); +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 + #undef EG #undef CG #define EG(field) (*pt_abi.eg_##field) @@ -157,6 +237,232 @@ zend_class_entry *pt_abi_register_internal_class(const char *name, const reg::Fu 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 @@ -170,20 +476,26 @@ zend_class_entry *pt_abi_register_internal_class(const char *name, const reg::Fu /* 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) { - if ((ce->ce_flags & ZEND_ACC_HAS_AST_PROPERTIES) && ZEND_MAP_PTR(ce->mutable_data)) { - zend_class_mutable_data *mutable_data = (zend_class_mutable_data *) ZEND_MAP_PTR_GET_IMM(ce->mutable_data); - return mutable_data->default_properties_table; + 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 ce->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) * (ce->default_properties_count - ((ce->ce_flags & ZEND_ACC_USE_GUARDS) ? 0 : 1)); + 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; @@ -198,10 +510,12 @@ static zend_always_inline void *pt_abi_object_alloc(size_t obj_size, zend_class_ #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)) -/* ce->info */ -#define PT_CE_INFO(ce) (*(decltype(((zend_class_entry *) nullptr)->info) *) ((const char *) (ce) + pt_abi.offset_class_entry_info)) /* 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 */ @@ -212,7 +526,7 @@ static zend_always_inline bool pt_abi_object_is_lazy(const zend_object *obj) return (extraFlags & pt_abi.object_lazy_flags) != 0; } -/* zend_is_true() returned int up to 8.4 and bool since; declared bool — +/* 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 diff --git a/turbo-ext/src/main.cpp b/turbo-ext/src/main.cpp index e8da9a73974..d3b1703fded 100644 --- a/turbo-ext/src/main.cpp +++ b/turbo-ext/src/main.cpp @@ -18,27 +18,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, @@ -159,29 +138,6 @@ 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; - -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; -} - -void pt_minit_registrations_run() -{ - for (const pt_minit_registration *registration = pt_minit_registrations; registration != nullptr; registration = registration->next) { - registration->run(); - } -} - static PHP_MINIT_FUNCTION(phpstan_turbo) { #ifdef ZTS @@ -207,7 +163,7 @@ static PHP_MINIT_FUNCTION(phpstan_turbo) static PHP_MSHUTDOWN_FUNCTION(phpstan_turbo) { - pt_arena_mshutdown(); + pt_core_mshutdown(); return SUCCESS; } @@ -219,35 +175,14 @@ static PHP_RINIT_FUNCTION(phpstan_turbo) 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(); + pt_core_rshutdown(); return SUCCESS; } 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 ec8df0f2dd3..5dce507f7bc 100644 --- a/turbo-ext/src/reg.h +++ b/turbo-ext/src/reg.h @@ -652,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 { 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 19b51d324f3..7064d9ad312 100644 --- a/turbo-ext/src/support.h +++ b/turbo-ext/src/support.h @@ -495,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); /* }}} */ @@ -518,7 +518,7 @@ struct pt_globals_t { char trusted_types_prefix[MAXPATHLEN + 16]; }; -extern pt_globals_t pt_globals; +PT_CORE_API extern pt_globals_t pt_globals; #define PT_G(v) (pt_globals.v) @@ -559,7 +559,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(); \ @@ -643,7 +648,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 @@ -920,7 +925,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(); /* }}} */ @@ -1181,7 +1186,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 */ From 2ccc4c9ca9dc1ebd27e8cf5576f0c285a4a6fb26 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 8 Oct 2026 12:22:14 +0200 Subject: [PATCH 3/6] turbo-ext: shared code compiles identically against PHP 8.3-8.6 (clang) The last differences between the header sets, each pinned in abi.h or moved into Abi.cpp: - the fresh-C-stack switch (a zend_fiber_context, its switch and a zend_try, whose SETJMP became sigsetjmp in 8.6) is implemented per minor - codegen pins for helpers 8.6 re-spelled without changing what they do: zend_string_equals_literal_ci / _equals_ci / starts_with_literal(_ci), ZSTR_IS_INTERNED, array_init(_size), Z_TRY_ADDREF_P / Z_TRY_DELREF_P - the comparison treats the zend_result rename (a different mangling of the same enum) and the symbol/section name tables as names, not code clang 14 in the CI images (gate-linux.sh with GATE_IMAGE=turbo-build-clang, -fgnuc-version=4.3 so the older headers enable ZEND_COLD for it too) now finds no differing function, data section or relocation outside Abi.o, Shadow.o, TrustedTypes.o and main.o across 8.3, 8.4, 8.5 and 8.6. GCC still differs in 15 string-building functions (8.6 const-qualified zval_get_string() and friends, inline helpers compiled inside the headers). Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_017h1GfB7vfJPgwcTHD9Fth6 --- .../bin/shared-core/compare-data-linux.sh | 4 +- .../bin/shared-core/compare-functions.py | 12 ++- turbo-ext/src/Abi.cpp | 88 +++++++++++++++++++ turbo-ext/src/Engine.cpp | 80 +---------------- turbo-ext/src/MutatingScope.cpp | 9 +- turbo-ext/src/abi.h | 50 +++++++++++ 6 files changed, 160 insertions(+), 83 deletions(-) diff --git a/turbo-ext/bin/shared-core/compare-data-linux.sh b/turbo-ext/bin/shared-core/compare-data-linux.sh index 52281b9f0bd..998917d538c 100755 --- a/turbo-ext/bin/shared-core/compare-data-linux.sh +++ b/turbo-ext/bin/shared-core/compare-data-linux.sh @@ -4,8 +4,8 @@ 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'; } +norm_relocs() { $O -r "$1" | grep -v 'file format' | sed -E 's/\.L[A-Za-z]*[0-9]+/.L/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 | md5)" != "$(data $S/$B/$o | md5)" ] && echo "DATA DIFF $o" diff --git a/turbo-ext/bin/shared-core/compare-functions.py b/turbo-ext/bin/shared-core/compare-functions.py index ff3ed01c602..4a2ba0c4fac 100644 --- a/turbo-ext/bin/shared-core/compare-functions.py +++ b/turbo-ext/bin/shared-core/compare-functions.py @@ -24,7 +24,7 @@ def flush(): m = func_re.match(line) if m: flush() - cur, lines, n = m.group(1), [], 0 + cur, lines, n = m.group(1).replace('16ZEND_RESULT_CODE', '11zend_result'), [], 0 continue if cur is None: continue @@ -39,8 +39,14 @@ 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) + # 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) diff --git a/turbo-ext/src/Abi.cpp b/turbo-ext/src/Abi.cpp index 9969ffc5166..cf13b5f0f75 100644 --- a/turbo-ext/src/Abi.cpp +++ b/turbo-ext/src/Abi.cpp @@ -7,7 +7,9 @@ #include "support.h" #include "reg.h" +#include "Engine.h" #include "zend_closures.h" +#include "zend_fibers.h" #include "zend_weakrefs.h" @@ -237,6 +239,91 @@ 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); @@ -284,4 +371,5 @@ void pt_abi_init() 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; } diff --git a/turbo-ext/src/Engine.cpp b/turbo-ext/src/Engine.cpp index 8469c8cd86f..4e46ba4b2f3 100644 --- a/turbo-ext/src/Engine.cpp +++ b/turbo-ext/src/Engine.cpp @@ -479,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); } /* }}} */ 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/abi.h b/turbo-ext/src/abi.h index e188c2dbb7c..aa5d33b3c7a 100644 --- a/turbo-ext/src/abi.h +++ b/turbo-ext/src/abi.h @@ -166,6 +166,9 @@ struct pt_abi_globals 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); }; /* Defined by the shared core, filled by the version-specific library. */ @@ -216,6 +219,53 @@ static zend_always_inline zend_class_entry *pt_abi_class_entry(T *ce) #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) From 28c29b88e559ab81115f5c7341e2953501646231 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 8 Oct 2026 12:43:31 +0200 Subject: [PATCH 4/6] turbo-ext: build the shared core and the per-version extension with make SPLIT=1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `make SPLIT=1` builds phpstan_turbo_core.so (everything but the four version-specific sources) and a phpstan_turbo.so linked against it, which finds it next to itself ($ORIGIN, @loader_path); `make SPLIT=1 extension` links only the extension against a core already in place, which is what a per-PHP-version CI leg does. `make pgo` and `make strip` handle the pair; a plain `make` (and the phpize build) still produce the one self-contained extension. The boundary is functions only — pt_abi, pt_globals and pt_type_op_infos are reached through accessors from the version-specific side, since a Windows DLL can delay-load functions but not data — and main.cpp is version-specific like Abi/Shadow/TrustedTypes. The extension refuses a core of another version: it registers nothing and reports "core-mismatch", which TurboExtensionEnabler rejects. On Windows it loads the core from its own directory in get_module() (PHP loads extensions with plain LoadLibrary, which would search the application's directory instead), and zend_is_true() goes through pt_abi there (no assembler names on MSVC). Verified in the CI images: a PGO+LTO core built in the 8.5 image (6.73 MB stripped) with extensions built against it in the 8.3, 8.4, 8.6 and 8.6 ZTS images (68 KB each): smoke.php ALL OK on each; a version-mismatched pair reports core-mismatch and stays inactive. macOS: the same via make SPLIT=1, ALL OK on 8.5. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_017h1GfB7vfJPgwcTHD9Fth6 --- turbo-ext/Makefile | 82 ++++++++++++++++++++++++++++++++++++----- turbo-ext/src/Abi.cpp | 1 + turbo-ext/src/Core.cpp | 27 +++++++++++++- turbo-ext/src/TypeOps.h | 6 ++- turbo-ext/src/abi.h | 20 +++++++++- turbo-ext/src/main.cpp | 80 +++++++++++++++++++++++++++++++++------- turbo-ext/src/support.h | 6 ++- turbo-ext/src/version.h | 19 ++++++++++ 8 files changed, 213 insertions(+), 28 deletions(-) create mode 100644 turbo-ext/src/version.h diff --git a/turbo-ext/Makefile b/turbo-ext/Makefile index ad64c0f048f..2cd9de43c1b 100644 --- a/turbo-ext/Makefile +++ b/turbo-ext/Makefile @@ -207,17 +207,81 @@ 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 + # 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 @@ -228,9 +292,9 @@ phpstan_turbo.so: $(OBJECTS) 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 $(wildcard $(CORE_LIB)) else - strip --strip-unneeded phpstan_turbo.so + strip --strip-unneeded phpstan_turbo.so $(wildcard $(CORE_LIB)) endif # every object depends on every header: the op tables and the trait helper @@ -240,7 +304,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)' > $@ @@ -262,7 +326,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 @@ -273,7 +337,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/src/Abi.cpp b/turbo-ext/src/Abi.cpp index cf13b5f0f75..e49d77d0d60 100644 --- a/turbo-ext/src/Abi.cpp +++ b/turbo-ext/src/Abi.cpp @@ -372,4 +372,5 @@ void pt_abi_init() 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/Core.cpp b/turbo-ext/src/Core.cpp index cb5a962444c..88dc9ad7d01 100644 --- a/turbo-ext/src/Core.cpp +++ b/turbo-ext/src/Core.cpp @@ -10,10 +10,35 @@ #include "support.h" #include "reg.h" #include "Engine.h" +#include "version.h" #include -PT_CORE_API pt_abi_globals pt_abi; +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 diff --git a/turbo-ext/src/TypeOps.h b/turbo-ext/src/TypeOps.h index 161ceee216f..d88663b50e9 100644 --- a/turbo-ext/src/TypeOps.h +++ b/turbo-ext/src/TypeOps.h @@ -181,7 +181,11 @@ struct pt_type_op_info uint8_t argKinds; /* two bits per argument, argument 0 in the low bits */ }; -PT_CORE_API extern const pt_type_op_info pt_type_op_infos[PT_OP_COUNT]; +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) { diff --git a/turbo-ext/src/abi.h b/turbo-ext/src/abi.h index aa5d33b3c7a..615a8e04325 100644 --- a/turbo-ext/src/abi.h +++ b/turbo-ext/src/abi.h @@ -66,7 +66,11 @@ * 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 @@ -169,10 +173,18 @@ struct pt_abi_globals /* 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. */ -PT_CORE_API extern pt_abi_globals pt_abi; +/* 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) */ @@ -583,6 +595,10 @@ static zend_always_inline bool pt_abi_object_is_lazy(const zend_object *obj) #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 diff --git a/turbo-ext/src/main.cpp b/turbo-ext/src/main.cpp index d3b1703fded..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,20 +21,6 @@ #include #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 @@ -138,11 +127,63 @@ static void ZEND_FASTCALL runtimeExitImmediately(INTERNAL_FUNCTION_PARAMETERS) _exit(EG(exit_status)); } +/* 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; + +extern "C" { +extern zend_module_entry phpstan_turbo_module_entry; +} + +#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() +{ + 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(""); @@ -163,6 +204,7 @@ static PHP_MINIT_FUNCTION(phpstan_turbo) static PHP_MSHUTDOWN_FUNCTION(phpstan_turbo) { + if (!pt_core_matches) return SUCCESS; pt_core_mshutdown(); return SUCCESS; @@ -170,6 +212,7 @@ static PHP_MSHUTDOWN_FUNCTION(phpstan_turbo) static PHP_RINIT_FUNCTION(phpstan_turbo) { + if (!pt_core_matches) return SUCCESS; #ifdef ZTS ZEND_TSRMLS_CACHE_UPDATE(); pt_abi_init(); @@ -182,6 +225,7 @@ static PHP_RINIT_FUNCTION(phpstan_turbo) static PHP_RSHUTDOWN_FUNCTION(phpstan_turbo) { + if (!pt_core_matches) return SUCCESS; pt_core_rshutdown(); return SUCCESS; @@ -212,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/support.h b/turbo-ext/src/support.h index 7064d9ad312..6980dfcbf72 100644 --- a/turbo-ext/src/support.h +++ b/turbo-ext/src/support.h @@ -518,7 +518,11 @@ struct pt_globals_t { char trusted_types_prefix[MAXPATHLEN + 16]; }; -PT_CORE_API extern pt_globals_t pt_globals; +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) 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 From 99790406e0999eda0ba49088d393f5960f4dab41 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 8 Oct 2026 13:03:11 +0200 Subject: [PATCH 5/6] Ship the turbo extension as a shared core plus a thin extension per PHP version phar.yml builds one profile-guided core per platform (turbo-compile-core, -core-musl-arm64, -core-windows for NTS and TS; the jobs turbo-origins now reuses from earlier runs) and links each PHP version's extension against it in the existing turbo-compile* legs, whose artifacts carry the core next to the extension so every test job finds it there. .github/scripts/turbo-dist-layout.sh maps the artifacts onto the dist layout (turbo-ext//phpstan_turbo_core.so|.dll next to phpstan_turbo-.so|.dll) for the aggregate artifact and the commit job, refusing legs of one platform that carry different cores. turbo-shared-core-gate (bin/shared-core/check-linux.sh) compiles the shared sources against every supported PHP's headers with clang and fails on any difference outside the four version-specific sources; the commit job waits for it. config.w32 gains --enable-phpstan-turbo=core / =extension; the extension DLL delay-loads the core from its own directory. TurboExtensionSelector offers no binary when the core is missing next to the extension. The Makefile's default goal stays the extension (it had become `objects`), and a plain `make strip` leaves a core put in place untouched. README.md documents the layout, the mechanisms that keep shared code version-neutral, the gate, and the procedure for supporting a new PHP version; turbo-ext/CLAUDE.md the rules for code in shared sources. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_017h1GfB7vfJPgwcTHD9Fth6 --- .github/scripts/download-php-windows-devel.sh | 25 + .github/scripts/find-turbo-origins.sh | 23 +- .github/scripts/turbo-dist-layout.sh | 59 ++ .github/workflows/phar.yml | 886 ++++++++++++------ src/Turbo/TurboExtensionSelector.php | 27 +- .../Turbo/TurboExtensionSelectorTest.php | 18 + turbo-ext/.gitignore | 1 + turbo-ext/CLAUDE.md | 23 + turbo-ext/Makefile | 12 +- turbo-ext/README.md | 170 ++++ turbo-ext/bin/shared-core/check-linux.sh | 68 ++ .../bin/shared-core/compare-data-linux.sh | 6 +- turbo-ext/config.w32 | 66 +- 13 files changed, 1059 insertions(+), 325 deletions(-) create mode 100644 .github/scripts/download-php-windows-devel.sh create mode 100755 .github/scripts/turbo-dist-layout.sh create mode 100755 turbo-ext/bin/shared-core/check-linux.sh 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/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 2cd9de43c1b..79f43f2bb7f 100644 --- a/turbo-ext/Makefile +++ b/turbo-ext/Makefile @@ -244,6 +244,9 @@ 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) @@ -288,13 +291,16 @@ extension: $(SHIM_OBJECTS) # 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 $(wildcard $(CORE_LIB)) + strip -x phpstan_turbo.so $(if $(filter 1,$(SPLIT)),$(CORE_LIB)) else - strip --strip-unneeded phpstan_turbo.so $(wildcard $(CORE_LIB)) + 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 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 index 998917d538c..ff2f9c39ecb 100755 --- a/turbo-ext/bin/shared-core/compare-data-linux.sh +++ b/turbo-ext/bin/shared-core/compare-data-linux.sh @@ -4,11 +4,11 @@ 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; s/16ZEND_RESULT_CODE/11zend_result/g'; } +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 | md5)" != "$(data $S/$B/$o | md5)" ] && echo "DATA DIFF $o" - [ "$(norm_relocs $o | md5)" != "$(norm_relocs $S/$B/$o | md5)" ] && echo "RELOC DIFF $o" + [ "$(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/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"); + } } From be7f5bc4ee10b14672ef763de52e1af43ec04232 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 8 Oct 2026 16:09:24 +0200 Subject: [PATCH 6/6] Bump expected turbo version --- src/Turbo/TurboExtensionEnabler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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;