From 6e6e59666158ea568420ea424bbdb57cc9ada70d Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Thu, 24 Sep 2026 12:14:46 +0800 Subject: [PATCH] feat(compiler): add embed module cache clearing and improve library detection - Register shutdown function to clear reflection resolver caches in embed SAPI - Add test to verify library builds exclude executable process title sources - Remove embed-specific request cache clearing from module shutdown - Add tests for embed binary module ordering behavior - Implement temporary module storage switching for interned strings - Add pre-shutdown functions for AST constant cleanup in embed mode - Update platform detection to search multiple library directories - Add test case for finding embed libraries outside standard lib dir - Modify module initialization to handle persistent/temporary module types correctly --- phpunit/src/CompilerBaseApiTest.php | 41 +++++++------- .../src/EnumCaseAstConstantLifecycleTest.php | 49 +++++++++++++++-- phpunit/src/Platform/PlatformTest.php | 45 ++++++++++++++++ src/Platform/UnixPlatform.php | 53 ++++++++++++------- src/Resolver/Reflection.php | 8 +++ src/Translator.php | 44 +++++++-------- src/compiler.php | 9 ++++ 7 files changed, 184 insertions(+), 65 deletions(-) diff --git a/phpunit/src/CompilerBaseApiTest.php b/phpunit/src/CompilerBaseApiTest.php index 0003eb07..fca5c266 100644 --- a/phpunit/src/CompilerBaseApiTest.php +++ b/phpunit/src/CompilerBaseApiTest.php @@ -1575,6 +1575,19 @@ YAML); } } + public function testLibraryBuildDoesNotCompileExecutableProcessTitleSources(): void + { + $this->setPropertyValue('buildMode', CompilerBase::BUILD_MODE_LIB); + $phpxDir = $this->invokeMethod('getPhpxDir'); + + $sources = $this->invokeMethod('prepareNativeSourceFiles', []); + + $this->assertContains($phpxDir . '/src/misc/typephp_runtime.cc', $sources); + $this->assertContains($phpxDir . '/src/misc/typephp_main.cc', $sources); + $this->assertNotContains($phpxDir . '/src/misc/php_cli_process_title.c', $sources); + $this->assertNotContains($phpxDir . '/src/misc/ps_title.c', $sources); + } + public function testProjectIndependentMiscObjectCacheSurvivesTargetNameChange(): void { $phpxDir = $this->invokeMethod('getPhpxDir'); @@ -1677,24 +1690,16 @@ YAML); $moduleInit = substr($extension, $moduleInitStart, $moduleCleanStart - $moduleInitStart); $moduleClean = substr($extension, $moduleCleanStart); $this->assertStringNotContainsString('slot.reset()', $moduleInit, $mode); - if ($mode === CompilerBase::BUILD_MODE_BIN) { - $this->assertStringContainsString( - 'if (strcmp(sapi_module.name, "embed") == 0)', - $moduleClean, - $mode, - ); - $this->assertStringContainsString( - 'php::setStaticProperty("RequestStaticCache", "values", php::Array{});', - $moduleClean, - $mode, - ); - } else { - $this->assertStringNotContainsString( - 'if (strcmp(sapi_module.name, "embed") == 0)', - $moduleClean, - $mode, - ); - } + $this->assertStringNotContainsString( + 'if (strcmp(sapi_module.name, "embed") == 0)', + $moduleClean, + $mode, + ); + $this->assertStringNotContainsString( + 'php::setStaticProperty("RequestStaticCache", "values", php::Array{});', + $moduleClean, + $mode, + ); $this->assertMatchesRegularExpression( '/PHP_RSHUTDOWN_FUNCTION\([^)]*\)\s*\{\s*' . 'php::request_shutdown\(\);\s*' diff --git a/phpunit/src/EnumCaseAstConstantLifecycleTest.php b/phpunit/src/EnumCaseAstConstantLifecycleTest.php index 3e1b36c5..79ec4942 100644 --- a/phpunit/src/EnumCaseAstConstantLifecycleTest.php +++ b/phpunit/src/EnumCaseAstConstantLifecycleTest.php @@ -35,6 +35,34 @@ final class EnumCaseAstConstantLifecycleTest extends \BaseTest self::assertLessThan($firstRegisterPos, $guardPos, 'the lifecycle guard must precede every class registration'); } + public function testEmbedBinaryLeavesTemporaryModuleOrderingToRuntime(): void + { + $extension = $this->generateExtension( + 'enum-case-class-constant.php', + 'ast_lifecycle_embed', + CompilerBase::BUILD_MODE_BIN, + ); + $minit = $this->sliceFunction($extension, 'PHP_MINIT_FUNCTION', 'PHP_MSHUTDOWN_FUNCTION'); + + self::assertStringNotContainsString( + 'registers enum-case class constants that must be released by MSHUTDOWN', + $minit, + ); + self::assertStringContainsString('register_class_', $minit); + self::assertMatchesRegularExpression( + '/EG\(current_module\)->type = MODULE_PERSISTENT;\s*' + . 'zend_interned_strings_switch_storage\(false\);.*register_class_.*' + . 'zend_interned_strings_switch_storage\(true\);\s*' + . 'EG\(current_module\)->type = MODULE_TEMPORARY;/s', + $minit, + ); + self::assertMatchesRegularExpression( + '/TYPEPHP_EMBED_PRE_SHUTDOWN_FUNCTION\(ast_lifecycle_embed\)\s*\{\s*' + . 'typephp_release_ast_constants_enum_case_class_constant\(\);\s*\}/', + $extension, + ); + } + public function testAstConstantRegistrationIsOrderedAfterEveryFallibleMinitStep(): void { $minit = $this->generateMinitBody('enum-case-class-constant.php', 'ast_lifecycle_order'); @@ -73,26 +101,37 @@ final class EnumCaseAstConstantLifecycleTest extends \BaseTest { $extension = $this->generateExtension('class-constant-codegen.php', 'ast_lifecycle_none'); - self::assertStringNotContainsString('MODULE_TEMPORARY', $extension); + self::assertStringNotContainsString( + 'registers enum-case class constants that must be released by MSHUTDOWN', + $extension, + ); self::assertStringNotContainsString('typephp_release_ast_constants_', $extension); } - private function generateMinitBody(string $fixture, string $target): string + private function generateMinitBody( + string $fixture, + string $target, + string $mode = CompilerBase::BUILD_MODE_EXT, + ): string { return $this->sliceFunction( - $this->generateExtension($fixture, $target), + $this->generateExtension($fixture, $target, $mode), 'PHP_MINIT_FUNCTION', 'PHP_MSHUTDOWN_FUNCTION', ); } - private function generateExtension(string $fixture, string $target): string + private function generateExtension( + string $fixture, + string $target, + string $mode = CompilerBase::BUILD_MODE_EXT, + ): string { global $translator; $compiler = CompilerTest::create(TYPEPHP_ROOT_PATH); $translator = $compiler; - $compiler->setBuildMode(CompilerBase::BUILD_MODE_EXT); + $compiler->setBuildMode($mode); $compiler->setTargetName($target); $source = TYPEPHP_ROOT_PATH . '/phpunit/code/' . $fixture; $compiler->addFiles([$source]); diff --git a/phpunit/src/Platform/PlatformTest.php b/phpunit/src/Platform/PlatformTest.php index 63f9cbb4..b29dfa3e 100644 --- a/phpunit/src/Platform/PlatformTest.php +++ b/phpunit/src/Platform/PlatformTest.php @@ -88,6 +88,51 @@ class PlatformTest extends TestCase } } + public function testLinuxFindsEmbedLibraryOutsidePhpConfigLibDir(): void + { + if (PHP_OS_FAMILY === 'Windows') { + self::markTestSkipped('The fake php-config fixture requires a POSIX shell'); + } + + $sdk = sys_get_temp_dir() . '/typephp-linux-sdk-' . bin2hex(random_bytes(6)); + mkdir($sdk . '/bin', 0777, true); + mkdir($sdk . '/extensions', 0777, true); + mkdir($sdk . '/lib', 0777, true); + touch($sdk . '/lib/libphp.so'); + $phpConfig = $sdk . '/bin/php-config'; + $script = "#!/bin/sh\ncase \"\$1\" in\n" + . " --version) printf '%s\\n' " . escapeshellarg(PHP_VERSION) . ";;\n" + . " --prefix) printf '%s\\n' " . escapeshellarg($sdk) . ";;\n" + . " --lib-dir) printf '%s\\n' " . escapeshellarg($sdk . '/extensions') . ";;\n" + . " --lib-embed) printf '%s\\n' libphp.so;;\n" + . "esac\n"; + file_put_contents($phpConfig, $script); + chmod($phpConfig, 0755); + + try { + $platform = new Linux(); + self::assertSame( + [$sdk . '/extensions', $sdk . '/lib'], + $platform->buildPhpLibPaths($sdk), + ); + self::assertSame( + [ + 'embed' => $sdk . '/lib/libphp.so', + 'static' => null, + 'is_shared' => true, + ], + $platform->detectPhpLibs($sdk), + ); + } finally { + unlink($phpConfig); + unlink($sdk . '/lib/libphp.so'); + rmdir($sdk . '/extensions'); + rmdir($sdk . '/lib'); + rmdir($sdk . '/bin'); + rmdir($sdk); + } + } + /** * 测试 Windows 平台基本功能 */ diff --git a/src/Platform/UnixPlatform.php b/src/Platform/UnixPlatform.php index 8fdad6a2..6e318963 100644 --- a/src/Platform/UnixPlatform.php +++ b/src/Platform/UnixPlatform.php @@ -421,16 +421,26 @@ abstract class UnixPlatform extends PlatformBase protected function resolvePhpLibDir(string $phpDir): ?string { + return $this->resolvePhpLibDirs($phpDir)[0] ?? null; + } + + /** @return list */ + private function resolvePhpLibDirs(string $phpDir): array + { + $libDirs = []; $phpConfig = $this->findPhpConfig($phpDir); if ($phpConfig !== null) { $libDir = $this->getPhpConfigValue($phpConfig, '--lib-dir'); if ($libDir !== null && is_dir($libDir)) { - return rtrim($libDir, '/'); + $libDirs[] = rtrim($libDir, '/'); } } $libDir = rtrim($phpDir, '/') . '/lib'; - return is_dir($libDir) ? $libDir : null; + if (is_dir($libDir) && !in_array($libDir, $libDirs, true)) { + $libDirs[] = $libDir; + } + return $libDirs; } /** @@ -438,8 +448,7 @@ abstract class UnixPlatform extends PlatformBase */ public function buildPhpLibPaths(string $phpDir): array { - $libPath = $this->resolvePhpLibDir($phpDir); - return $libPath === null ? [] : [$libPath]; + return $this->resolvePhpLibDirs($phpDir); } /** @@ -447,8 +456,8 @@ abstract class UnixPlatform extends PlatformBase */ public function detectPhpLibs(string $phpDir): array { - $libPath = $this->resolvePhpLibDir($phpDir); - if ($libPath === null) { + $libPaths = $this->resolvePhpLibDirs($phpDir); + if ($libPaths === []) { throw new \RuntimeException("PHP library directory not found for installation: {$phpDir}"); } @@ -459,23 +468,31 @@ abstract class UnixPlatform extends PlatformBase $phpConfig = $this->findPhpConfig($phpDir); $configuredEmbed = $phpConfig === null ? null : $this->getPhpConfigValue($phpConfig, '--lib-embed'); if ($configuredEmbed !== null) { - $configuredPath = str_starts_with($configuredEmbed, '/') - ? $configuredEmbed - : $libPath . '/' . $configuredEmbed; - if (is_file($configuredPath)) { - if (str_ends_with($configuredPath, '.a')) { - $staticLib = $configuredPath; - } else { - $embedLib = $configuredPath; + $configuredPaths = str_starts_with($configuredEmbed, '/') + ? [$configuredEmbed] + : array_map( + static fn (string $libPath): string => $libPath . '/' . $configuredEmbed, + $libPaths, + ); + foreach ($configuredPaths as $configuredPath) { + if (is_file($configuredPath)) { + if (str_ends_with($configuredPath, '.a')) { + $staticLib = $configuredPath; + } else { + $embedLib = $configuredPath; + } + break; } } } if ($embedLib === null && $staticLib === null) { - $sharedCandidate = $libPath . '/libphp.' . $ext; - $staticCandidate = $libPath . '/libphp.a'; - $embedLib = is_file($sharedCandidate) ? $sharedCandidate : null; - $staticLib = is_file($staticCandidate) ? $staticCandidate : null; + foreach ($libPaths as $libPath) { + $sharedCandidate = $libPath . '/libphp.' . $ext; + $staticCandidate = $libPath . '/libphp.a'; + $embedLib ??= is_file($sharedCandidate) ? $sharedCandidate : null; + $staticLib ??= is_file($staticCandidate) ? $staticCandidate : null; + } } $hasEmbed = $embedLib !== null; diff --git a/src/Resolver/Reflection.php b/src/Resolver/Reflection.php index 55da4cc8..2cdc78cf 100644 --- a/src/Resolver/Reflection.php +++ b/src/Resolver/Reflection.php @@ -16,6 +16,14 @@ class Reflection private static array $classes = []; private static array $interfaces = []; + /** Release reflector wrappers before an embedded compiler unloads its module. */ + public static function clearCaches(): void + { + self::$functions = []; + self::$classes = []; + self::$interfaces = []; + } + public static function isTypePhpExtension(mixed $extensionName): bool { return is_string($extensionName) diff --git a/src/Translator.php b/src/Translator.php index bd8c2bd6..4de3a22c 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -1614,7 +1614,7 @@ CODE; // minit begin $releaseAstConstantFns = array_unique($this->releaseAstConstantFns); $code .= 'PHP_MINIT_FUNCTION(' . $this->getModuleName() . ') {' . PHP_EOL; - if ($releaseAstConstantFns !== []) { + if ($releaseAstConstantFns !== [] && $this->isBuildModeExt()) { // Lifecycle contract for persistent enum-case AST constants: Zend's // internal-class teardown (destroy_zend_class) only tolerates them // after this module's MSHUTDOWN has released them. A temporary @@ -1666,7 +1666,20 @@ CODE; if ($releaseAstConstantFns !== [] && str_contains($registrationCode, 'return FAILURE')) { throw new \LogicException('MINIT must not fail after class registration begins: a FAILURE return would leave persistent enum-case AST constants in the class table with no MSHUTDOWN guaranteed to release them before destroy_zend_class(). Move the fallible step before the first register_class_*() call, or release the AST constants on its failure path.'); } + // A module loaded after request startup normally uses request-local + // interned strings. Internal class metadata (notably enum cases and + // persistent constant ASTs) requires process-lifetime names instead. + // Restrict the storage switch to the one-time MINIT registration + // block; RINIT/RSHUTDOWN do no extra work. + $code .= 'if (type == MODULE_TEMPORARY) {' . PHP_EOL; + $code .= $this->getIndent() . 'EG(current_module)->type = MODULE_PERSISTENT;' . PHP_EOL; + $code .= $this->getIndent() . 'zend_interned_strings_switch_storage(false);' . PHP_EOL; + $code .= '}' . PHP_EOL; $code .= $registrationCode; + $code .= 'if (type == MODULE_TEMPORARY) {' . PHP_EOL; + $code .= $this->getIndent() . 'zend_interned_strings_switch_storage(true);' . PHP_EOL; + $code .= $this->getIndent() . 'EG(current_module)->type = MODULE_TEMPORARY;' . PHP_EOL; + $code .= '}' . PHP_EOL; $code .= 'return SUCCESS;' . PHP_EOL; $code .= '}' . PHP_EOL . PHP_EOL; // minit end @@ -1792,28 +1805,6 @@ CODE; foreach ($this->nativeStaticInitializers as $name => $_) { $code .= $this->escapeGlobalVar($name) . ' = false;' . PHP_EOL; } - if ($this->isBuildModeBin() && (!$this->isSapiBuild() || $this->hasSapi('embed'))) { - // Embed registers the generated module after request startup and - // unloads that temporary module before Zend tears down class - // statics. Release only static array caches which may retain - // Reflection objects pointing into the temporary module. This is - // deliberately skipped for ext/CLI/FPM request shutdown. - $code .= 'if (strcmp(sapi_module.name, "embed") == 0) {' . PHP_EOL; - foreach ($this->symbols->classes() as $classDef) { - if ($classDef->trait) { - continue; - } - foreach ($classDef->properties as $property) { - if (!$property->isStatic() || !$property->arrayInitPlan) { - continue; - } - $code .= $this->getIndent(2) . 'php::setStaticProperty(' - . $this->genCharPtr($classDef->getNamespacedName(false), true) . ', ' - . $this->genCharPtr($property->name) . ', php::Array{});' . PHP_EOL; - } - } - $code .= '}' . PHP_EOL; - } $code .= $this->genRequestArrayDefaultCleanup(); foreach ($this->constants as $name => $const) { if ($const->type !== Type::VAR) { @@ -2006,6 +1997,11 @@ CODE; if (!$this->isSapiBuild() || $this->hasSapi('embed')) { $code .= 'TYPEPHP_EMBED_GET_MODULE_FUNCTION(' . $this->targetName . ') {' . PHP_EOL; $code .= $this->getIndent() . 'return &' . $projectNamespace . '::' . $moduleName . '_module_entry;' . PHP_EOL; + $code .= '}' . PHP_EOL . PHP_EOL; + $code .= 'TYPEPHP_EMBED_PRE_SHUTDOWN_FUNCTION(' . $this->targetName . ') {' . PHP_EOL; + foreach ($releaseAstConstantFns as $releaseAstConstantFn) { + $code .= $this->getIndent() . $releaseAstConstantFn . '();' . PHP_EOL; + } $code .= '}' . PHP_EOL; } } else { @@ -2486,7 +2482,7 @@ CODE; array_push($sourceFiles, ...$this->getEmbeddedRuntimeSources()); } - if (($this->isBuildModeBin() || $this->hasSapi('embed')) + if ($this->isBuildModeBin() && !$this->isWasiTarget() && !$this->isIosTarget()) { $sourceFiles[] = $this->getPhpxDir() . '/src/misc/php_cli_process_title.c'; diff --git a/src/compiler.php b/src/compiler.php index 3fac6578..6909494e 100644 --- a/src/compiler.php +++ b/src/compiler.php @@ -9,6 +9,7 @@ use TypePhp\Build\NativeSourceProjectConfig; use TypePhp\Build\ProjectBuildRunner; use TypePhp\PythonTools\Command as PythonToolsCommand; use TypePhp\Cli\CompletionCommand; +use TypePhp\Resolver\Reflection as ReflectionResolver; function main(int $argc, array $argv): void { @@ -21,6 +22,14 @@ function runCompiler(int $argc, array $argv, CompilerRuntime $runtime): void // memory. The default CLI limit (commonly 128M) is too small for larger builds. ini_set('memory_limit', '-1'); + // The late-loaded embed module is unloaded before PHP's object store. Drop + // cached reflector wrappers while their referenced internal symbols are + // still alive. Embed serves one request per compiler process; never add + // this traversal to an extension/CLI/FPM request-shutdown path. + if (php_sapi_name() === 'embed') { + register_shutdown_function([ReflectionResolver::class, 'clearCaches']); + } + // The PHP entrypoint already loaded Composer's project autoloader in // bin/bootstrap.php. The native binary loads its embedded copy here. if (!$runtime->sourceEntry) {