diff --git a/.gitignore b/.gitignore index 92110594..d46d61f6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ /.idea /logs +/build /vendor /tmp /examples/wordpress diff --git a/bin/compiler.php b/bin/compiler.php index 2764eb20..bf21073e 100755 --- a/bin/compiler.php +++ b/bin/compiler.php @@ -14,6 +14,11 @@ $path = $argv[1]; $translator = new Translator(ROOT_PATH); $translator->setIndent(' '); +$path = realpath($path); +if ($path === false) { + die("path not exists: $path\n"); +} + if (is_dir($path)) { $scanner = new FileScanner($path); $list = $scanner->scan(); @@ -26,14 +31,26 @@ if (is_dir($path)) { $sourceFiles = []; $objectFiles = []; -// 分析 PHP 文件,生成 C++ 文件 +// 分析 PHP 文件,预处理 +foreach ($list as $k => $file) { + if (FileScanner::isPhpFile($file)) { + try { + $translator->prepare($file); + } catch (Unsupported $e) { + echo " unsupported syntax: " . $e->getMessage() . "\n"; + echo " skip: " . $file . "\n"; + unset($list[$k]); + } + } +} + +$translator->sortFiles($list); + +// 生成 C++ 文件 foreach ($list as $file) { try { if (FileScanner::isPhpFile($file)) { - $code = $translator->convert($file); - $info = pathinfo($file); - $cppFile = $info['dirname'] . '/' . $info['filename'] . '.cc'; - $translator->save($code, $cppFile); + $cppFile = $translator->convert($file); } elseif (FileScanner::isCppFile($file)) { $cppFile = $file; } else { diff --git a/examples/project/call.php b/examples/project/call.php new file mode 100644 index 00000000..f3ced80c --- /dev/null +++ b/examples/project/call.php @@ -0,0 +1,12 @@ + +#include "phpx_func.h" + +using namespace php; + +Int php_ext_test(Int a, Int b) { + auto c = a + b; + var_dump(c); + var_dump(php_uname()); + return c; +} diff --git a/examples/project/ext.stub.php b/examples/project/ext.stub.php new file mode 100644 index 00000000..f106e1be --- /dev/null +++ b/examples/project/ext.stub.php @@ -0,0 +1,4 @@ +excludePatterns = $patterns; + return $this; + } + + public function getDirectory(): string + { + return $this->directory; + } + + private function isExcluded(string $filePath): bool + { + $excluded = false; + foreach ($this->excludePatterns as $pattern) { + if ($this->matchPattern($pattern, $filePath)) { + $excluded = true; + break; + } + } + return $excluded; + } + public function scan(): array { $files = []; @@ -59,20 +82,16 @@ class FileScanner if ($file->isFile()) { if (self::isPhpFile($file)) { $filePath = $file->getPathname(); - $excluded = false; - foreach ($this->excludePatterns as $pattern) { - if ($this->matchPattern($pattern, $filePath)) { - $excluded = true; - break; - } - } - if (!$excluded) { - $files[] = $filePath; - } + } elseif (self::isCppFile($file)) { + $filePath = $file->getPathname(); + } else { + continue; + } + if (!$this->isExcluded($filePath)) { + $files[] = $filePath; } } } - sort($files); return $files; } diff --git a/src/Php/FileSorter.php b/src/Php/FileSorter.php new file mode 100644 index 00000000..38c21e1e --- /dev/null +++ b/src/Php/FileSorter.php @@ -0,0 +1,94 @@ +functionDeclInFile = $functionDeclInFile; + $this->functionCallInFile = $functionCallInFile; + } + + public function sort(): array + { + $dependencies = $this->buildDependencies(); + + $allFiles = $this->getAllFiles(); + + $inDegree = array_fill_keys($allFiles, 0); + foreach ($dependencies as $deps) { + foreach ($deps as $dep) { + $inDegree[$dep]++; + } + } + + $queue = []; + foreach ($inDegree as $file => $degree) { + if ($degree === 0) { + $queue[] = $file; + } + } + + $sorted = []; + while (!empty($queue)) { + $current = array_shift($queue); + $sorted[] = $current; + + if (isset($dependencies[$current])) { + foreach ($dependencies[$current] as $dep) { + $inDegree[$dep]--; + if ($inDegree[$dep] === 0) { + $queue[] = $dep; + } + } + } + } + + if (count($sorted) !== count($allFiles)) { + throw new \RuntimeException("Circular dependency of function call detected"); + } + + return array_reverse($sorted); + } + + private function buildDependencies(): array + { + $dependencies = []; + + foreach ($this->functionCallInFile as $call) { + $callerFile = $call['file']; + $functionName = $call['name']; + + if (isset($this->functionDeclInFile[$functionName])) { + $declFile = $this->functionDeclInFile[$functionName]; + + if ($callerFile !== $declFile) { + if (!isset($dependencies[$callerFile])) { + $dependencies[$callerFile] = []; + } + + if (!in_array($declFile, $dependencies[$callerFile])) { + $dependencies[$callerFile][] = $declFile; + } + } + } + } + + return $dependencies; + } + + private function getAllFiles(): array + { + $files = array_values($this->functionDeclInFile); + + foreach ($this->functionCallInFile as $call) { + $files[] = $call['file']; + } + + return array_unique($files); + } +} \ No newline at end of file diff --git a/src/Php/RedoException.php b/src/Php/RedoException.php new file mode 100644 index 00000000..67ed9a4f --- /dev/null +++ b/src/Php/RedoException.php @@ -0,0 +1,8 @@ +rootPath = $rootPath; + $this->setBuildDir($rootPath . '/build'); $climate = new CLImate; $this->climate = $climate; $climate->arguments->add([ @@ -306,21 +312,32 @@ class Translator extends \PhpAot\Core\Translator return $this->genIncludeHeaderFiles() . $this->genExternGlobalVars() . $cppCode; } - public function convert(string $file): string + private function removeCommonPrefix(string $str1, string $str2): string { - if (!file_exists($file)) { - throw new \Exception('File not exists: ' . $file); + $len = min(strlen($str1), strlen($str2)); + $prefixLen = 0; + + for ($i = 0; $i < $len; $i++) { + if ($str1[$i] === $str2[$i]) { + $prefixLen++; + } else { + break; + } } - $phpCode = file_get_contents($file); - $this->file = realpath($file); - $this->dir = dirname($this->file); + return substr($str2, $prefixLen); + } + public function convert(string $file): string + { + $phpCode = $this->loadFile($file); while (true) { try { - return $this->doConvert($phpCode); - } catch (ReturnTypeChanged $e) { - // 某些情况,例如返回值变更,需要重新解析 - $this->climate->cyan('Return type changed, retrying...'); + $cppCode = $this->doConvert($phpCode); + $info = pathinfo($file); + $cppFile = $this->buildDir . '/' . $this->removeCommonPrefix($this->buildDir, $info['dirname'] . '/' . $info['filename'] . '.cc'); + $this->save($cppCode, $cppFile); + return $cppFile; + } catch (RedoException $e) { continue; } } @@ -328,6 +345,10 @@ class Translator extends \PhpAot\Core\Translator public function save(string $code, string $file): void { + $dir = dirname($file); + if (!is_dir($dir)) { + mkdir($dir, 0777, true); + } file_put_contents($file, $code); $this->formatCppCode($file); } @@ -370,9 +391,8 @@ class Translator extends \PhpAot\Core\Translator $this->tmpVarIndex = 0; } - private function parseFunctionDef($v): string + private function getFunctionName(Node $v): string { - $this->resetScope(); $names[] = $this->parseIdentifier($v->name); if ($this->class) { $names[] = strtolower($this->class); @@ -380,20 +400,40 @@ class Translator extends \PhpAot\Core\Translator if ($this->namespace) { $names[] = strtolower(str_replace('\\', '_', $this->namespace)); } + return implode('__', array_reverse($names)); + } - $name = implode('__', array_reverse($names)); + private function parseFunctionDeclaration(string $name, Node $v): FunctionDef + { + $functionDef = new FunctionDef(); + $this->functionDef = $functionDef; + $functionDef->name = $name; + if ($v->returnType) { + $functionDef->returnType = $this->getTypeFromZendType($this->parseIdentifier($v->returnType)); + } else { + // .stub 存根定义 C++ Native 函数,必须设置返回值类型 + if ($this->stubFile) { + throw new Exception('No return type for ' . $name); + } + $functionDef->returnType = 'void'; + } + $this->parseParams($v->params); + return $functionDef; + } + + private function parseFunctionDef(Node $v): string + { + $this->resetScope(); + $name = $this->getFunctionName($v); if (isset($this->nativeFunctions[$name])) { $this->functionDef = $this->nativeFunctions[$name]; } else { - $this->functionDef = new FunctionDef(); - $this->functionDef->name = $name; - if ($v->returnType) { - $this->functionDef->returnType = $this->getTypeFromZendType($this->parseIdentifier($v->returnType)); - } else { - $this->functionDef->returnType = 'void'; + $this->nativeFunctions[$name] = $this->parseFunctionDeclaration($name, $v); + if (isset($this->redoAfterDeclare[$name])) { + unset($this->redoAfterDeclare[$name]); + $this->climate->cyan('Received redo request, retrying...'); + throw new RedoException; } - $this->parseParams($v->params); - $this->nativeFunctions[$name] = $this->functionDef; } foreach ($this->functionDef->argInfoList as $argInfo) { @@ -482,6 +522,10 @@ class Translator extends \PhpAot\Core\Translator $list = []; $this->functionDef->argCountRequired = count($params); foreach ($params as $param) { + // .stub 存根定义 C++ Native 函数,必须设置函数的参数类型 + if ($this->stubFile and !$param->type) { + throw new \RuntimeException('No type for ' . $this->parseIdentifier($param->var)); + } $type = $this->parseType($param->type); $name = $this->parseIdentifier($param->var); $list[] = $type . ' ' . $name; @@ -726,11 +770,10 @@ class Translator extends \PhpAot\Core\Translator case self::EXPR_VARIABLE: return $this->parseIdentifier($expr); case 'Scalar_MagicConst_File': - return $this->parseMagicConstFile($expr); case 'Scalar_MagicConst_Dir': - return $this->parseMagicConstDir($expr); case 'Scalar_MagicConst_Line': - return $this->parseMagicConstLine($expr); + case 'Scalar_MagicConst_Function': + return $this->parseMagicConst($expr); case 'Scalar_InterpolatedString': return $this->parseInterpolatedString($expr); case 'Expr_Cast_Int': @@ -958,7 +1001,9 @@ class Translator extends \PhpAot\Core\Translator private function resetReturnType(string $type): void { $this->functionDef->returnType = $type; - throw new ReturnTypeChanged; + // 返回值变更,需要重新解析 + $this->climate->cyan('Return type changed, retrying...'); + throw new RedoException; } private function detectVarType($var): string @@ -1377,6 +1422,12 @@ class Translator extends \PhpAot\Core\Translator $name = ''; } elseif ($expr->name->getType() === 'Name') { $name = $this->parseIdentifier($expr->name); + // 在预处理阶段检测到函数声明,但是未定义,说明在当前文件,但是顺序错误 + if (isset($this->functionDeclInFile[$name]) + and $this->functionDeclInFile[$name] === $this->file + and !$this->isNativeFunction($name)) { + $this->redoAfterDeclare[$name] = true; + } if ($this->isNativeFunction($name)) { return self::PREFIX . $name . '(' . $this->parseCallArgs($expr->args, $name) . ')'; } @@ -2004,9 +2055,20 @@ class Translator extends \PhpAot\Core\Translator return $var . ' ^= ' . $this->parseIdentifier($node->expr); } - private function parseMagicConstFile(mixed $expr): string + private function parseMagicConst(mixed $expr): string { - return '"' . $this->escapeString($this->file) . '"'; + switch ($expr->getType()) { + case 'Scalar_MagicConst_Dir': + return '"' . $this->escapeString($this->dir) . '"'; + case 'Scalar_MagicConst_File': + return '"' . $this->escapeString($this->file) . '"'; + case 'Scalar_MagicConst_Line': + return $expr->getStartLine(); + case 'Scalar_MagicConst_Function': + return '"' . $this->escapeString($this->functionDef->name) . '"'; + default: + abort($expr); + } } private function parseForeach(Node $node): string @@ -2173,11 +2235,6 @@ class Translator extends \PhpAot\Core\Translator return 'php::include(' . $this->parseIdentifier($expr->expr) . ')'; } - private function parseMagicConstDir(mixed $expr): string - { - return '"' . $this->escapeString($this->dir) . '"'; - } - private function parseBreak(mixed $v): string { if (!$this->inLoop and !$this->inSwitch) { @@ -2467,11 +2524,6 @@ class Translator extends \PhpAot\Core\Translator return 'php::getStaticProperty(' . $this->identifierToStr($expr->class) . ', ' . $this->identifierToStr($expr->name) . ')'; } - private function parseMagicConstLine(Node $expr): int - { - return $expr->getStartLine(); - } - private function parseThrow(mixed $expr): string { if ($expr->expr->getType() != self::EXPR_VARIABLE and $expr->expr->getType() != self::EXPR_NEW) { @@ -2574,7 +2626,7 @@ class Translator extends \PhpAot\Core\Translator $this->nativeConstants[$name] = $constInfo; } - private function hasConstant(string $name) + private function hasConstant(string $name): bool { return isset($this->nativeConstants[$name]); } @@ -2607,4 +2659,152 @@ class Translator extends \PhpAot\Core\Translator { return $var->getType() == self::EXPR_VARIABLE and $this->hasVar($var->name) and $this->getVarType($var->name) == self::TYPE_ARRAY; } + + private function setBuildDir(string $string): void + { + $this->buildDir = $string; + if (!is_dir($this->buildDir)) { + mkdir($this->buildDir, 0777, true); + } + } + + public function prepare(string $file) + { + $phpCode = $this->loadFile($file); + + $this->climate->info('prepare: ' . $this->file); + $parser = (new ParserFactory())->createForNewestSupportedVersion(); + $ast = $parser->parse($phpCode); + + $traverser = new NodeTraverser; + $prettyPrinter = new PrettyPrinter\Standard; + $traverser->addVisitor(new Visitor()); + $stmts = $traverser->traverse($ast); + + foreach ($stmts as $v) { + $type = $v->getType(); + switch ($type) { + case 'Stmt_Namespace': + $this->prepareNamespaceDef($v); + break; + case 'Stmt_Class': + $this->prepareClassDef($v); + break; + case 'Stmt_Function': + $this->prepareFunctionDef($v) . PHP_EOL; + break; + case 'Stmt_Const': + break; + default: + $this->fatalError($v, 'Unsupported statement: ' . $type); + break; + } + } + + $nodeFinder = new NodeFinder(); + $functionCalls = $nodeFinder->findInstanceOf($ast, Node\Expr\FuncCall::class); + + foreach ($functionCalls as $call) { + if ($call->name instanceof Node\Name) { + $name = $call->name->toString(); + $this->functionCallInFile[] = [ + 'name' => $name, + 'file' => $this->file, + 'line' => $call->getLine(), + ]; + } + } + } + + private function prepareFunctionDef(Node $v): void + { + $name = $this->getFunctionName($v); + if ($this->stubFile) { + $this->nativeFunctions[$name] = $this->parseFunctionDeclaration($name, $v); + } else { + $this->functionDeclInFile[$name] = $this->file; + } + } + + private function prepareNamespaceDef(Node $node): void + { + $this->namespace = $this->parseIdentifier($node->name); + foreach($node->stmts as $v2) { + $type2 = $v2->getType(); + switch ($type2) { + case 'Stmt_Class': + $this->prepareClassDef($v2); + break; + case 'Stmt_Function': + $this->prepareFunctionDef($v2) . PHP_EOL; + break; + case 'Stmt_Use': + case 'Stmt_Const': + break; + default: + abort($v2); + } + } + $this->namespace = ''; + $this->uses = []; + } + + private function prepareClassDef(Node $v): string + { + $this->class = $this->parseIdentifier($v->name); + $code = ''; + foreach($v->stmts as $v) { + $type = $v->getType(); + switch ($type) { + case 'Stmt_ClassConst': + case 'Stmt_Property': + break; + case 'Stmt_ClassMethod': + $code .= $this->prepareFunctionDef($v) . PHP_EOL; + break; + default: + abort($v); + } + } + $this->class = ''; + return $code; + } + + public function sortFiles(array &$list): void + { + foreach ($this->functionCallInFile as $k => $call) { + if (!isset($this->functionDeclInFile[$call['name']])) { + unset($this->functionCallInFile[$k]); + } + } + $sorter = new FileSorter($this->functionDeclInFile, $this->functionCallInFile); + $sortedFiles = $sorter->sort(); + + foreach ($list as $file) { + if (!$this->isStubFile($file) and !in_array($file, $sortedFiles)) { + $sortedFiles[] = $file; + } + } + $list = $sortedFiles; + } + + private function isStubFile(string $file): bool + { + return str_ends_with($file, '.stub.php'); + } + + private function loadFile(string $file): string + { + if (!file_exists($file)) { + throw new \Exception('File not exists: ' . $file); + } + $phpCode = file_get_contents($file); + if (!$phpCode) { + throw new \Exception('Can not read file: ' . $file); + } + $this->file = realpath($file); + $this->dir = dirname($this->file); + $this->stubFile = $this->isStubFile($file); + return $phpCode; + } }