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
master
韩天峰 2 weeks ago
parent ceeb1da3f9
commit 6e6e596661
  1. 25
      phpunit/src/CompilerBaseApiTest.php
  2. 49
      phpunit/src/EnumCaseAstConstantLifecycleTest.php
  3. 45
      phpunit/src/Platform/PlatformTest.php
  4. 39
      src/Platform/UnixPlatform.php
  5. 8
      src/Resolver/Reflection.php
  6. 44
      src/Translator.php
  7. 9
      src/compiler.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 public function testProjectIndependentMiscObjectCacheSurvivesTargetNameChange(): void
{ {
$phpxDir = $this->invokeMethod('getPhpxDir'); $phpxDir = $this->invokeMethod('getPhpxDir');
@ -1677,24 +1690,16 @@ YAML);
$moduleInit = substr($extension, $moduleInitStart, $moduleCleanStart - $moduleInitStart); $moduleInit = substr($extension, $moduleInitStart, $moduleCleanStart - $moduleInitStart);
$moduleClean = substr($extension, $moduleCleanStart); $moduleClean = substr($extension, $moduleCleanStart);
$this->assertStringNotContainsString('slot.reset()', $moduleInit, $mode); $this->assertStringNotContainsString('slot.reset()', $moduleInit, $mode);
if ($mode === CompilerBase::BUILD_MODE_BIN) { $this->assertStringNotContainsString(
$this->assertStringContainsString(
'if (strcmp(sapi_module.name, "embed") == 0)', 'if (strcmp(sapi_module.name, "embed") == 0)',
$moduleClean, $moduleClean,
$mode, $mode,
); );
$this->assertStringContainsString(
'php::setStaticProperty("RequestStaticCache", "values", php::Array{});',
$moduleClean,
$mode,
);
} else {
$this->assertStringNotContainsString( $this->assertStringNotContainsString(
'if (strcmp(sapi_module.name, "embed") == 0)', 'php::setStaticProperty("RequestStaticCache", "values", php::Array{});',
$moduleClean, $moduleClean,
$mode, $mode,
); );
}
$this->assertMatchesRegularExpression( $this->assertMatchesRegularExpression(
'/PHP_RSHUTDOWN_FUNCTION\([^)]*\)\s*\{\s*' '/PHP_RSHUTDOWN_FUNCTION\([^)]*\)\s*\{\s*'
. 'php::request_shutdown\(\);\s*' . 'php::request_shutdown\(\);\s*'

@ -35,6 +35,34 @@ final class EnumCaseAstConstantLifecycleTest extends \BaseTest
self::assertLessThan($firstRegisterPos, $guardPos, 'the lifecycle guard must precede every class registration'); 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 public function testAstConstantRegistrationIsOrderedAfterEveryFallibleMinitStep(): void
{ {
$minit = $this->generateMinitBody('enum-case-class-constant.php', 'ast_lifecycle_order'); $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'); $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); 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( return $this->sliceFunction(
$this->generateExtension($fixture, $target), $this->generateExtension($fixture, $target, $mode),
'PHP_MINIT_FUNCTION', 'PHP_MINIT_FUNCTION',
'PHP_MSHUTDOWN_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; global $translator;
$compiler = CompilerTest::create(TYPEPHP_ROOT_PATH); $compiler = CompilerTest::create(TYPEPHP_ROOT_PATH);
$translator = $compiler; $translator = $compiler;
$compiler->setBuildMode(CompilerBase::BUILD_MODE_EXT); $compiler->setBuildMode($mode);
$compiler->setTargetName($target); $compiler->setTargetName($target);
$source = TYPEPHP_ROOT_PATH . '/phpunit/code/' . $fixture; $source = TYPEPHP_ROOT_PATH . '/phpunit/code/' . $fixture;
$compiler->addFiles([$source]); $compiler->addFiles([$source]);

@ -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 平台基本功能 * 测试 Windows 平台基本功能
*/ */

@ -421,16 +421,26 @@ abstract class UnixPlatform extends PlatformBase
protected function resolvePhpLibDir(string $phpDir): ?string protected function resolvePhpLibDir(string $phpDir): ?string
{ {
return $this->resolvePhpLibDirs($phpDir)[0] ?? null;
}
/** @return list<string> */
private function resolvePhpLibDirs(string $phpDir): array
{
$libDirs = [];
$phpConfig = $this->findPhpConfig($phpDir); $phpConfig = $this->findPhpConfig($phpDir);
if ($phpConfig !== null) { if ($phpConfig !== null) {
$libDir = $this->getPhpConfigValue($phpConfig, '--lib-dir'); $libDir = $this->getPhpConfigValue($phpConfig, '--lib-dir');
if ($libDir !== null && is_dir($libDir)) { if ($libDir !== null && is_dir($libDir)) {
return rtrim($libDir, '/'); $libDirs[] = rtrim($libDir, '/');
} }
} }
$libDir = rtrim($phpDir, '/') . '/lib'; $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 public function buildPhpLibPaths(string $phpDir): array
{ {
$libPath = $this->resolvePhpLibDir($phpDir); return $this->resolvePhpLibDirs($phpDir);
return $libPath === null ? [] : [$libPath];
} }
/** /**
@ -447,8 +456,8 @@ abstract class UnixPlatform extends PlatformBase
*/ */
public function detectPhpLibs(string $phpDir): array public function detectPhpLibs(string $phpDir): array
{ {
$libPath = $this->resolvePhpLibDir($phpDir); $libPaths = $this->resolvePhpLibDirs($phpDir);
if ($libPath === null) { if ($libPaths === []) {
throw new \RuntimeException("PHP library directory not found for installation: {$phpDir}"); throw new \RuntimeException("PHP library directory not found for installation: {$phpDir}");
} }
@ -459,23 +468,31 @@ abstract class UnixPlatform extends PlatformBase
$phpConfig = $this->findPhpConfig($phpDir); $phpConfig = $this->findPhpConfig($phpDir);
$configuredEmbed = $phpConfig === null ? null : $this->getPhpConfigValue($phpConfig, '--lib-embed'); $configuredEmbed = $phpConfig === null ? null : $this->getPhpConfigValue($phpConfig, '--lib-embed');
if ($configuredEmbed !== null) { if ($configuredEmbed !== null) {
$configuredPath = str_starts_with($configuredEmbed, '/') $configuredPaths = str_starts_with($configuredEmbed, '/')
? $configuredEmbed ? [$configuredEmbed]
: $libPath . '/' . $configuredEmbed; : array_map(
static fn (string $libPath): string => $libPath . '/' . $configuredEmbed,
$libPaths,
);
foreach ($configuredPaths as $configuredPath) {
if (is_file($configuredPath)) { if (is_file($configuredPath)) {
if (str_ends_with($configuredPath, '.a')) { if (str_ends_with($configuredPath, '.a')) {
$staticLib = $configuredPath; $staticLib = $configuredPath;
} else { } else {
$embedLib = $configuredPath; $embedLib = $configuredPath;
} }
break;
}
} }
} }
if ($embedLib === null && $staticLib === null) { if ($embedLib === null && $staticLib === null) {
foreach ($libPaths as $libPath) {
$sharedCandidate = $libPath . '/libphp.' . $ext; $sharedCandidate = $libPath . '/libphp.' . $ext;
$staticCandidate = $libPath . '/libphp.a'; $staticCandidate = $libPath . '/libphp.a';
$embedLib = is_file($sharedCandidate) ? $sharedCandidate : null; $embedLib ??= is_file($sharedCandidate) ? $sharedCandidate : null;
$staticLib = is_file($staticCandidate) ? $staticCandidate : null; $staticLib ??= is_file($staticCandidate) ? $staticCandidate : null;
}
} }
$hasEmbed = $embedLib !== null; $hasEmbed = $embedLib !== null;

@ -16,6 +16,14 @@ class Reflection
private static array $classes = []; private static array $classes = [];
private static array $interfaces = []; 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 public static function isTypePhpExtension(mixed $extensionName): bool
{ {
return is_string($extensionName) return is_string($extensionName)

@ -1614,7 +1614,7 @@ CODE;
// minit begin // minit begin
$releaseAstConstantFns = array_unique($this->releaseAstConstantFns); $releaseAstConstantFns = array_unique($this->releaseAstConstantFns);
$code .= 'PHP_MINIT_FUNCTION(' . $this->getModuleName() . ') {' . PHP_EOL; $code .= 'PHP_MINIT_FUNCTION(' . $this->getModuleName() . ') {' . PHP_EOL;
if ($releaseAstConstantFns !== []) { if ($releaseAstConstantFns !== [] && $this->isBuildModeExt()) {
// Lifecycle contract for persistent enum-case AST constants: Zend's // Lifecycle contract for persistent enum-case AST constants: Zend's
// internal-class teardown (destroy_zend_class) only tolerates them // internal-class teardown (destroy_zend_class) only tolerates them
// after this module's MSHUTDOWN has released them. A temporary // after this module's MSHUTDOWN has released them. A temporary
@ -1666,7 +1666,20 @@ CODE;
if ($releaseAstConstantFns !== [] && str_contains($registrationCode, 'return FAILURE')) { 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.'); 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 .= $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 .= 'return SUCCESS;' . PHP_EOL;
$code .= '}' . PHP_EOL . PHP_EOL; $code .= '}' . PHP_EOL . PHP_EOL;
// minit end // minit end
@ -1792,28 +1805,6 @@ CODE;
foreach ($this->nativeStaticInitializers as $name => $_) { foreach ($this->nativeStaticInitializers as $name => $_) {
$code .= $this->escapeGlobalVar($name) . ' = false;' . PHP_EOL; $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(); $code .= $this->genRequestArrayDefaultCleanup();
foreach ($this->constants as $name => $const) { foreach ($this->constants as $name => $const) {
if ($const->type !== Type::VAR) { if ($const->type !== Type::VAR) {
@ -2006,6 +1997,11 @@ CODE;
if (!$this->isSapiBuild() || $this->hasSapi('embed')) { if (!$this->isSapiBuild() || $this->hasSapi('embed')) {
$code .= 'TYPEPHP_EMBED_GET_MODULE_FUNCTION(' . $this->targetName . ') {' . PHP_EOL; $code .= 'TYPEPHP_EMBED_GET_MODULE_FUNCTION(' . $this->targetName . ') {' . PHP_EOL;
$code .= $this->getIndent() . 'return &' . $projectNamespace . '::' . $moduleName . '_module_entry;' . 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; $code .= '}' . PHP_EOL;
} }
} else { } else {
@ -2486,7 +2482,7 @@ CODE;
array_push($sourceFiles, ...$this->getEmbeddedRuntimeSources()); array_push($sourceFiles, ...$this->getEmbeddedRuntimeSources());
} }
if (($this->isBuildModeBin() || $this->hasSapi('embed')) if ($this->isBuildModeBin()
&& !$this->isWasiTarget() && !$this->isWasiTarget()
&& !$this->isIosTarget()) { && !$this->isIosTarget()) {
$sourceFiles[] = $this->getPhpxDir() . '/src/misc/php_cli_process_title.c'; $sourceFiles[] = $this->getPhpxDir() . '/src/misc/php_cli_process_title.c';

@ -9,6 +9,7 @@ use TypePhp\Build\NativeSourceProjectConfig;
use TypePhp\Build\ProjectBuildRunner; use TypePhp\Build\ProjectBuildRunner;
use TypePhp\PythonTools\Command as PythonToolsCommand; use TypePhp\PythonTools\Command as PythonToolsCommand;
use TypePhp\Cli\CompletionCommand; use TypePhp\Cli\CompletionCommand;
use TypePhp\Resolver\Reflection as ReflectionResolver;
function main(int $argc, array $argv): void 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. // memory. The default CLI limit (commonly 128M) is too small for larger builds.
ini_set('memory_limit', '-1'); 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 // The PHP entrypoint already loaded Composer's project autoloader in
// bin/bootstrap.php. The native binary loads its embedded copy here. // bin/bootstrap.php. The native binary loads its embedded copy here.
if (!$runtime->sourceEntry) { if (!$runtime->sourceEntry) {

Loading…
Cancel
Save