Skip to content

Commit 142425d

Browse files
ondrejmirtesclaude
andcommitted
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
1 parent ba5ae74 commit 142425d

6 files changed

Lines changed: 160 additions & 83 deletions

File tree

‎turbo-ext/bin/shared-core/compare-data-linux.sh‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
S=${SHARED_CORE_WORK_DIR:?set SHARED_CORE_WORK_DIR to a scratch directory}
55
O=${LLVM_OBJDUMP:-/opt/homebrew/opt/llvm/bin/llvm-objdump}
66
A=$1; B=$2
7-
norm_relocs() { $O -r "$1" | grep -v 'file format' | sed -E 's/\.L[A-Za-z]*[0-9]+/.L/g'; }
8-
data() { $O -s "$1" | awk '/^Contents of section / { keep = ($4 !~ /^\.text/ && $4 !~ /^\.comment/) } keep'; }
7+
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'; }
8+
data() { $O -s "$1" | sed 's/16ZEND_RESULT_CODE/11zend_result/g' | awk '/^Contents of section / { keep = ($4 !~ /^\.(text|comment|strtab|shstrtab|symtab)/) } keep'; }
99
cd "$S/$A"
1010
for o in $(find src -name '*.o' | sort); do
1111
[ "$(data $o | md5)" != "$(data $S/$B/$o | md5)" ] && echo "DATA DIFF $o"

‎turbo-ext/bin/shared-core/compare-functions.py‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def flush():
2424
m = func_re.match(line)
2525
if m:
2626
flush()
27-
cur, lines, n = m.group(1), [], 0
27+
cur, lines, n = m.group(1).replace('16ZEND_RESULT_CODE', '11zend_result'), [], 0
2828
continue
2929
if cur is None:
3030
continue
@@ -39,8 +39,14 @@ def flush():
3939
s = re.sub(r'\$_\d+', '$_', s)
4040
s = re.sub(r'\.cold\.\d+', '.cold', s)
4141
s = re.sub(r'(l_\.str|ltmp|LJTI|lJTI|l___const|lCPI)[\.\d_]+', r'\1', s)
42-
# GCC's local labels (.LC0, .LANCHOR1, .L42) and clone suffixes
43-
s = re.sub(r'\.L[A-Za-z]*\d+', '.L', s)
42+
# 8.6 renamed enum ZEND_RESULT_CODE to zend_result: a different
43+
# mangling of the same type in our own functions' names
44+
s = s.replace('16ZEND_RESULT_CODE', '11zend_result')
45+
# local labels: GCC's .LC0, .LANCHOR1, .L42 and clang's .L.str.356 —
46+
# numbered in the order the translation unit declares them, so one
47+
# more string literal in a header renumbers every later one (their
48+
# contents are compared by compare-data-*.sh)
49+
s = re.sub(r'(\.L[A-Za-z_.]*?)\.?\d+', r'\1', s)
4450
# GCC addresses merged strings through the section of whichever
4551
# function emitted them first: section order, not content
4652
s = re.sub(r'\.rodata\.[^ +]*\.str1\.\d+(\+0x[0-9a-f]+)?', '.rodata.str', s)

‎turbo-ext/src/Abi.cpp‎

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
#include "support.h"
88
#include "reg.h"
99

10+
#include "Engine.h"
1011
#include "zend_closures.h"
12+
#include "zend_fibers.h"
1113
#include "zend_weakrefs.h"
1214

1315

@@ -237,6 +239,91 @@ static ssize_t abi_stream_read(php_stream *stream, char *buf, size_t count)
237239
return php_stream_read(stream, buf, count);
238240
}
239241

