diff --git a/.github/actions/unix-arm64-build/action.yml b/.github/actions/unix-arm64-build/action.yml index a53caff1..ebdfe575 100644 --- a/.github/actions/unix-arm64-build/action.yml +++ b/.github/actions/unix-arm64-build/action.yml @@ -30,7 +30,7 @@ runs: php-version: ${{ inputs.php-version }} coverage: none extensions: mbstring - ini-values: opcache.jit=0, precision=17, memory_limit=4G, error_reporting=E_ERROR|E_WARNING, display_errors=1, display_startup_errors=1, log_errors=0 + ini-values: precision=17, memory_limit=4G, error_reporting=E_ERROR|E_WARNING, display_errors=1, display_startup_errors=1, log_errors=0 tools: composer:v2 env: fail-fast: true diff --git a/.github/workflows/linux-x64.yml b/.github/workflows/linux-x64.yml index 6ef454f3..678c57b0 100644 --- a/.github/workflows/linux-x64.yml +++ b/.github/workflows/linux-x64.yml @@ -364,7 +364,7 @@ jobs: php-version: ${{ matrix.php }} coverage: none extensions: curl, redis, mbstring, ffi - ini-values: ffi.enable=1, phpy.enable_operator_overloading=0, opcache.jit=0, precision=17, memory_limit=4G, error_reporting=E_ERROR|E_WARNING, display_errors=1, display_startup_errors=1, log_errors=0 + ini-values: ffi.enable=1, phpy.enable_operator_overloading=0, precision=17, memory_limit=4G, error_reporting=E_ERROR|E_WARNING, display_errors=1, display_startup_errors=1, log_errors=0 tools: composer:v2 env: fail-fast: true diff --git a/.gitignore b/.gitignore index 8899f8fc..a2b60ae3 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,6 @@ /projects /.php-cs-fixer.cache /.phpunit.cache -/__pycache__ /composer.lock *.o *.dll @@ -23,6 +22,7 @@ *.exp *.class *.wasm +__pycache__ /tpc tests/**/*.diff tests/**/*.exp diff --git a/phpunit/src/CompilerBaseApiTest.php b/phpunit/src/CompilerBaseApiTest.php index f57d0824..9a1ff572 100644 --- a/phpunit/src/CompilerBaseApiTest.php +++ b/phpunit/src/CompilerBaseApiTest.php @@ -1213,7 +1213,7 @@ YAML); $this->assertContains('/usr/local/lib', $libraryPaths); } - public function testRequestShutdownClearsRuntimeMapsInEveryBuildMode(): void + public function testRequestLifecycleOwnsRuntimeCacheStorageInEveryBuildMode(): void { global $translator; foreach ([CompilerBase::BUILD_MODE_BIN, CompilerBase::BUILD_MODE_LIB, CompilerBase::BUILD_MODE_EXT] as $mode) { @@ -1227,21 +1227,24 @@ YAML); $compiler->convertFile($testFile); $code = file_get_contents($compiler->genExtension()); - $this->assertStringContainsString('#include ', $code, $mode); + $this->assertStringContainsString('struct php_request_cache_storage final {', $code, $mode); $this->assertStringContainsString( - 'std::memset(php_func_map, 0, sizeof(php_func_map));', + 'static THREAD_LOCAL php_request_cache_storage *php_request_cache = nullptr;', $code, $mode, ); $this->assertStringContainsString( - 'std::memset(php_class_map, 0, sizeof(php_class_map));', + 'php_request_cache = new (std::nothrow) php_request_cache_storage{};', $code, $mode, ); + $this->assertStringContainsString('delete php_request_cache;', $code, $mode); + $this->assertStringContainsString('php_request_cache = nullptr;', $code, $mode); + $this->assertStringNotContainsString('std::memset(php_func_map', $code, $mode); + $this->assertStringNotContainsString('std::memset(php_class_map', $code, $mode); + $this->assertStringNotContainsString('static THREAD_LOCAL zend_function *php_func_map[', $code, $mode); + $this->assertStringNotContainsString('static THREAD_LOCAL zend_class_entry *php_class_map[', $code, $mode); $this->assertStringNotContainsString('php_property_map', $code, $mode); - $this->assertStringNotContainsString('func_map = {}', $code, $mode); - $this->assertStringNotContainsString('class_map = {}', $code, $mode); - $this->assertStringNotContainsString('property_map = {}', $code, $mode); } } @@ -1284,7 +1287,8 @@ YAML); $this->assertStringNotContainsString('slot.reset()', $moduleInit, $mode); $this->assertMatchesRegularExpression( '/PHP_RSHUTDOWN_FUNCTION\([^)]*\)\s*\{\s*' - . 'for \(auto &slot : php_property_cache_map\) \{\s*slot\.reset\(\);\s*\}\s*' + . 'delete php_request_cache;\s*' + . 'php_request_cache = nullptr;\s*' . 'php::request_shutdown\(\);/s', $extension, $mode, diff --git a/phpunit/src/PropertyCacheCodegenTest.php b/phpunit/src/PropertyCacheCodegenTest.php index 4ce38896..576c9037 100644 --- a/phpunit/src/PropertyCacheCodegenTest.php +++ b/phpunit/src/PropertyCacheCodegenTest.php @@ -1,9 +1,65 @@ setBuildMode(CompilerBase::BUILD_MODE_EXT); + $compiler->setTargetName('request_cache_storage'); + $source = TYPEPHP_ROOT_PATH . '/phpunit/code/property-cache-sites.php'; + $compiler->addFiles([$source]); + $compiler->prepareFile($source); + $compiler->convertFile($source); + $extension = file_get_contents($compiler->genExtension()); + + self::assertIsString($extension); + self::assertStringContainsString('#include ', $extension); + self::assertStringContainsString('struct php_request_cache_storage final {', $extension); + self::assertStringContainsString('zend_class_entry *class_map[', $extension); + self::assertStringContainsString('zend_function *func_map[', $extension); + self::assertStringContainsString('php::PropertyCacheSlot property_cache_map[', $extension); + self::assertStringContainsString( + 'static THREAD_LOCAL php_request_cache_storage *php_request_cache = nullptr;', + $extension, + ); + self::assertStringContainsString( + 'php_request_cache = new (std::nothrow) php_request_cache_storage{};', + $extension, + ); + self::assertStringContainsString('delete php_request_cache;', $extension); + self::assertStringContainsString('php_request_cache = nullptr;', $extension); + + self::assertStringNotContainsString('static THREAD_LOCAL zend_class_entry *php_class_map[', $extension); + self::assertStringNotContainsString('static THREAD_LOCAL zend_function *php_func_map[', $extension); + self::assertStringNotContainsString( + 'static THREAD_LOCAL php::PropertyCacheSlot php_property_cache_map[', + $extension, + ); + self::assertStringNotContainsString('std::memset(php_class_map', $extension); + self::assertStringNotContainsString('std::memset(php_func_map', $extension); + + // Module-lifetime caches remain statically allocated and do not enter TLS. + self::assertStringContainsString( + 'static php::PersistentCacheSlot php_persistent_class_map[', + $extension, + ); + self::assertStringContainsString( + 'static php::PersistentCacheSlot php_persistent_func_map[', + $extension, + ); + self::assertStringContainsString( + 'static php::PersistentCacheSlot php_persistent_property_map[', + $extension, + ); + } + public function testOnlyStaticallyNamedPropertySitesReceiveZendCacheSlots(): void { global $translator; diff --git a/src/Translator.php b/src/Translator.php index 8e1a482b..b2997287 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -871,6 +871,9 @@ class Translator extends Preprocessor $this->indentLevel++; $code = $this->genIncludeHeaderFiles(); + // Only the generated module entry allocates request-cache storage. + // Keep out of the shared PCH dependency set used by every source. + $code .= '#include ' . PHP_EOL; if ($this->isBuildModeEmbed()) { $code .= '#include ' . PHP_EOL; @@ -911,16 +914,29 @@ class Translator extends Preprocessor $code .= 'zend_class_entry *' . $ce . ';' . PHP_EOL; } + $code .= "// request-local caches \n"; + // Keep only one pointer in ELF TLS. Large generated TLS arrays can + // exceed AArch64's static TLS addressing range when Opcache JIT is + // enabled, while the cache entries themselves have request lifetime. + // Ensure each array has at least one element to remain valid C++ when + // a project does not use that cache kind. + $code .= 'struct php_request_cache_storage final {' . PHP_EOL; + $code .= $this->getIndent() . 'zend_class_entry *' . self::CLASS_MAP . '[' + . max(1, count($this->classMap)) . ']{};' . PHP_EOL; + $code .= $this->getIndent() . 'zend_function *' . self::FUNC_MAP . '[' + . max(1, count($this->funcMap)) . ']{};' . PHP_EOL; + $code .= $this->getIndent() . 'php::PropertyCacheSlot property_cache_map[' + . max(1, $this->propertyAccessCacheIndex) . ']{};' . PHP_EOL; + $code .= '};' . PHP_EOL; + $code .= 'static THREAD_LOCAL php_request_cache_storage *php_request_cache = nullptr;' . PHP_EOL; + $code .= "// class entry \n"; - // Ensure the array has at least one element to avoid C/C++ compile errors. - $code .= 'static THREAD_LOCAL zend_class_entry *' . self::PREFIX . self::CLASS_MAP . '[' . max(1, count($this->classMap)) . '];' . PHP_EOL; // Internal/compiled symbols have module lifetime. They are initialized // lazily after PHP startup, so disable_functions/disable_classes have // already finalized the runtime tables. ZTS publishes them atomically. $code .= 'static php::PersistentCacheSlot ' . self::PREFIX . self::PERSISTENT_CLASS_MAP . '[' . max(1, count($this->persistentClassMap)) . ']{};' . PHP_EOL; $code .= "// func \n"; - $code .= 'static THREAD_LOCAL zend_function *' . self::PREFIX . self::FUNC_MAP . '[' . max(1, count($this->funcMap)) . '];' . PHP_EOL; $code .= 'static php::PersistentCacheSlot ' . self::PREFIX . self::PERSISTENT_FUNC_MAP . '[' . max(1, count($this->persistentFuncMap)) . ']{};' . PHP_EOL; $code .= $this->genPythonModuleStorage(); @@ -929,38 +945,35 @@ class Translator extends Preprocessor // No dynamic propMap: the property offset cache only covers declared // properties of compiled/built-in classes (see getPropertyId). $code .= 'static php::PersistentCacheSlot ' . self::PREFIX . self::PERSISTENT_PROP_MAP . '[' . max(1, count($this->persistentPropMap)) . ']{};' . PHP_EOL; - // Zend's object handlers use three adjacent pointers as one cache - // entry. Keep these slots request-local: a named access may receive a - // class provided by an ordinary PHP script whose CE is not persistent. - $code .= 'static THREAD_LOCAL php::PropertyCacheSlot ' . self::PREFIX . 'property_cache_map[' - . max(1, $this->propertyAccessCacheIndex) . ']{};' . PHP_EOL; - $code .= "// functions \n"; $code .= <<<'CODE' zend_class_entry *get_class(RequestClassId class_id, const php::Str &class_name) { const auto index = static_cast(class_id); - if (UNEXPECTED(php_class_map[index] == nullptr)) { - php_class_map[index] = php::getClassEntrySafe(class_name); + auto &slot = php_request_cache->class_map[index]; + if (UNEXPECTED(slot == nullptr)) { + slot = php::getClassEntrySafe(class_name); } - return php_class_map[index]; + return slot; } zend_function *get_func(RequestFuncId func_id, const php::Str &func_name) { const auto index = static_cast(func_id); - if (UNEXPECTED(php_func_map[index] == nullptr)) { - php_func_map[index] = php::getFunction(func_name); + auto &slot = php_request_cache->func_map[index]; + if (UNEXPECTED(slot == nullptr)) { + slot = php::getFunction(func_name); } - return php_func_map[index]; + return slot; } zend_function *get_method(RequestFuncId func_id, const php::Str &method_name, RequestClassId class_id, const php::Str &class_name) { const auto index = static_cast(func_id); - if (UNEXPECTED(php_func_map[index] == nullptr)) { + auto &slot = php_request_cache->func_map[index]; + if (UNEXPECTED(slot == nullptr)) { auto ce = get_class(class_id, class_name); - php_func_map[index] = php::getMethod(ce, method_name); + slot = php::getMethod(ce, method_name); } - return php_func_map[index]; + return slot; } zend_class_entry *get_persistent_class(PersistentClassId class_id, const php::Str &class_name) { @@ -994,7 +1007,7 @@ uint32_t get_persistent_prop(PersistentPropertyId prop_id, const php::Str &prop_ } php::PropertyCacheSlot &get_property_cache(PropertyCacheId cache_id) { - return php_property_cache_map[static_cast(cache_id)]; + return php_request_cache->property_cache_map[static_cast(cache_id)]; } CODE; $code .= "\n\n"; @@ -1263,19 +1276,21 @@ CODE; } } - // User-code symbols have request lifetime regardless of the build mode. - // Embedded/library hosts may start more than one Zend request in the - // same process, so never let these pointers survive RSHUTDOWN. - // Internal/compiled symbols remain in the module-lifetime persistent maps. - $code .= 'std::memset(' . self::PREFIX . self::FUNC_MAP . ', 0, sizeof(' . self::PREFIX . self::FUNC_MAP . '));' . PHP_EOL; - $code .= 'std::memset(' . self::PREFIX . self::CLASS_MAP . ', 0, sizeof(' . self::PREFIX . self::CLASS_MAP . '));' . PHP_EOL; - $code .= '}' . PHP_EOL . PHP_EOL; // module_clean end $moduleName = $this->getModuleName(); // rinit begin $code .= 'PHP_RINIT_FUNCTION(' . $moduleName . ') {' . PHP_EOL; + $code .= 'if (UNEXPECTED(php_request_cache != nullptr)) {' . PHP_EOL; + $code .= $this->getIndent() . 'php_error_docref(nullptr, E_WARNING, "TypePHP request cache is already initialized");' . PHP_EOL; + $code .= $this->getIndent() . 'return FAILURE;' . PHP_EOL; + $code .= '}' . PHP_EOL; + $code .= 'php_request_cache = new (std::nothrow) php_request_cache_storage{};' . PHP_EOL; + $code .= 'if (UNEXPECTED(php_request_cache == nullptr)) {' . PHP_EOL; + $code .= $this->getIndent() . 'php_error_docref(nullptr, E_WARNING, "Unable to allocate TypePHP request cache");' . PHP_EOL; + $code .= $this->getIndent() . 'return FAILURE;' . PHP_EOL; + $code .= '}' . PHP_EOL; $code .= 'php::request_init();' . PHP_EOL; $code .= 'module_init();' . PHP_EOL; @@ -1311,9 +1326,8 @@ CODE; $code .= <<