Skip to content

Commit 6cc9894

Browse files
committed
feat(build): add static library build system and SAPI integration
- Create CMakeLists.txt for building phpx as a static library with core, std, and typephp components - Add object libraries for mpdecimal and wren-gc third-party dependencies - Configure proper include directories and compile definitions for PHPX build - Integrate typephp_sapi.cc into test suite for CLI argument preparation - Refactor opcode table lifecycle management with separate startup/shutdown phases for process and request levels - Implement runtime hook chaining for compatibility with Zend extensions like OPcache - Add embedded file existence and path kind checking utilities - Create CLI argument preparation functions that prepend project entry file while preserving PHP CLI options - Add comprehensive tests for SAPI argument handling scenarios
1 parent 864aed3 commit 6cc9894

8 files changed

Lines changed: 365 additions & 28 deletions

File tree

‎CMakeLists.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,7 @@ if(BUILD_TESTS)
494494
list(FILTER TEST_FILES EXCLUDE REGEX "/python_bridge\\.cpp$")
495495
endif()
496496
add_executable(phpx-tests ${TEST_FILES})
497+
target_sources(phpx-tests PRIVATE src/misc/typephp_sapi.cc)
497498
target_include_directories(phpx-tests PRIVATE tests/include)
498499

499500
if(IS_WINDOWS)