242+
/* {{{ continuing a call on a fresh C stack (pt_engine_with_stack()) */
243+
244+
namespace {
245+
246+
/* one call continued on a fresh stack: the body, and the VM state it left
247+
* behind — zend_fiber_switch_context() restores the caller's state from
248+
* before the switch, which the body may legitimately have changed */
249+
struct FreshStackCall
250+
{
251+
void (*body)(void *);
252+
void *data;
253+
bool bailout;
254+
zend_vm_stack vmStack;
255+
zval *vmStackTop;
256+
zval *vmStackEnd;
257+
size_t vmStackPageSize;
258+
int errorReporting;
259+
};
260+
261+
/* the call the coroutine starting next runs (read once, at its start) */
262+
FreshStackCall *pt_fresh_stack_call = nullptr;
263+
264+
/* identifies the contexts in fiber observers */
265+
char pt_fresh_stack_kind;
266+
267+
ZEND_STACK_ALIGNED void freshStackCoroutine(zend_fiber_transfer *transfer)
268+
{
269+
(void) transfer; /* already addresses the caller, which is where it returns */
270+
FreshStackCall *call = pt_fresh_stack_call;
271+
zend_fiber_context *context = EG(current_fiber_context);
272+
#ifdef ZEND_CHECK_STACK_LIMIT
273+
EG(stack_base) = zend_fiber_stack_base(context->stack);
274+
EG(stack_limit) = zend_fiber_stack_limit(context->stack);
275+
#endif
276+
zend_try {
277+
call->body(call->data);
278+
} zend_catch {
279+
call->bailout = true;
280+
} zend_end_try();
281+
call->vmStack = EG(vm_stack);
282+
call->vmStackTop = EG(vm_stack_top);
283+
call->vmStackEnd = EG(vm_stack_end);
284+
call->vmStackPageSize = EG(vm_stack_page_size);
285+
call->errorReporting = EG(error_reporting);
286+
}
287+
288+
} // namespace
289+
290+
static void abi_run_on_fresh_stack(void (*body)(void *), void *data)
291+
{
292+
zend_fiber_context context;
293+
memset(&context, 0, sizeof(context));
294+
if (UNEXPECTED(zend_fiber_init_context(&context, &pt_fresh_stack_kind, freshStackCoroutine, PT_ENGINE_FRESH_STACK_SIZE_LIMIT) == FAILURE)) {
295+
/* no stack to be had: continue here, where the engine's own stack
296+
* limit decides */
297+
if (EG(exception) != NULL) {
298+
zend_clear_exception();
299+
}
300+
body(data);
301+
return;
302+
}
303+
304+
FreshStackCall call = { body, data, false, NULL, NULL, NULL, 0, 0 };
305+
FreshStackCall *previous = pt_fresh_stack_call;
306+
pt_fresh_stack_call = &call;
307+
zend_fiber_transfer transfer;
308+
transfer.context = &context;
309+
transfer.flags = 0;
310+
ZVAL_NULL(&transfer.value);
311+
/* returns once the body finished; the dead context is destroyed then */
312+
zend_fiber_switch_context(&transfer);
313+
pt_fresh_stack_call = previous;
314+
315+
EG(vm_stack) = call.vmStack;
316+
EG(vm_stack_top) = call.vmStackTop;
317+
EG(vm_stack_end) = call.vmStackEnd;
318+
EG(vm_stack_page_size) = call.vmStackPageSize;
319+
EG(error_reporting) = call.errorReporting;
320+
if (UNEXPECTED(call.bailout)) {
321+
zend_bailout();
322+
}
323+
}
324+
325+
/* }}} */
326+
240327
void pt_abi_init()
241328
{
242329
#define PT_ABI_EG_FIELD(field) pt_abi.eg_##field = &EG(field);
@@ -284,4 +371,5 @@ void pt_abi_init()
284371
pt_abi.call_known_function = abi_call_known_function;
285372
pt_abi.stream_free = abi_stream_free;
286373
pt_abi.stream_read = abi_stream_read;
374+
pt_abi.run_on_fresh_stack = abi_run_on_fresh_stack;
287375
}

‎turbo-ext/src/Engine.cpp‎

Lines changed: 3 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -479,85 +479,11 @@ bool pt_engine_call_node_callback(zval *callback, zval *node, zval *scope)
479479
return !pt_type_call_callable(callback, 2, argv).isUndef();
480480
}
481481

