From be77cfaab17a73f75255aa0d1ea1b642ae67cdee Mon Sep 17 00:00:00 2001 From: rango Date: Wed, 29 Apr 2026 18:13:18 +0800 Subject: [PATCH] =?UTF-8?q?feat(compiler):=20=E5=A2=9E=E5=BC=BA=20PHP=20?= =?UTF-8?q?=E7=BC=96=E8=AF=91=E5=99=A8=E5=AF=B9=20Windows=20=E5=B9=B3?= =?UTF-8?q?=E5=8F=B0=E5=92=8C=20ZTS=20=E7=89=88=E6=9C=AC=E7=9A=84=E6=94=AF?= =?UTF-8?q?=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 检测 PHP 是否为线程安全版本(ZTS)并相应配置编译选项 - 添加 PHPX_HOME 环境变量支持用于自定义 phpx 目录 - 优化 Windows 平台下 PHP 路径检测逻辑,支持从 SDK/include 目录读取头文件 - 实现 Windows 平台下 PHP 库文件的多路径查找策略(SDK/lib, lib, 根目录) - 添加 Windows 编译时必需的宏定义(ZEND_WIN32, PHP_WIN32, ZTS 等) - 修复跨平台路径分隔符处理问题,统一 Windows 使用反斜杠 - 增强错误处理和用户提示信息 - 新增 Preprocessor 类用于源文件预处理和依赖分析 --- src/Php/CompilerBase.php | 210 +++++++++++++++++++++++++++++++++------ src/Php/Preprocessor.php | 12 ++- src/Php/Translator.php | 152 +++++++++++++++++++++++++--- 3 files changed, 325 insertions(+), 49 deletions(-) diff --git a/src/Php/CompilerBase.php b/src/Php/CompilerBase.php index 7bd97dad..311ce6a3 100644 --- a/src/Php/CompilerBase.php +++ b/src/Php/CompilerBase.php @@ -253,6 +253,7 @@ class CompilerBase extends \PhpAot\Core\Translator protected bool $forTest = false; protected Parser $parser; protected PrettyPrinter $printer; + protected bool $isPhpZts = false; // PHP 是否为线程安全版本 /** * 在预处理阶段获取所有类的方法名称,检测子类和父类中存在的同名方法,解决动态绑定方法调用的问题 @@ -287,6 +288,9 @@ class CompilerBase extends \PhpAot\Core\Translator // 检测操作系统并设置编译器 $this->detectPlatform(); + + // 检测 PHP 是否为线程安全版本(ZTS) + $this->isPhpZts = defined('PHP_ZTS') && PHP_ZTS === 1; } protected function detectPlatform(): void @@ -307,6 +311,13 @@ class CompilerBase extends \PhpAot\Core\Translator protected function getPhpxDir(): string { + // 优先使用环境变量 PHPX_HOME + $phpxDir = getenv('PHPX_HOME'); + if ($phpxDir && is_dir($phpxDir)) { + return rtrim($phpxDir, '\\/'); + } + + // 默认路径 return $this->rootPath . '/vendor/swoole/phpx'; } @@ -323,13 +334,13 @@ class CompilerBase extends \PhpAot\Core\Translator public function getPhpDir(): string { if ($this->isWindows()) { - // Windows 下尝试从环境变量或注册表获取 PHP 路径 + // Windows 下尝试从环境变量获取 PHP 路径 $phpDir = getenv('PHP_HOME'); if ($phpDir && is_dir($phpDir)) { return rtrim($phpDir, '\\/'); } - // 尝试从 php.exe 路径推断 + // 尝试从 php.exe 路径推断(使用 where 命令) $phpExe = exec('where php 2>nul'); if ($phpExe) { $phpDir = dirname($phpExe); @@ -341,9 +352,18 @@ class CompilerBase extends \PhpAot\Core\Translator // 默认路径 return 'C:\\php'; } else { - $phpDir = shell_exec('php-config --prefix'); + // Unix/Linux/macOS 下使用 php-config 获取 PHP 路径 + $phpDir = shell_exec('php-config --prefix 2>/dev/null'); if (empty($phpDir)) { - $this->error('The `php-config` is not found'); + // 如果 php-config 不可用,尝试从 which php 推断 + $phpExe = trim(shell_exec('which php 2>/dev/null')); + if ($phpExe && file_exists($phpExe)) { + $phpDir = dirname(dirname($phpExe)); + if (is_dir($phpDir)) { + return $phpDir; + } + } + $this->error('The `php-config` is not found. Please install PHP development package or set PHP_HOME environment variable'); } return trim($phpDir); } @@ -640,6 +660,12 @@ class CompilerBase extends \PhpAot\Core\Translator protected function removeCommonPrefix(string $short, string $long): string { + // Windows 下统一使用反斜杠 + if ($this->isWindows()) { + $short = str_replace('/', '\\', $short); + $long = str_replace('/', '\\', $long); + } + $len = min(strlen($short), strlen($long)); $prefixLen = 0; @@ -651,7 +677,16 @@ class CompilerBase extends \PhpAot\Core\Translator } } - return substr($long, $prefixLen); + $result = substr($long, $prefixLen); + + // 移除开头的路径分隔符 + if ($this->isWindows()) { + $result = ltrim($result, '\\'); + } else { + $result = ltrim($result, '/'); + } + + return $result; } protected function getVarType(string $name): string @@ -2105,7 +2140,21 @@ class CompilerBase extends \PhpAot\Core\Translator $this->getBuildDir() . '/include', $this->getPhpxDir() . '/src/misc', ]; - $out = '$(php-config --includes) '; + + // 尝试使用 php-config 获取包含路径,如果失败则手动构建 + $phpIncludes = shell_exec('php-config --includes 2>/dev/null'); + if ($phpIncludes) { + $out = trim($phpIncludes) . ' '; + } else { + // 手动构建包含路径 + $phpInclude = $this->getPhpDir() . '/include/php'; + if (is_dir($phpInclude)) { + $out = '-I ' . $phpInclude . ' '; + } else { + $out = ''; + } + } + foreach ($list as $li) { $out .= '-I ' . $li . ' '; } @@ -2121,15 +2170,32 @@ class CompilerBase extends \PhpAot\Core\Translator $this->getPhpxDir() . '\\src\\misc', ]; - // 获取 PHP 的包含路径(Windows) - $phpInclude = $this->getPhpDir() . '\\include'; - if (is_dir($phpInclude)) { - $list[] = $phpInclude; + // Windows 下 PHP 头文件必须从 SDK/include 读取 + $phpSdkInclude = $this->getPhpDir() . '\\SDK\\include'; + if (!is_dir($phpSdkInclude)) { + throw new \RuntimeException( + "PHP SDK include directory not found: {$phpSdkInclude}\n" . + "Please ensure PHP is installed with SDK headers at the expected location." + ); + } + + // 添加主包含目录 + $list[] = $phpSdkInclude; + + // 添加子目录:main, Zend, TSRM, ext + $subDirs = ['main', 'Zend', 'TSRM', 'ext']; + foreach ($subDirs as $subDir) { + $subPath = $phpSdkInclude . '\\' . $subDir; + if (is_dir($subPath)) { + $list[] = $subPath; + } } $out = ''; foreach ($list as $li) { - $out .= '/I "' . $li . '" '; + // 确保路径使用双引号包裹,处理可能的空格 + $normalizedPath = str_replace('/', '\\', $li); + $out .= '/I "' . $normalizedPath . '" '; } return $out; @@ -2142,9 +2208,24 @@ class CompilerBase extends \PhpAot\Core\Translator } $list = [ - '$(php-config --prefix)/lib', $this->getPhpxDir() . '/lib', ]; + + // 尝试使用 php-config 获取库路径,如果失败则手动构建 + $phpLibDir = shell_exec('php-config --prefix 2>/dev/null'); + if ($phpLibDir) { + $phpLibPath = trim($phpLibDir) . '/lib'; + if (is_dir($phpLibPath)) { + array_unshift($list, $phpLibPath); + } + } else { + // 手动添加 PHP 库路径 + $phpLibPath = $this->getPhpDir() . '/lib'; + if (is_dir($phpLibPath)) { + array_unshift($list, $phpLibPath); + } + } + $out = ''; foreach ($list as $li) { $out .= '-L ' . $li . ' '; @@ -2155,10 +2236,25 @@ class CompilerBase extends \PhpAot\Core\Translator protected function parseWindowsLdflags(): string { - $list = [ - $this->getPhpDir() . '\\lib', - $this->getPhpxDir() . '\\lib', - ]; + $list = []; + + // Windows 下 PHP 库文件固定从 SDK/lib 读取 + $phpLib = $this->getPhpDir() . '\\SDK\\lib'; + if (is_dir($phpLib)) { + $list[] = $phpLib; + } else { + // 备选:尝试直接从 lib 目录 + $phpLibAlt = $this->getPhpDir() . '\\lib'; + if (is_dir($phpLibAlt)) { + $list[] = $phpLibAlt; + } + } + + // phpx 库文件 + $phpxLib = $this->getPhpxDir() . '\\lib'; + if (is_dir($phpxLib)) { + $list[] = $phpxLib; + } $out = ''; foreach ($list as $li) { @@ -2207,22 +2303,53 @@ class CompilerBase extends \PhpAot\Core\Translator } if ($this->buildMode === 'bin') { - // 优先使用 .lib 导入库 - $phpLib = $this->getPhpDir() . '\\lib\\php8.lib'; - if (file_exists($phpLib)) { - $list[] = '"' . $phpLib . '"'; - } else { - // 尝试其他可能的文件名 - $altPhpLib = $this->getPhpDir() . '\\lib\\php8ts.lib'; + // Windows 下 PHP 库文件和 DLL 的查找顺序: + // 1. SDK/lib/php8.lib + // 2. SDK/lib/php8ts.lib + // 3. lib/php8.lib + // 4. lib/php8ts.lib + // 5. 根目录/php8.dll + + $phpDirs = [ + $this->getPhpDir() . '\\SDK\\lib', // 优先从 SDK/lib 查找 + $this->getPhpDir() . '\\lib', // 备选从 lib 查找 + ]; + + $found = false; + foreach ($phpDirs as $phpDir) { + if (!is_dir($phpDir)) continue; + + // 尝试 php8.lib + $phpLib = $phpDir . '\\php8.lib'; + if (file_exists($phpLib)) { + $list[] = '"' . $phpLib . '"'; + $found = true; + break; + } + + // 尝试 php8ts.lib + $altPhpLib = $phpDir . '\\php8ts.lib'; if (file_exists($altPhpLib)) { $list[] = '"' . $altPhpLib . '"'; + $found = true; + break; + } + } + + // 如果都没找到 .lib,尝试直接使用根目录的 DLL + if (!$found) { + $phpDll = $this->getPhpDir() . '\\php8.dll'; + if (file_exists($phpDll)) { + $this->climate->magenta('php8.lib not found, using php8.dll directly'); + $this->climate->info('Note: This may require additional setup'); + $list[] = '"' . $phpDll . '"'; } else { - // 最后尝试直接使用 DLL - $phpDll = $this->getPhpDir() . '\\php8.dll'; - if (file_exists($phpDll)) { - $this->climate->magenta('php8.lib not found, using php8.dll directly'); + // 尝试 php8ts.dll + $phpTsDll = $this->getPhpDir() . '\\php8ts.dll'; + if (file_exists($phpTsDll)) { + $this->climate->magenta('php8.lib not found, using php8ts.dll directly'); $this->climate->info('Note: This may require additional setup'); - $list[] = '"' . $phpDll . '"'; + $list[] = '"' . $phpTsDll . '"'; } } } @@ -2249,8 +2376,22 @@ class CompilerBase extends \PhpAot\Core\Translator protected function addWindowsCompilationOption(string &$cmd, bool $link): void { - // 添加包含路径 - $cmd .= ' ' . $this->parseWindowsIncludes(); + // 添加包含路径(仅在编译时,不在链接时) + if (!$link) { + $cmd .= ' ' . $this->parseWindowsIncludes(); + } + + // Windows 平台必需的宏定义(参考 CMakeLists.txt) + $cmd .= ' /DZEND_WIN32'; // 标识 Windows 平台 + $cmd .= ' /DPHP_WIN32'; // 标识 Windows 平台 + $cmd .= ' /DZEND_DEBUG=0'; // 禁用调试模式 + + // 根据 PHP 是否为线程安全版本决定是否添加 ZTS 宏 + if ($this->isPhpZts) { + $cmd .= ' /DZTS'; // 启用线程安全 + } + + $cmd .= ' /DZEND_ENABLE_STATIC_TSRMLS_CACHE'; // 启用静态 TSRM 缓存 // 优化级别 switch ($this->optimizeLevel) { @@ -2273,8 +2414,15 @@ class CompilerBase extends \PhpAot\Core\Translator // 警告级别 $cmd .= ' /W3'; + + // 禁用 PHP SDK 头文件中的常见警告 + $cmd .= ' /wd4244'; // 禁用类型转换警告(__int64 到 int) + $cmd .= ' /wd4146'; // 禁用一元负运算符应用于无符号类型的警告 + + // 禁用编译器版权信息输出 + $cmd .= ' /nologo'; - // C++ 标准 + // C++ 标准(仅在编译时) if (!$link && !str_contains($this->cxxflags, '/std:')) { $cmd .= ' /std:c++17'; } diff --git a/src/Php/Preprocessor.php b/src/Php/Preprocessor.php index 9d32f998..e25617e7 100644 --- a/src/Php/Preprocessor.php +++ b/src/Php/Preprocessor.php @@ -66,15 +66,21 @@ class Preprocessor extends CompilerBase public function getCppFile(string $file): string { $info = pathinfo($file); - - return $this->buildDir . '/' . $this->removeCommonPrefix($this->buildDir, $info['dirname'] . '/' . $info['filename'] . '.cc'); + + // Windows 下使用反斜杠,其他平台使用正斜杠 + $separator = $this->isWindows() ? '\\' : '/'; + $relativePath = $this->removeCommonPrefix($this->buildDir, $info['dirname']); + + return $this->buildDir . $separator . $relativePath . $separator . $info['filename'] . '.cc'; } public function getObjectFile(string $cppFile): string { $info = pathinfo($cppFile); $ext = $this->isWindows() ? '.obj' : '.o'; - return $info['dirname'] . '/' . $info['filename'] . $ext; + + // 保持与 cppFile 相同的路径分隔符 + return $info['dirname'] . ($this->isWindows() ? '\\' : '/') . $info['filename'] . $ext; } public function hasCppFileCache(string $file): bool diff --git a/src/Php/Translator.php b/src/Php/Translator.php index 1062c3e5..b7dff04a 100644 --- a/src/Php/Translator.php +++ b/src/Php/Translator.php @@ -213,12 +213,56 @@ class Translator extends Preprocessor public function prepare(string $path): array { - $ext = $this->isMacos() ? 'dylib' : 'so'; - if (!is_file($this->getPhpDir() . '/lib/libphp.' . $ext)) { - $this->error("The `libphp.{$ext}` is not found, please run `make` to build it"); - } - if (!is_file($this->getPhpxDir() . '/lib/libphpx.' . $ext)) { - $this->error("The `libphpx.{$ext}` is not found, please run `make` to build it"); + // 根据平台检查库文件(仅在构建二进制文件时需要) + if ($this->buildMode === 'bin') { + if ($this->isWindows()) { + // Windows 平台检查 dll 和 lib 文件 + // 按照新的路径规则检查:SDK/lib, lib, 根目录 + $phpDirs = [ + $this->getPhpDir() . '\\SDK\\lib', + $this->getPhpDir() . '\\lib', + ]; + + $foundLib = false; + foreach ($phpDirs as $phpDir) { + if (is_dir($phpDir)) { + if (is_file($phpDir . '\\php8.lib') || is_file($phpDir . '\\php8ts.lib')) { + $foundLib = true; + break; + } + } + } + + // 如果没找到 lib,检查根目录的 DLL + if (!$foundLib) { + $phpDll = $this->getPhpDir() . '\\php8.dll'; + $phpTsDll = $this->getPhpDir() . '\\php8ts.dll'; + if (!is_file($phpDll) && !is_file($phpTsDll)) { + $this->climate->warning("The `php8.lib` or `php8.dll` is not found in PHP directory, please check your PHP installation"); + } + } + + $phpxLib = $this->getPhpxDir() . '\\lib\\phpx.lib'; + $phpxDll = $this->getPhpxDir() . '\\lib\\phpx.dll'; + if (!is_file($phpxLib) && !is_file($phpxDll)) { + $this->climate->warning("The `phpx.lib` or `phpx.dll` is not found in PHX directory"); + $this->climate->info("Note: If you are building an extension (-m ext), this is OK. For binary mode, please run `make` to build it"); + } + } else { + // Unix/Linux/macOS 平台检查 .so 或 .dylib 文件 + $ext = $this->isMacos() ? 'dylib' : 'so'; + $phpLib = $this->getPhpDir() . '/lib/libphp.' . $ext; + $phpxLib = $this->getPhpxDir() . '/lib/libphpx.' . $ext; + + if (!is_file($phpLib)) { + $this->climate->warning("The `libphp.{$ext}` is not found"); + $this->climate->info("Note: If you are building an extension (-m ext), this is OK. For binary mode, please run `make` to build it"); + } + if (!is_file($phpxLib)) { + $this->climate->warning("The `libphpx.{$ext}` is not found"); + $this->climate->info("Note: If you are building an extension (-m ext), this is OK. For binary mode, please run `make` to build it"); + } + } } $clangFormatVersion = shell_exec('clang-format --version'); @@ -321,13 +365,14 @@ class Translator extends Preprocessor $literalStringsCount = count($this->literalStrings); $lines[] = 'extern ' . self::TYPE_STR . ' ' . self::LITERAL_STRINGS . '[' . $literalStringsCount . '];' . PHP_EOL; - $classCount = count($this->classMap); + // 确保数组大小至少为 1,避免 C/C++ 编译错误 + $classCount = max(1, count($this->classMap)); $lines[] = 'extern zend_class_entry *' . self::PREFIX . self::CLASS_MAP . '[' . $classCount . '];' . PHP_EOL; - $funcCount = count($this->funcMap); + $funcCount = max(1, count($this->funcMap)); $lines[] = 'extern zend_function *' . self::PREFIX . self::FUNC_MAP . '[' . $funcCount . '];' . PHP_EOL; - $propCount = count($this->propMap); + $propCount = max(1, count($this->propMap)); $lines[] = 'extern uint32_t ' . self::PREFIX . self::PROP_MAP . '[' . $propCount . '];' . PHP_EOL; foreach ($this->classes as $classDef) { @@ -378,13 +423,14 @@ class Translator extends Preprocessor } $code .= "// class entry \n"; - $code .= 'zend_class_entry *' . self::PREFIX . self::CLASS_MAP . '[' . count($this->classMap) . '];' . PHP_EOL; + // 确保数组大小至少为 1,避免 C/C++ 编译错误 + $code .= 'zend_class_entry *' . self::PREFIX . self::CLASS_MAP . '[' . max(1, count($this->classMap)) . '];' . PHP_EOL; $code .= "// func \n"; - $code .= 'zend_function *' . self::PREFIX . self::FUNC_MAP . '[' . count($this->funcMap) . '];' . PHP_EOL; + $code .= 'zend_function *' . self::PREFIX . self::FUNC_MAP . '[' . max(1, count($this->funcMap)) . '];' . PHP_EOL; $code .= "// property \n"; - $code .= 'uint32_t ' . self::PREFIX . self::PROP_MAP . '[' . count($this->propMap) . '];' . PHP_EOL; + $code .= 'uint32_t ' . self::PREFIX . self::PROP_MAP . '[' . max(1, count($this->propMap)) . '];' . PHP_EOL; $code .= <<<'CODE' zend_class_entry *php_get_class(int class_id, const php::Str &class_name) { @@ -661,7 +707,63 @@ CODE; $sourceFiles[] = $this->getPhpxDir() . '/src/misc/ps_title.c'; } - // 并行编译 + // Windows 不支持 pcntl_fork,使用串行编译或 proc_open + if ($this->isWindows()) { + return $this->compileOnWindows($sourceFiles); + } + + // Unix/Linux/macOS 使用 pcntl 并行编译 + return $this->compileWithPcntl($sourceFiles, $job); + } + + /** + * Windows 平台编译(不使用 pcntl) + */ + protected function compileOnWindows(array $sourceFiles): array + { + $objectFiles = []; + $totalFiles = count($sourceFiles); + $failedFiles = []; + + $this->climate->lightBlue("Starting compilation for {$totalFiles} files (Windows mode)"); + + foreach ($sourceFiles as $cppFile) { + $objectFile = $this->getObjectFile($cppFile); + + try { + $this->compileFile($cppFile, $objectFile, false); + if (is_file($objectFile)) { + $objectFiles[] = $objectFile; + } else { + $failedFiles[] = $cppFile; + $this->climate->red("Compilation failed: {$cppFile}"); + } + } catch (\Throwable $e) { + $failedFiles[] = $cppFile; + $this->climate->red("Compilation error: {$cppFile} - " . $e->getMessage()); + } + } + + if (!empty($failedFiles)) { + throw new \Exception('Compilation failed for: ' . implode(', ', $failedFiles)); + } + + $this->climate->green("Successfully compiled {$totalFiles} files"); + return $objectFiles; + } + + /** + * Unix/Linux/macOS 平台并行编译(使用 pcntl) + */ + protected function compileWithPcntl(array $sourceFiles, int $job): array + { + // 检查 pcntl 扩展是否可用 + if (!function_exists('pcntl_fork')) { + $this->climate->warning('pcntl extension not available, using sequential compilation'); + return $this->compileOnWindows($sourceFiles); + } + + $objectFiles = []; $totalFiles = count($sourceFiles); $runningProcesses = 0; $processPipes = []; @@ -738,7 +840,6 @@ CODE; } $this->climate->green("Successfully compiled {$totalFiles} files"); - return $objectFiles; } @@ -781,7 +882,28 @@ CODE; $this->addCompilationOption($linkCmd, true); $this->climate->comment($linkCmd); - shell_exec($linkCmd); + + // 执行链接并捕获输出 + exec($linkCmd . ' 2>&1', $output, $ret); + + // 显示输出(如果有) + if (!empty($output)) { + foreach ($output as $line) { + $this->climate->out($line); + } + } + + // 检查链接是否成功 + if ($ret !== 0) { + $this->error('link failed: ' . $targetFile); + } + + // 验证目标文件是否生成 + if (!file_exists($targetFile)) { + $this->error('target file not generated: ' . $targetFile); + } + + $this->climate->green('Build successful: ' . $targetFile); } public function genFunctionDeclaration(string $file): void