‎include/typephp_opcode_table.h‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,21 @@ extern "C" {
2424
const typephp_opcode_entry *typephp_project_opcode_table(size_t *count);
2525
const typephp_embedded_file_entry *typephp_project_embedded_file_table(size_t *count);
2626
const char *typephp_project_php_version(void);
27+
const char *typephp_project_entry_file(void);
2728

2829
#ifdef _WIN32
2930
/* Return the bytes of the archive linked as a Windows PE resource. */
3031
const uint8_t *typephp_embedded_archive_data(void);
3132
#endif
3233

3334
void typephp_opcode_table_install(void);
35+
void typephp_opcode_table_startup(void);
36+
void typephp_opcode_table_request_startup(void);
37+
void typephp_opcode_table_request_shutdown(void);
38+
void typephp_opcode_table_shutdown(void);
39+
int typephp_embedded_file_exists(const char *path);
40+
/* 0: missing, 1: file, 2: directory. */
41+
int typephp_embedded_path_kind(const char *path);
3442
void typephp_opcode_table_require(const char *path);
3543
void typephp_opcode_table_uninstall(void);
3644

‎include/typephp_sapi.h‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#ifndef TYPEPHP_SAPI_H
2+
#define TYPEPHP_SAPI_H
3+
4+
#ifdef __cplusplus
5+
extern "C" {
6+
#endif
7+
8+
/*
9+
* Prepare the argv consumed by PHP CLI. The options pointer is PHP CLI's own
10+
* opt_struct table, kept opaque here so this ABI does not expose PHP internals.
11+
* allocated_argv is non-null only when an application entry was inserted.
12+
*/
13+
int typephp_cli_prepare_arguments(int *argc, char ***argv, const void *options, char ***allocated_argv);
14+
void typephp_cli_release_arguments(char **allocated_argv);
15+
16+
#ifdef __cplusplus
17+
}
18+
#endif
19+
20+
#endif

‎sapi-static/CMakeLists.txt‎

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
cmake_minimum_required(VERSION 3.20)
2+
project(phpx_sapi_static LANGUAGES C CXX)
3+
4+
set(PHPX_ROOT "" CACHE PATH "PHPX source directory")
5+
set(PHPX_PHP_PREFIX "" CACHE PATH "Private PHP installation prefix")
6+
if(NOT EXISTS "${PHPX_ROOT}/include/phpx.h")
7+
message(FATAL_ERROR "Invalid PHPX source directory: ${PHPX_ROOT}")
8+
endif()
9+
if(NOT EXISTS "${PHPX_PHP_PREFIX}/include/php/main/php.h")
10+
message(FATAL_ERROR "Invalid private PHP prefix: ${PHPX_PHP_PREFIX}")
11+
endif()
12+
13+
file(GLOB PHPX_CORE_SOURCES "${PHPX_ROOT}/src/core/*.cc")
14+
file(GLOB PHPX_STD_SOURCES "${PHPX_ROOT}/src/std/*.cc")
15+
file(GLOB PHPX_TYPEPHP_SOURCES "${PHPX_ROOT}/src/typephp/*.cc")
16+
list(FILTER PHPX_CORE_SOURCES EXCLUDE REGEX "/[^/]+_nano\\.cc$")
17+
list(FILTER PHPX_TYPEPHP_SOURCES EXCLUDE REGEX "/[^/]+_nano\\.cc$")
18+
19+
file(GLOB MPDEC_C_SOURCES "${PHPX_ROOT}/thirdparty/mpdecimal/libmpdec/*.c")
20+
list(FILTER MPDEC_C_SOURCES EXCLUDE REGEX "bench")
21+
add_library(mpdecimal_obj OBJECT
22+
${MPDEC_C_SOURCES}
23+
"${PHPX_ROOT}/thirdparty/mpdecimal/libmpdec++/decimal.cc"
24+
)
25+
target_include_directories(mpdecimal_obj PUBLIC
26+
"${PHPX_ROOT}/thirdparty/mpdecimal/libmpdec"
27+
"${PHPX_ROOT}/thirdparty/mpdecimal/libmpdec++"
28+
)
29+
target_compile_definitions(mpdecimal_obj PRIVATE CONFIG_64 ANSI HAVE_UINT128_T)
30+
set_target_properties(mpdecimal_obj PROPERTIES C_STANDARD 99 POSITION_INDEPENDENT_CODE ON)
31+
32+
add_library(wren_gc_obj OBJECT "${PHPX_ROOT}/thirdparty/wren-gc/src/wren_gc.c")
33+
target_include_directories(wren_gc_obj PUBLIC "${PHPX_ROOT}/thirdparty/wren-gc/include")
34+
set_target_properties(wren_gc_obj PROPERTIES C_STANDARD 11 POSITION_INDEPENDENT_CODE ON)
35+
36+
add_library(phpx STATIC
37+
${PHPX_CORE_SOURCES}
38+
${PHPX_STD_SOURCES}
39+
${PHPX_TYPEPHP_SOURCES}
40+
$<TARGET_OBJECTS:mpdecimal_obj>
41+
$<TARGET_OBJECTS:wren_gc_obj>
42+
)
43+
set_target_properties(phpx PROPERTIES
44+
OUTPUT_NAME phpx
45+
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib"
46+
CXX_STANDARD 17
47+
CXX_STANDARD_REQUIRED ON
48+
POSITION_INDEPENDENT_CODE ON
49+
)
50+
target_include_directories(phpx PRIVATE
51+
"${PHPX_ROOT}/include"
52+
"${PHPX_ROOT}/src/misc"
53+
"${PHPX_PHP_PREFIX}/include/php"
54+
"${PHPX_PHP_PREFIX}/include/php/main"
55+
"${PHPX_PHP_PREFIX}/include/php/Zend"
56+
"${PHPX_PHP_PREFIX}/include/php/TSRM"
57+
"${PHPX_PHP_PREFIX}/include/php/ext"
58+
"${PHPX_PHP_PREFIX}/include/php/ext/date/lib"
59+
"${PHPX_ROOT}/thirdparty/mpdecimal/libmpdec"
60+
"${PHPX_ROOT}/thirdparty/mpdecimal/libmpdec++"
61+
"${PHPX_ROOT}/thirdparty/wren-gc/include"
62+
)

‎src/misc/typephp_opcode_table.cc‎

Lines changed: 76 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ zend_result (*old_stream_open)(zend_file_handle *) = nullptr;
4646
php_stream_wrapper_ops wrapped_file_ops;
4747
php_stream_wrapper wrapped_file_wrapper;
4848
bool file_wrapper_installed = false;
49+
bool runtime_hooks_installed = false;
50+
bool opcode_table_started = false;
4951

5052
std::string lexical_path(const std::string &path) {
5153
return std::filesystem::path(path).lexically_normal().string();
@@ -184,7 +186,8 @@ int stat_embedded_file(php_stream_wrapper *, const char *filename, int flags,
184186

185187
}
186188

187-
extern "C" void typephp_opcode_table_install(void) {
189+
extern "C" void typephp_opcode_table_startup(void) {
190+
if (opcode_table_started) return;
188191
size_t count = 0;
189192
const typephp_opcode_entry *table = typephp_project_opcode_table(&count);
190193
size_t raw_count = 0;
@@ -200,12 +203,27 @@ extern "C" void typephp_opcode_table_install(void) {
200203
for (size_t i = 0; i < raw_count; ++i) {
201204
raw_entries.emplace(raw_table[i].path, &raw_table[i]);
202205
}
203-
old_compile_file = zend_compile_file;
204-
old_resolve_path = zend_resolve_path;
205-
old_stream_open = zend_stream_open_function;
206-
zend_compile_file = embedded_compile_file;
207-
zend_resolve_path = resolve_path;
208-
zend_stream_open_function = stream_open;
206+
opcode_table_started = true;
207+
}
208+
209+
extern "C" void typephp_opcode_table_request_startup(void) {
210+
if (!opcode_table_started) return;
211+
212+
// Zend extensions such as OPcache install their compiler hooks after
213+
// module MINIT. Chain TypePHP outside those hooks at the first request so
214+
// embedded opcodes are resolved before OPcache tries to stat the synthetic
215+
// zend_file_handle. Keep the process-wide chain for subsequent requests.
216+
if (!runtime_hooks_installed) {
217+
old_compile_file = zend_compile_file;
218+
old_resolve_path = zend_resolve_path;
219+
old_stream_open = zend_stream_open_function;
220+
zend_compile_file = embedded_compile_file;
221+
zend_resolve_path = resolve_path;
222+
zend_stream_open_function = stream_open;
223+
runtime_hooks_installed = true;
224+
}
225+
226+
if (file_wrapper_installed) return;
209227

210228
wrapped_file_ops = *php_plain_files_wrapper.wops;
211229
wrapped_file_ops.stream_opener = open_embedded_file;
@@ -222,6 +240,35 @@ extern "C" void typephp_opcode_table_install(void) {
222240
zend_string_release(protocol);
223241
}
224242

243+
extern "C" void typephp_opcode_table_install(void) {
244+
typephp_opcode_table_startup();
245+
typephp_opcode_table_request_startup();
246+
}
247+
248+
extern "C" int typephp_embedded_file_exists(const char *path) {
249+
if (!path) return 0;
250+
const std::string name(path);
251+
return lookup(entries, name) != nullptr || lookup(raw_entries, name) != nullptr;
252+
}
253+
254+
extern "C" int typephp_embedded_path_kind(const char *path) {
255+
if (!path) return 0;
256+
std::string name(path);
257+
if (lookup(entries, name) || lookup(raw_entries, name)) return 1;
258+
if (name.rfind("file://", 0) == 0) name.erase(0, 7);
259+
name = lexical_path(name);
260+
if (!name.empty() && name.back() != std::filesystem::path::preferred_separator) {
261+
name.push_back(std::filesystem::path::preferred_separator);
262+
}
263+
const auto contains_child = [&name](const auto &table) {
264+
for (const auto &item : table) {
265+
if (item.first.rfind(name, 0) == 0) return true;
266+
}
267+
return false;
268+
};
269+
return contains_child(entries) || contains_child(raw_entries) ? 2 : 0;
270+
}
271+
225272
extern "C" void typephp_opcode_table_require(const char *path) {
226273
const typephp_opcode_entry *entry = lookup(entries, std::string(path));
227274
if (!entry) {
@@ -236,17 +283,7 @@ extern "C" void typephp_opcode_table_require(const char *path) {
236283
zend_destroy_file_handle(&handle);
237284
}
238285

239-
extern "C" void typephp_opcode_table_uninstall(void) {
240-
if (old_compile_file) {
241-
zend_compile_file = old_compile_file;
242-
zend_resolve_path = old_resolve_path;
243-
zend_stream_open_function = old_stream_open;
244-
old_compile_file = nullptr;
245-
old_resolve_path = nullptr;
246-
old_stream_open = nullptr;
247-
}
248-
entries.clear();
249-
raw_entries.clear();
286+
extern "C" void typephp_opcode_table_request_shutdown(void) {
250287
if (file_wrapper_installed) {
251288
zend_string *protocol = zend_string_init("file", sizeof("file") - 1, 0);
252289
php_unregister_url_stream_wrapper_volatile(protocol);
@@ -255,3 +292,24 @@ extern "C" void typephp_opcode_table_uninstall(void) {
255292
file_wrapper_installed = false;
256293
}
257294
}
295+
296+
extern "C" void typephp_opcode_table_shutdown(void) {
297+
if (!opcode_table_started) return;
298+
typephp_opcode_table_request_shutdown();
299+
if (runtime_hooks_installed) {
300+
if (zend_compile_file == embedded_compile_file) zend_compile_file = old_compile_file;
301+
if (zend_resolve_path == resolve_path) zend_resolve_path = old_resolve_path;
302+
if (zend_stream_open_function == stream_open) zend_stream_open_function = old_stream_open;
303+
runtime_hooks_installed = false;
304+
}
305+
old_compile_file = nullptr;
306+
old_resolve_path = nullptr;
307+
old_stream_open = nullptr;
308+
entries.clear();
309+
raw_entries.clear();
310+
opcode_table_started = false;
311+
}
312+
313+
extern "C" void typephp_opcode_table_uninstall(void) {
314+
typephp_opcode_table_shutdown();
315+
}

‎src/misc/typephp_runtime.cc‎

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@ BEGIN_EXTERN_C()
1111
END_EXTERN_C()
1212

1313
static void module_init(zend_module_entry *module) {
14-
if (zend_register_module_ex(module, MODULE_PERSISTENT) == nullptr) {
14+
// php_embed_init() has already started the request. Registering another
15+
// persistent module at this point appends persistent function/class keys
16+
// after Zend's request cleanup boundary. A temporary module is the Zend
17+
// supported form for a module loaded during a request, and its destructor
18+
// removes those symbols before shutdown_executor() walks the tables.
19+
if (zend_register_module_ex(module, MODULE_TEMPORARY) == nullptr) {
1520
zend_error(E_ERROR, "Failed to register module [%s]", module->name);
1621
exit(255);
1722
}
@@ -61,15 +66,14 @@ static void cli_register_file_handles() {
6166
}
6267

6368
static void module_shutdown(zend_module_entry *module) {
64-
/**
65-
* There is a bug in PHP's handling of internal strings. All interned strings are released in the request shutdown
66-
* function, but then released again in the php_embed_shutdown function, resulting in a use-after-free issue. These
67-
* must be manually removed from the module table to prevent double release.
68-
*/
69+
// Removing a temporary module runs its MSHUTDOWN handler and unregisters
70+
// its functions, classes, constants and INI entries before Embed shuts the
71+
// request down.
6972
auto name_len = strlen(module->name);
7073
auto lcname = zend_string_alloc(name_len, module->type == MODULE_PERSISTENT);
7174
zend_str_tolower_copy(ZSTR_VAL(lcname), module->name, name_len);
7275
zend_hash_del(&module_registry, lcname);
76+
zend_string_release(lcname);
7377
}
7478

7579
static zend_module_entry *typephp_runtime_module = nullptr;
@@ -142,10 +146,8 @@ extern "C" void typephp_runtime_stop(void) {
142146
}
143147
zend_end_try();
144148

145-
// Keep this manual cleanup and registry removal. Registering an internal
146-
// module after request startup exposes a PHP Embed double-release bug for
147-
// its persistent strings if the module remains registered through
148-
// php_module_shutdown().
149+
// The TypePHP module was registered after request startup and therefore is
150+
// absent from PHP's precomputed shutdown lists. Run and unload it manually.
149151
typephp_runtime_module->request_shutdown_func(typephp_runtime_module->type, typephp_runtime_module->module_number);
150152
module_shutdown(typephp_runtime_module);
151153
typephp_opcode_table_uninstall();

‎src/misc/typephp_sapi.cc‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#include <typephp_opcode_table.h>
2+
#include <typephp_sapi.h>
3+
4+
extern "C" {
5+
#include "php.h"
6+
#include "php_getopt.h"
7+
}
8+
9+
#include <cstdio>
10+
#include <cstdlib>
11+
#include <cstring>
12+
13+
extern "C" int typephp_cli_prepare_arguments(int *argc, char ***argv, const void *options, char ***allocated_argv) {
14+
*allocated_argv = nullptr;
15+
16+
bool php_cli_mode = false;
17+
if (*argc > 1) {
18+
php_cli_mode = strcmp((*argv)[1], "-") == 0 || strcmp((*argv)[1], "--") == 0;
19+
if (!php_cli_mode) {
20+
char *option_argument = nullptr;
21+
int option_index = 1;
22+
int option = php_getopt(
23+
*argc, *argv, static_cast<const opt_struct *>(options), &option_argument, &option_index, 0, 2);
24+
php_cli_mode = option != EOF;
25+
}
26+
}
27+
if (php_cli_mode) {
28+
return SUCCESS;
29+
}
30+
31+
const char *entry = typephp_project_entry_file();
32+
if (entry == nullptr || *entry == '\0') {
33+
return FAILURE;
34+
}
35+
36+
char **application_argv = static_cast<char **>(calloc(static_cast<size_t>(*argc) + 2, sizeof(char *)));
37+
if (application_argv == nullptr) {
38+
return FAILURE;
39+
}
40+
application_argv[0] = (*argv)[0];
41+
application_argv[1] = const_cast<char *>(entry);
42+
for (int i = 1; i < *argc; ++i) {
43+
application_argv[i + 1] = (*argv)[i];
44+
}
45+
*argv = application_argv;
46+
*allocated_argv = application_argv;
47+
++*argc;
48+
return SUCCESS;
49+
}
50+
51+
extern "C" void typephp_cli_release_arguments(char **allocated_argv) {
52+
free(allocated_argv);
53+
}

0 commit comments

Comments
 (0)