Skip to content

Commit 2cf40d2

Browse files
ondrejmirtesclaude
andcommitted
turbo-ext: build the shared core and the per-version extension with make SPLIT=1
`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) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017h1GfB7vfJPgwcTHD9Fth6
1 parent 142425d commit 2cf40d2

8 files changed

Lines changed: 213 additions & 28 deletions

File tree

‎turbo-ext/Makefile‎

Lines changed: 73 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -207,17 +207,81 @@ CXXFLAGS += $(PGO_FLAGS)
207207
SOURCES := $(wildcard src/*.cpp) $(wildcard src/parser/*.cpp)
208208
OBJECTS := $(SOURCES:.cpp=.o)
209209

210+
# The shared core and the per-PHP-version extension (README.md, "Shared
211+
# core"). The sources below are the only ones compiled against a PHP
212+
# version's own layout; every other object compiles to the same code against
213+
# each supported version's headers (bin/shared-core/gate-*.sh proves it), so
214+
# it is built once per platform into the core. TerminateHandler.cpp goes into
215+
# both: each links libstdc++ in statically.
216+
SHIM_SOURCES := src/Abi.cpp src/Shadow.cpp src/TrustedTypes.cpp src/main.cpp
217+
SHIM_OBJECTS := $(SHIM_SOURCES:.cpp=.o) src/TerminateHandler.o
218+
CORE_OBJECTS := $(filter-out $(SHIM_SOURCES:.cpp=.o),$(OBJECTS))
219+
CORE_LIB := phpstan_turbo_core.so
220+
221+
# `make SPLIT=1` builds the pair — phpstan_turbo_core.so and a
222+
# phpstan_turbo.so loading it from its own directory — instead of the one
223+
# self-contained phpstan_turbo.so a plain `make` (and the phpize build)
224+
# produces; main.cpp then checks the core's version at startup
225+
ifeq ($(SPLIT),1)
226+
CXXFLAGS += -DPHPSTANTURBO_SPLIT
227+
endif
228+
229+
ifeq ($(UNAME_S),Darwin)
230+
CORE_LINK_FLAGS := -install_name @loader_path/$(CORE_LIB)
231+
EXTENSION_LINK_FLAGS :=
232+
else
233+
# --exclude-libs: the statically folded libstdc++ stays out of the dynamic
234+
# symbol tables of both, so neither binds the other's copy
235+
CORE_LINK_FLAGS := -Wl,-soname,$(CORE_LIB) -Wl,--exclude-libs,ALL
236+
EXTENSION_LINK_FLAGS := -Wl,-rpath,'$$ORIGIN' -Wl,--exclude-libs,ALL
237+
endif
238+
239+
# the symbols a shared object exports — a stripped one keeps only its
240+
# dynamic table on ELF
241+
ifeq ($(UNAME_S),Darwin)
242+
NM_EXPORTS := nm -g
243+
else
244+
NM_EXPORTS := nm -D --defined-only
245+
endif
246+
210247
# the objects alone — what bin/shared-core/gate-*.sh compare between the PHP
211248
# versions' headers
212249
objects: $(OBJECTS)
213250
.PHONY: objects
214251

252+
# a shared object links with undefined symbols allowed (the engine's are
253+
# resolved at load time), so a helper declared but never defined only
254+
# surfaces as a jump to NULL at run time — fail the build instead; the
255+
# extension's may come from the core, which must then export them
256+
define pt_check_undefined
257+
@undefined=$$(nm -u $(1) | grep -E 'pt_[a-z_]+|phpstanturbo' | awk '{print $$NF}' | while read -r sym; do \
258+
if [ -n "$(2)" ] && $(NM_EXPORTS) $(2) 2>/dev/null | awk '$$2 != "U" {print $$3}' | grep -qx "$$sym"; then continue; fi; \
259+
echo "$$sym"; done); \
260+
if [ -n "$$undefined" ]; then echo "undefined extension symbols in $(1):"; echo "$$undefined"; rm -f $(1); exit 1; fi
261+
endef
262+
263+
ifeq ($(SPLIT),1)
264+
phpstan_turbo.so: $(SHIM_OBJECTS) $(CORE_LIB)
265+
$(CXX) `$(PHP_CONFIG) --ldflags` -shared $(LTO_FLAGS) $(LINK_FLAGS) $(EXTENSION_LINK_FLAGS) $(PGO_FLAGS) -o $@ $(SHIM_OBJECTS) $(CORE_LIB)
266+
$(call pt_check_undefined,$@,$(CORE_LIB))
267+
else
215268
phpstan_turbo.so: $(OBJECTS)
216269
$(CXX) `$(PHP_CONFIG) --ldflags` -shared $(LTO_FLAGS) $(LINK_FLAGS) $(PGO_FLAGS) -o $@ $(OBJECTS)
217-
@# a shared object links with undefined symbols allowed (the engine's are
218-
@# resolved at load time), so a helper declared but never defined only
219-
@# surfaces as a jump to NULL at run time — fail the build instead
220-
@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
270+
$(call pt_check_undefined,$@,)
271+
endif
272+
273+
$(CORE_LIB): $(CORE_OBJECTS)
274+
$(CXX) `$(PHP_CONFIG) --ldflags` -shared $(LTO_FLAGS) $(LINK_FLAGS) $(CORE_LINK_FLAGS) $(PGO_FLAGS) -o $@ $(CORE_OBJECTS)
275+
$(call pt_check_undefined,$@,)
276+
277+
# the extension alone, against a $(CORE_LIB) already in place (built for
278+
# another PHP version, by the CI job building the platform's core): the
279+
# per-version CI legs compile only $(SHIM_SOURCES)
280+
extension: $(SHIM_OBJECTS)
281+
@test -f $(CORE_LIB) || { echo "$(CORE_LIB) is missing — build it, or put the platform's core here"; exit 1; }
282+
$(CXX) `$(PHP_CONFIG) --ldflags` -shared $(LTO_FLAGS) $(LINK_FLAGS) $(EXTENSION_LINK_FLAGS) $(PGO_FLAGS) -o phpstan_turbo.so $(SHIM_OBJECTS) $(CORE_LIB)
283+
$(call pt_check_undefined,phpstan_turbo.so,$(CORE_LIB))
284+
.PHONY: extension
221285

222286
# The distributed binaries ship without symbol tables: a quarter of the .so
223287
# (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)
228292
strip:
229293
@test -f phpstan_turbo.so || { echo "phpstan_turbo.so is missing — build it first"; exit 1; }
230294
ifeq ($(UNAME_S),Darwin)
231-
strip -x phpstan_turbo.so
295+
strip -x phpstan_turbo.so $(wildcard $(CORE_LIB))
232296
else
233-
strip --strip-unneeded phpstan_turbo.so
297+
strip --strip-unneeded phpstan_turbo.so $(wildcard $(CORE_LIB))
234298
endif
235299

236300
# 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)
240304
src/%.o: src/%.cpp $(HEADERS)
241305
$(CXX) $(CXXFLAGS) -c -o $@ $<
242306

243-
src/main.o: version.stamp
307+
src/main.o src/Core.o: version.stamp
244308

245309
version.stamp: FORCE
246310
@echo '$(PHPSTANTURBO_VERSION)' | cmp -s - $@ 2>/dev/null || echo '$(PHPSTANTURBO_VERSION)' > $@
@@ -262,7 +326,7 @@ pgo:
262326
$(MAKE) pgo-clean
263327
$(MAKE) PGO_FLAGS="$(PGO_GEN_FLAGS)" phpstan_turbo.so
264328
PGO_DIR="$(PGO_DIR)" bin/pgo-train.sh
265-
rm -f $(OBJECTS) phpstan_turbo.so
329+
rm -f $(OBJECTS) phpstan_turbo.so $(CORE_LIB)
266330
ifeq ($(CXX_IS_CLANG),1)
267331
$(LLVM_PROFDATA) merge -output=$(PGO_DIR)/turbo.profdata $(PGO_DIR)/*.profraw
268332
endif
@@ -273,7 +337,7 @@ endif
273337
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)
274338

275339
pgo-clean:
276-
rm -f $(OBJECTS) phpstan_turbo.so version.stamp
340+
rm -f $(OBJECTS) phpstan_turbo.so $(CORE_LIB) version.stamp
277341
rm -rf $(PGO_DIR)
278342
find src -name '*.gcda' -delete
279343

‎turbo-ext/src/Abi.cpp‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,4 +372,5 @@ void pt_abi_init()
372372
pt_abi.stream_free = abi_stream_free;
373373
pt_abi.stream_read = abi_stream_read;
374374
pt_abi.run_on_fresh_stack = abi_run_on_fresh_stack;
375+
pt_abi.is_true = [](const zval *op) -> bool { return zend_is_true((zval *) op); };
375376
}

‎turbo-ext/src/Core.cpp‎

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,35 @@
1010
#include "support.h"
1111
#include "reg.h"
1212
#include "Engine.h"
13+
#include "version.h"
1314

1415
#include <vector>
1516

16-
PT_CORE_API pt_abi_globals pt_abi;
17+
pt_abi_globals pt_abi;
18+
19+
/* the core's data, for the version-specific library: through functions,
20+
* since a Windows DLL can delay-load functions only */
21+
PT_CORE_API pt_abi_globals *pt_core_abi()
22+
{
23+
return &pt_abi;
24+
}
25+
26+
PT_CORE_API pt_globals_t *pt_core_globals()
27+
{
28+
return &pt_globals;
29+
}
30+
31+
PT_CORE_API const pt_type_op_info *pt_core_type_op_infos()
32+
{
33+
return pt_type_op_infos;
34+
}
35+
36+
/* the version the core was built at — the extension refuses a core of
37+
* another version (main.cpp) */
38+
PT_CORE_API const char *pt_core_version()
39+
{
40+
return PHPSTANTURBO_VERSION;
41+
}
1742

1843
/* the PT_MINIT_REGISTRATION() functions of every file, in name order; a
1944
* constant-initialized pointer, so it is null before any of the file-static

‎turbo-ext/src/TypeOps.h‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,11 @@ struct pt_type_op_info
181181
uint8_t argKinds; /* two bits per argument, argument 0 in the low bits */
182182
};
183183

184-
PT_CORE_API extern const pt_type_op_info pt_type_op_infos[PT_OP_COUNT];
184+
extern const pt_type_op_info pt_type_op_infos[PT_OP_COUNT];
185+
PT_CORE_API const pt_type_op_info *pt_core_type_op_infos();
186+
#ifdef PHPSTANTURBO_ABI_IMPL
187+
#define pt_type_op_infos (pt_core_type_op_infos())
188+
#endif
185189

186190
constexpr uint8_t pt_type_op_kinds(pt_type_op_arg a0 = PT_OPARG_ANY, pt_type_op_arg a1 = PT_OPARG_ANY)
187191
{

‎turbo-ext/src/abi.h‎

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,11 @@
6666
* hidden. The dependency only runs that way — the core never references a
6767
* symbol of the version-specific library. */
6868
#if defined(_WIN32)
69+
#ifdef PHPSTANTURBO_CORE_BUILD
70+
#define PT_CORE_API __declspec(dllexport)
71+
#else
6972
#define PT_CORE_API
73+
#endif
7074
#else
7175
#define PT_CORE_API __attribute__((visibility("default")))
7276
#endif
@@ -169,10 +173,18 @@ struct pt_abi_globals
169173
/* pt_engine_run_on_fresh_stack(): a fiber context, its switch and a
170174
* zend_try around the body (8.6 moved SETJMP to sigsetjmp) */
171175
void (*run_on_fresh_stack)(void (*body)(void *), void *data);
176+
/* zend_is_true() where the declaration cannot be renamed (MSVC, below) */
177+
bool (*is_true)(const zval *op);
172178
};
173179

174-
/* Defined by the shared core, filled by the version-specific library. */
175-
PT_CORE_API extern pt_abi_globals pt_abi;
180+
/* Defined by the shared core, filled by the version-specific library, which
181+
* reaches it through pt_core_abi() (abi.h's PHPSTANTURBO_ABI_IMPL block) */
182+
extern pt_abi_globals pt_abi;
183+
PT_CORE_API pt_abi_globals *pt_core_abi();
184+
PT_CORE_API const char *pt_core_version();
185+
#ifdef PHPSTANTURBO_ABI_IMPL
186+
#define pt_abi (*pt_core_abi())
187+
#endif
176188

177189
/* fills pt_abi from the running engine; first thing MINIT (and, in ZTS
178190
* 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)
583595
#define PT_ABI_SYMBOL_STR(prefix, name) PT_ABI_SYMBOL_STR2(prefix, name)
584596
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));
585597
#define zend_is_true(op) pt_abi_zend_is_true(op)
598+
#else
599+
/* MSVC has no assembler names for declarations: through Abi.cpp, which
600+
* calls the running engine's own */
601+
#define zend_is_true(op) pt_abi.is_true(op)
586602
#endif
587603
#endif
588604

‎turbo-ext/src/main.cpp‎

Lines changed: 66 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,30 +8,19 @@
88
* the boundary-economics rules in turbo-ext/README.md).
99
*/
1010

11+
/* version-specific: the module entry carries the engine's API number */
12+
#define PHPSTANTURBO_ABI_IMPL
1113
#include "support.h"
1214
#include "reg.h"
1315
#include "Engine.h"
16+
#include "version.h"
1417

1518
#ifdef PHP_WIN32
1619
#include <process.h>
1720
#else
1821
#include <unistd.h>
1922
#endif
2023

21-
/* The short SHA of the last commit touching the watched set: baked from git
22-
* by the Makefile (quoted string passed directly), or from the VERSION.txt
23-
* the subsplit workflow commits into phpstan/turbo-ext; "dev" with neither,
24-
* which the enabler rejects. config.w32 and config.m4 pass the bare token as
25-
* PHPSTANTURBO_VERSION_RAW — quote characters do not survive the Windows
26-
* configure-to-nmake pipeline — and it is stringized here. */
27-
#ifdef PHPSTANTURBO_VERSION_RAW
28-
#define PT_VERSION_STR2(x) #x
29-
#define PT_VERSION_STR(x) PT_VERSION_STR2(x)
30-
#define PHPSTANTURBO_VERSION PT_VERSION_STR(PHPSTANTURBO_VERSION_RAW)
31-
#endif
32-
#ifndef PHPSTANTURBO_VERSION
33-
#define PHPSTANTURBO_VERSION "dev"
34-
#endif
3524

3625
/* PHPStanTurbo\Runtime::configure() — cold-path configuration entry point.
3726
* TurboExtensionEnabler passes the generated class map (derived from the
@@ -138,11 +127,63 @@ static void ZEND_FASTCALL runtimeExitImmediately(INTERNAL_FUNCTION_PARAMETERS)
138127
_exit(EG(exit_status));
139128
}
140129

130+
/* Whether the shared core loaded next to this library carries the same
131+
* version: a mismatched pair (a torn update of the distributed files) must
132+
* never run together — the extension then registers nothing and reports
133+
* the mismatch as its version, which TurboExtensionEnabler rejects, so
134+
* PHPStan runs without it. */
135+
static bool pt_core_matches = false;
136+
137+
extern "C" {
138+
extern zend_module_entry phpstan_turbo_module_entry;
139+
}
140+
141+
#ifdef PHP_WIN32
142+
/* Windows searches the dependencies of a DLL loaded by full path in the
143+
* application's directory, not the DLL's own, and PHP loads extensions with
144+
* plain LoadLibrary(): the core is delay-loaded (config.w32) and loaded here
145+
* by its full path, next to this DLL, before PHP calls anything that needs
146+
* it — the delay-load helper then finds it already loaded by name. */
147+
#ifdef ZTS
148+
#define PT_CORE_DLL L"phpstan_turbo_core-zts.dll"
149+
#else
150+
#define PT_CORE_DLL L"phpstan_turbo_core.dll"
151+
#endif
152+
static bool pt_load_core()
153+
{
154+
HMODULE self = NULL;
155+
if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, (LPCWSTR) &pt_load_core, &self)) return false;
156+
wchar_t path[4096];
157+
DWORD length = GetModuleFileNameW(self, path, (DWORD) (sizeof(path) / sizeof(path[0])));
158+
if (length == 0 || length >= sizeof(path) / sizeof(path[0])) return false;
159+
wchar_t *slash = wcsrchr(path, L'\\');
160+
size_t directory = slash != NULL ? (size_t) (slash - path) + 1 : 0;
161+
size_t name = wcslen(PT_CORE_DLL);
162+
if (directory + name + 1 > sizeof(path) / sizeof(path[0])) return false;
163+
wmemcpy(path + directory, PT_CORE_DLL, name + 1);
164+
return LoadLibraryExW(path, NULL, LOAD_WITH_ALTERED_SEARCH_PATH) != NULL;
165+
}
166+
static bool pt_core_loaded = false;
167+
#endif
168+
141169
static PHP_MINIT_FUNCTION(phpstan_turbo)
142170
{
143171
#ifdef ZTS
144172
ZEND_TSRMLS_CACHE_UPDATE();
145173
#endif
174+
#ifdef PHPSTANTURBO_SPLIT
175+
#ifdef PHP_WIN32
176+
if (!pt_core_loaded) {
177+
phpstan_turbo_module_entry.version = "core-missing";
178+
return SUCCESS;
179+
}
180+
#endif
181+
if (strcmp(pt_core_version(), PHPSTANTURBO_VERSION) != 0) {
182+
phpstan_turbo_module_entry.version = "core-mismatch";
183+
return SUCCESS;
184+
}
185+
#endif
186+
pt_core_matches = true;
146187
pt_abi_init();
147188

148189
static const reg::Arg returnsBool = reg::boolArg("");
@@ -163,13 +204,15 @@ static PHP_MINIT_FUNCTION(phpstan_turbo)
163204

164205
static PHP_MSHUTDOWN_FUNCTION(phpstan_turbo)
165206
{
207+
if (!pt_core_matches) return SUCCESS;
166208
pt_core_mshutdown();
167209

168210
return SUCCESS;
169211
}
170212

171213
static PHP_RINIT_FUNCTION(phpstan_turbo)
172214
{
215+
if (!pt_core_matches) return SUCCESS;
173216
#ifdef ZTS
174217
ZEND_TSRMLS_CACHE_UPDATE();
175218
pt_abi_init();
@@ -182,6 +225,7 @@ static PHP_RINIT_FUNCTION(phpstan_turbo)
182225

183226
static PHP_RSHUTDOWN_FUNCTION(phpstan_turbo)
184227
{
228+
if (!pt_core_matches) return SUCCESS;
185229
pt_core_rshutdown();
186230

187231
return SUCCESS;
@@ -212,6 +256,14 @@ zend_module_entry phpstan_turbo_module_entry = {
212256
STANDARD_MODULE_PROPERTIES,
213257
};
214258

259+
#if defined(PHP_WIN32) && defined(PHPSTANTURBO_SPLIT)
260+
ZEND_DLEXPORT zend_module_entry *get_module(void)
261+
{
262+
pt_core_loaded = pt_core_loaded || pt_load_core();
263+
return &phpstan_turbo_module_entry;
264+
}
265+
#else
215266
ZEND_GET_MODULE(phpstan_turbo)
267+
#endif
216268

217269
}

‎turbo-ext/src/support.h‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -518,7 +518,11 @@ struct pt_globals_t {
518518
char trusted_types_prefix[MAXPATHLEN + 16];
519519
};
520520

521-
PT_CORE_API extern pt_globals_t pt_globals;
521+
extern pt_globals_t pt_globals;
522+
PT_CORE_API pt_globals_t *pt_core_globals();
523+
#ifdef PHPSTANTURBO_ABI_IMPL
524+
#define pt_globals (*pt_core_globals())
525+
#endif
522526

523527
#define PT_G(v) (pt_globals.v)
524528

‎turbo-ext/src/version.h‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#ifndef PHPSTANTURBO_VERSION_H
2+
#define PHPSTANTURBO_VERSION_H
3+
4+
/* The short SHA of the last commit touching the watched set: baked from git
5+
* by the Makefile (quoted string passed directly), or from the VERSION.txt
6+
* the subsplit workflow commits into phpstan/turbo-ext; "dev" with neither,
7+
* which the enabler rejects. config.w32 and config.m4 pass the bare token as
8+
* PHPSTANTURBO_VERSION_RAW — quote characters do not survive the Windows
9+
* configure-to-nmake pipeline — and it is stringized here. */
10+
#ifdef PHPSTANTURBO_VERSION_RAW
11+
#define PT_VERSION_STR2(x) #x
12+
#define PT_VERSION_STR(x) PT_VERSION_STR2(x)
13+
#define PHPSTANTURBO_VERSION PT_VERSION_STR(PHPSTANTURBO_VERSION_RAW)
14+
#endif
15+
#ifndef PHPSTANTURBO_VERSION
16+
#define PHPSTANTURBO_VERSION "dev"
17+
#endif
18+
19+
#endif

0 commit comments

Comments
 (0)