482-
namespace {
483-
484-
/* one call continued on a fresh stack: the body, and the VM state it left
485-
* behind — zend_fiber_switch_context() restores the caller's state from
486-
* before the switch, which the body may legitimately have changed */
487-
struct FreshStackCall
488-
{
489-
void (*body)(void *);
490-
void *data;
491-
bool bailout;
492-
zend_vm_stack vmStack;
493-
zval *vmStackTop;
494-
zval *vmStackEnd;
495-
size_t vmStackPageSize;
496-
int errorReporting;
497-
};
498-
499-
/* the call the coroutine starting next runs (read once, at its start) */
500-
FreshStackCall *pt_fresh_stack_call = nullptr;
501-
502-
/* identifies the contexts in fiber observers */
503-
char pt_fresh_stack_kind;
504-
505-
ZEND_STACK_ALIGNED void freshStackCoroutine(zend_fiber_transfer *transfer)
506-
{
507-
(void) transfer; /* already addresses the caller, which is where it returns */
508-
FreshStackCall *call = pt_fresh_stack_call;
509-
zend_fiber_context *context = EG(current_fiber_context);
510-
#ifdef ZEND_CHECK_STACK_LIMIT
511-
EG(stack_base) = zend_fiber_stack_base(context->stack);
512-
EG(stack_limit) = zend_fiber_stack_limit(context->stack);
513-
#endif
514-
zend_try {
515-
call->body(call->data);
516-
} zend_catch {
517-
call->bailout = true;
518-
} zend_end_try();
519-
call->vmStack = EG(vm_stack);
520-
call->vmStackTop = EG(vm_stack_top);
521-
call->vmStackEnd = EG(vm_stack_end);
522-
call->vmStackPageSize = EG(vm_stack_page_size);
523-
call->errorReporting = EG(error_reporting);
524-
}
525-
526-
} // namespace
527-
528482
void pt_engine_run_on_fresh_stack(void (*body)(void *), void *data)
529483
{
530-
zend_fiber_context context;
531-
memset(&context, 0, sizeof(context));
532-
if (UNEXPECTED(zend_fiber_init_context(&context, &pt_fresh_stack_kind, freshStackCoroutine, PT_ENGINE_FRESH_STACK_SIZE_LIMIT) == FAILURE)) {
533-
/* no stack to be had: continue here, where the engine's own stack
534-
* limit decides */
535-
if (EG(exception) != NULL) {
536-
zend_clear_exception();
537-
}
538-
body(data);
539-
return;
540-
}
541-
542-
FreshStackCall call = { body, data, false, NULL, NULL, NULL, 0, 0 };
543-
FreshStackCall *previous = pt_fresh_stack_call;
544-
pt_fresh_stack_call = &call;
545-
zend_fiber_transfer transfer;
546-
transfer.context = &context;
547-
transfer.flags = 0;
548-
ZVAL_NULL(&transfer.value);
549-
/* returns once the body finished; the dead context is destroyed then */
550-
zend_fiber_switch_context(&transfer);
551-
pt_fresh_stack_call = previous;
552-
553-
EG(vm_stack) = call.vmStack;
554-
EG(vm_stack_top) = call.vmStackTop;
555-
EG(vm_stack_end) = call.vmStackEnd;
556-
EG(vm_stack_page_size) = call.vmStackPageSize;
557-
EG(error_reporting) = call.errorReporting;
558-
if (UNEXPECTED(call.bailout)) {
559-
zend_bailout();
560-
}
484+
/* the fiber context, its switch and the bailout around the body are
485+
* engine internals of each minor (Abi.cpp) */
486+
pt_abi.run_on_fresh_stack(body, data);
561487
}
562488

563489
/* }}} */

‎turbo-ext/src/MutatingScope.cpp‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2636,7 +2636,14 @@ class MutatingScope
26362636
PHP_MD5Update(&context, (const unsigned char *) ZSTR_VAL(parts.s), ZSTR_LEN(parts.s));
26372637
}
26382638
PHP_MD5Final(digest, &context);
2639-
make_digest_ex(hex, digest, sizeof(digest));
2639+
/* make_digest_ex()'s lowercase hex, spelled out: its length parameter
2640+
* became a size_t in 8.5, so a call compiles differently per version */
2641+
static const char hexDigits[] = "0123456789abcdef";
2642+
for (size_t i = 0; i < sizeof(digest); i++) {
2643+
hex[2 * i] = hexDigits[digest[i] >> 4];
2644+
hex[2 * i + 1] = hexDigits[digest[i] & 0x0f];
2645+
}
2646+
hex[32] = '\0';
26402647
smart_str_free(&parts);
26412648
return zv::Val::string(hex, 32);
26422649
}

