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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
turbo-ext: shared 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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017h1GfB7vfJPgwcTHD9Fth6
  • Loading branch information
ondrejmirtes and claude committed Oct 8, 2026
commit 2ccc4c9ca9dc1ebd27e8cf5576f0c285a4a6fb26
4 changes: 2 additions & 2 deletions turbo-ext/bin/shared-core/compare-data-linux.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
12 changes: 9 additions & 3 deletions turbo-ext/bin/shared-core/compare-functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down
88 changes: 88 additions & 0 deletions turbo-ext/src/Abi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"


Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
80 changes: 3 additions & 77 deletions turbo-ext/src/Engine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/* }}} */
Expand Down
9 changes: 8 additions & 1 deletion turbo-ext/src/MutatingScope.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
50 changes: 50 additions & 0 deletions turbo-ext/src/abi.h
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down Expand Up @@ -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)
Expand Down