‎turbo-ext/src/abi.h‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,9 @@ struct pt_abi_globals
166166
void (*call_known_function)(zend_function *fn, zend_object *object, zend_class_entry *calledScope, zval *retval, uint32_t paramCount, zval *params, HashTable *namedParams);
167167
int (*stream_free)(php_stream *stream, int closeOptions);
168168
ssize_t (*stream_read)(php_stream *stream, char *buf, size_t count);
169+
/* pt_engine_run_on_fresh_stack(): a fiber context, its switch and a
170+
* zend_try around the body (8.6 moved SETJMP to sigsetjmp) */
171+
void (*run_on_fresh_stack)(void (*body)(void *), void *data);
169172
};
170173

171174
/* 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)
216219
#define ZEND_COLD __attribute__((cold))
217220
#endif
218221

222+
/* Codegen pins: helpers whose result is the same in every minor but whose
223+
* spelling changed (8.6 turned macros into inline functions and back), so
224+
* the shared code would compile to different instructions per header set.
225+
* One definition for all — 8.6's. */
226+
static zend_always_inline bool pt_abi_string_equals_cstr_ci(const zend_string *s1, const char *s2, size_t s2_length)
227+
{
228+
return ZSTR_LEN(s1) == s2_length && !zend_binary_strcasecmp(ZSTR_VAL(s1), ZSTR_LEN(s1), s2, s2_length);
229+
}
230+
static zend_always_inline bool pt_abi_string_is_interned(const zend_string *s)
231+
{
232+
return GC_FLAGS(s) & IS_STR_INTERNED;
233+
}
234+
#undef zend_string_equals_literal_ci
235+
#define zend_string_equals_literal_ci(str, literal) pt_abi_string_equals_cstr_ci(str, "" literal, sizeof(literal) - 1)
236+
#undef zend_string_starts_with_literal
237+
#define zend_string_starts_with_literal(str, prefix) zend_string_starts_with_cstr(str, "" prefix, sizeof(prefix) - 1)
238+
#undef zend_string_starts_with_literal_ci
239+
#define zend_string_starts_with_literal_ci(str, prefix) zend_string_starts_with_cstr_ci(str, "" prefix, sizeof(prefix) - 1)
240+
#undef ZSTR_IS_INTERNED
241+
#define ZSTR_IS_INTERNED(s) pt_abi_string_is_interned(s)
242+
#undef zend_string_equals_ci
243+
#define zend_string_equals_ci(s1, s2) pt_abi_string_equals_cstr_ci(s1, ZSTR_VAL(s2), ZSTR_LEN(s2))
244+
/* array_init(): a macro up to 8.5, an inline function from 8.6 */
245+
static zend_always_inline void pt_abi_array_init_size(zval *arg, uint32_t size)
246+
{
247+
ZVAL_ARR(arg, zend_new_array(size));
248+
}
249+
#undef array_init
250+
#define array_init(arg) pt_abi_array_init_size((arg), 0)
251+
#undef array_init_size
252+
#define array_init_size(arg, size) pt_abi_array_init_size((arg), (size))
253+
/* 8.5 evaluates the argument once */
254+
#undef Z_TRY_ADDREF_P
255+
#define Z_TRY_ADDREF_P(pz) do { \
256+
zval *_pz = (pz); \
257+
if (Z_REFCOUNTED_P(_pz)) { \
258+
Z_ADDREF_P(_pz); \
259+
} \
260+
} while (0)
261+
#undef Z_TRY_DELREF_P
262+
#define Z_TRY_DELREF_P(pz) do { \
263+
zval *_pz = (pz); \
264+
if (Z_REFCOUNTED_P(_pz)) { \
265+
Z_DELREF_P(_pz); \
266+
} \
267+
} while (0)
268+
219269
#undef EG
220270
#undef CG
221271
#define EG(field) (*pt_abi.eg_##field)

0 commit comments

Comments
 (0)