diff --git a/ast.py b/ast.py new file mode 100644 index 00000000..b3a53ca2 --- /dev/null +++ b/ast.py @@ -0,0 +1,495 @@ +#!/usr/bin/env python3 +""" +使用 libclang 解析 C++ 代码并添加头文件路径 +""" + +import clang.cindex +from clang.cindex import Index, CursorKind, TypeKind, StorageClass +import json +import sys +import os +from pathlib import Path +import subprocess +import argparse + + +class PHPConfigHelper: + """PHP 配置辅助类""" + + def __init__(self, php_config_path='php-config'): + self.php_config = php_config_path + self._check_availability() + + def _check_availability(self): + """检查 php-config 是否可用""" + try: + result = subprocess.run( + [self.php_config, '--version'], + capture_output=True, + text=True, + check=True + ) + print(f"✓ 找到 PHP {result.stdout.strip()}") + except (FileNotFoundError, subprocess.CalledProcessError) as e: + print(f"警告: php-config 不可用: {e}") + # 尝试使用常见路径 + common_paths = [ + '/usr/bin/php-config', + '/usr/local/bin/php-config', + '/opt/php/bin/php-config' + ] + for path in common_paths: + if os.path.exists(path): + self.php_config = path + try: + result = subprocess.run( + [self.php_config, '--version'], + capture_output=True, + text=True, + check=True + ) + print(f"✓ 找到 PHP {result.stdout.strip()} 在 {path}") + return + except (FileNotFoundError, subprocess.CalledProcessError): + continue + print("警告: php-config 在任何常见路径都不可用,使用默认路径") + + def get_includes(self): + """ + 获取 include 路径列表 + + Returns: + list: 头文件路径列表(不带 -I 前缀) + """ + try: + result = subprocess.run( + [self.php_config, '--includes'], + capture_output=True, + text=True, + check=True + ) + + # 解析输出: "-I/path1 -I/path2" -> ['/path1', '/path2'] + includes = [] + for flag in result.stdout.strip().split(): + if flag.startswith('-I'): + includes.append(flag[2:]) + + return includes + except (subprocess.CalledProcessError, FileNotFoundError): + print("警告: 无法获取 PHP includes,使用默认路径") + return ['/usr/include/php', '/usr/include/php/20210902'] # 默认路径 + + def get_include_dir(self): + """获取主 include 目录""" + try: + result = subprocess.run( + [self.php_config, '--include-dir'], + capture_output=True, + text=True, + check=True + ) + return result.stdout.strip() + except (subprocess.CalledProcessError, FileNotFoundError): + return '/usr/include/php' + + def get_extension_dir(self): + """获取扩展目录""" + try: + result = subprocess.run( + [self.php_config, '--extension-dir'], + capture_output=True, + text=True, + check=True + ) + return result.stdout.strip() + except (subprocess.CalledProcessError, FileNotFoundError): + return '/usr/lib/php' + + def get_version(self): + """获取 PHP 版本""" + try: + result = subprocess.run( + [self.php_config, '--version'], + capture_output=True, + text=True, + check=True + ) + return result.stdout.strip() + except (subprocess.CalledProcessError, FileNotFoundError): + return 'unknown' + + def get_php_binary(self): + """获取 PHP 二进制路径""" + try: + result = subprocess.run( + [self.php_config, '--php-binary'], + capture_output=True, + text=True, + check=True + ) + return result.stdout.strip() + except (subprocess.CalledProcessError, FileNotFoundError): + return 'php' + + def get_configure_options(self): + """获取配置选项""" + try: + result = subprocess.run( + [self.php_config, '--configure-options'], + capture_output=True, + text=True, + check=True + ) + return result.stdout.strip() + except (subprocess.CalledProcessError, FileNotFoundError): + return '' + + def get_all_info(self): + """获取所有配置信息""" + return { + 'version': self.get_version(), + 'includes': self.get_includes(), + 'include_dir': self.get_include_dir(), + 'extension_dir': self.get_extension_dir(), + 'php_binary': self.get_php_binary(), + 'configure_options': self.get_configure_options(), + } + + +class ClangParser: + def __init__(self, libclang_path=None): + """ + 初始化 Clang 解析器 + + Args: + libclang_path: libclang 库的路径(可选) + """ + if libclang_path: + try: + clang.cindex.Config.set_library_file(libclang_path) + except Exception as e: + print(f"警告: 无法设置 libclang 路径 {libclang_path}: {e}") + print("尝试使用默认路径...") + + try: + self.index = Index.create() + except Exception as e: + print(f"错误: 无法创建 Clang 索引: {e}") + print("请确保已安装 python3-clang 和 clang 库") + raise + + def parse_file(self, filename, include_paths=None, defines=None, + compiler_args=None, language='c++'): + """ + 解析 C++ 文件 + + Args: + filename: 要解析的文件路径 + include_paths: 头文件搜索路径列表 + defines: 宏定义列表 ['MACRO=value', 'DEBUG'] + compiler_args: 额外的编译器参数 + language: 语言类型 ('c', 'c++', 'objective-c') + + Returns: + TranslationUnit 对象 + """ + if not os.path.exists(filename): + raise FileNotFoundError(f"文件不存在: {filename}") + + args = [] + + # 1. 设置语言标准 + if language == 'c++': + args.extend([ + '-x', 'c++', + '-std=c++14', # 更标准的 C++ 版本 + ]) + elif language == 'c': + args.extend(['-x', 'c', '-std=c11']) + + # 2. 添加头文件搜索路径 + if include_paths: + for path in include_paths: + if os.path.exists(path): # 检查路径是否存在 + args.append(f'-I{path}') + else: + print(f"警告: 包含路径不存在: {path}") + + # 3. 添加宏定义 + if defines: + for define in defines: + args.append(f'-D{define}') + + # 4. 添加额外的编译器参数 + if compiler_args: + args.extend(compiler_args) + + # 5. 常用的编译选项 + args.extend([ + '-Wno-pragma-once-outside-header', # 忽略警告 + '-ferror-limit=0', # 不限制错误数量 + '-fno-delayed-template-parsing', # 避免某些 C++ 模板解析问题 + '-w', # 禁用所有警告以减少输出 + ]) + + print(f"编译参数: {' '.join(args)}") + + # 解析文件 + try: + tu = self.index.parse( + filename, + args=args, + options=clang.cindex.TranslationUnit.PARSE_DETAILED_PROCESSING_RECORD + ) + except Exception as e: + print(f"解析文件时出错: {e}") + print("尝试使用最小参数集...") + # 尝试使用最小参数集 + minimal_args = ['-x', 'c++', '-std=c++14', '-w'] + if include_paths: + for path in include_paths: + if os.path.exists(path): + minimal_args.append(f'-I{path}') + try: + tu = self.index.parse( + filename, + args=minimal_args, + options=clang.cindex.TranslationUnit.PARSE_DETAILED_PROCESSING_RECORD + ) + print("使用最小参数集成功解析") + except Exception as e2: + print(f"使用最小参数集也失败: {e2}") + print("尝试解析不包含头文件的简化版本...") + # 创建一个临时文件,移除头文件包含行 + temp_filename = filename + ".tmp" + with open(filename, 'r') as original: + lines = original.readlines() + + # 移除 #include 行 + filtered_lines = [line for line in lines if not line.strip().startswith('#include')] + + with open(temp_filename, 'w') as temp: + temp.writelines(filtered_lines) + + try: + tu = self.index.parse( + temp_filename, + args=minimal_args, + options=clang.cindex.TranslationUnit.PARSE_DETAILED_PROCESSING_RECORD + ) + print("解析简化版本成功") + # 清理临时文件 + os.remove(temp_filename) + except Exception as e3: + print(f"简化版本也失败: {e3}") + # 清理临时文件 + if os.path.exists(temp_filename): + os.remove(temp_filename) + raise + + # 检查诊断信息 + if tu.diagnostics: + print(f"\n诊断信息 ({len(tu.diagnostics)} 个):") + error_count = 0 + warning_count = 0 + + for diag in tu.diagnostics: + if diag.severity >= 3: # 错误级别 + error_count += 1 + else: # 警告级别 + warning_count += 1 + + print(f"错误: {error_count}, 警告: {warning_count}") + + # 只显示前几个诊断信息,避免输出过多 + for i, diag in enumerate(tu.diagnostics): + if i >= 5: # 只显示前5个 + print("... 还有更多诊断信息") + break + print(f" [{diag.severity}] {diag.spelling}") + if diag.location.file: + print(f" at {diag.location.file.name}:{diag.location.line}") + + return tu + + def extract_functions(self, tu, name_prefixes=None): + """ + 提取函数定义 + + Args: + tu: TranslationUnit 对象 + name_prefixes: 函数名前缀过滤列表 + + Returns: + 函数信息列表 + """ + functions = [] + + def visit_node(node, depth=0): + # 只处理函数声明/定义 + if node.kind == CursorKind.FUNCTION_DECL: + try: + func_info = self.parse_function(node) + + # 过滤函数名 + if name_prefixes: + if any(func_info['name'].startswith(prefix) + for prefix in name_prefixes): + functions.append(func_info) + else: + functions.append(func_info) + except Exception as e: + print(f"解析函数时出错: {e}") + + # 递归访问子节点 + for child in node.get_children(): + visit_node(child, depth + 1) + + visit_node(tu.cursor) + return functions + + def parse_function(self, cursor): + """ + 解析函数详细信息 + """ + # 检查方法是否存在 + def safe_call(method, default_value=None): + try: + return method() + except AttributeError: + return default_value + + # 基本信息 + func_info = { + 'name': cursor.spelling, + 'displayName': cursor.displayname, + 'mangledName': cursor.mangled_name, + 'returnType': cursor.result_type.spelling, + 'isStatic': cursor.storage_class == StorageClass.STATIC, + 'isInline': safe_call(lambda: cursor.is_inline_function(), False), + 'isVirtual': safe_call(lambda: cursor.is_virtual_method(), False), + 'isConst': safe_call(lambda: cursor.is_const_method(), False), + 'location': { + 'file': str(cursor.location.file) if cursor.location.file else None, + 'line': cursor.location.line, + 'column': cursor.location.column, + }, + 'parameters': [], + 'namespaces': self.get_namespaces(cursor), + } + + # 解析参数 + for arg in cursor.get_arguments(): + param_info = { + 'name': arg.spelling or f'arg{len(func_info["parameters"])}', + 'type': arg.type.spelling, + 'canonicalType': arg.type.get_canonical().spelling, + } + + # 检查是否有默认值 + try: + for token in arg.get_tokens(): + if token.spelling == '=': + # 有默认值 + param_info['hasDefault'] = True + break + except: + # 如果无法获取 tokens,跳过默认值检查 + pass + + func_info['parameters'].append(param_info) + + return func_info + + def get_namespaces(self, cursor): + """ + 获取函数所在的命名空间 + """ + namespaces = [] + parent = cursor.semantic_parent + + while parent and parent.kind != CursorKind.TRANSLATION_UNIT: + if parent.kind == CursorKind.NAMESPACE: + namespaces.insert(0, parent.spelling) + parent = parent.semantic_parent + + return namespaces + + +def main(): + parser = argparse.ArgumentParser(description='使用 libclang 解析 C++ 代码并提取函数信息') + parser.add_argument('filename', help='要解析的 C++ 文件路径') + parser.add_argument('--libclang-path', help='libclang 库路径') + parser.add_argument('--include-paths', nargs='*', help='额外的包含路径') + parser.add_argument('--function-prefixes', nargs='*', help='函数名前缀过滤器') + + args = parser.parse_args() + + if not os.path.exists(args.filename): + print(f"错误: 文件不存在: {args.filename}") + sys.exit(1) + + try: + # 创建解析器 + parser_obj = ClangParser(libclang_path=args.libclang_path) + + # 配置头文件路径 + include_paths = args.include_paths or [ + "/usr/include/linux", + "/home/swoole/workspace/projects/phpx/include" + ] + + # 尝试获取 PHP 配置的头文件路径 + try: + php_config = PHPConfigHelper() + php_includes = php_config.get_includes() + include_paths.extend(php_includes) + except Exception as e: + print(f"警告: 无法获取 PHP 配置: {e}") + print("继续使用默认路径...") + + # 配置宏定义 + defines = [ + 'HAVE_CONFIG_H', + 'ZEND_ENABLE_STATIC_TSRMLS_CACHE=1', + ] + + # 额外的编译器参数 + compiler_args = [ + '-fparse-all-comments', # 解析所有注释 + '-Wno-unknown-pragmas', + ] + + # 解析文件 + tu = parser_obj.parse_file( + args.filename, + include_paths=include_paths, + defines=defines, + compiler_args=compiler_args, + language='c++' + ) + + # 提取函数 + name_prefixes = args.function_prefixes or None + functions = parser_obj.extract_functions(tu, name_prefixes=name_prefixes) + + # 输出结果 + output = { + 'file': args.filename, + 'functions': functions, + 'total': len(functions), + } + + print(json.dumps(output, indent=2, ensure_ascii=False)) + + except clang.cindex.TranslationUnitLoadError as e: + print(f"翻译单元加载错误: {e}") + print("这通常意味着 C++ 代码包含语法错误或缺少必要的头文件") + sys.exit(1) + except Exception as e: + print(f"错误: {e}") + sys.exit(1) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/bin/compiler.php b/bin/compiler.php index 90e3f8b0..c1b819b7 100755 --- a/bin/compiler.php +++ b/bin/compiler.php @@ -2,24 +2,59 @@ setIndent(' '); -$code = $translator->convert($file); -$info = pathinfo($file); -$cppFile = $info['dirname'] . '/' . $info['filename'] . '.cc'; -$translator->save($code, $cppFile); + +if (is_dir($path)) { + $scanner = new FileScanner($path); + $list = $scanner->scan(); + $targetFile = basename($path); +} else { + $list = [$path]; + $targetFile = basename($path, '.php'); +} + +$sourceFiles = []; +$objectFiles = []; + +// 分析 PHP 文件,生成 C++ 文件 +foreach ($list as $file) { + try { + if (str_ends_with($file, '.php')) { + $code = $translator->convert($file); + $info = pathinfo($file); + $cppFile = $info['dirname'] . '/' . $info['filename'] . '.cc'; + $translator->save($code, $cppFile); + } else { + $cppFile = $file; + } + $sourceFiles[] = $cppFile; + } catch (Unsupported $e) { + echo " skip: " . $file . "\n"; + } +} + +// 生成所有函数声明 $translator->genFunctionDeclaration("./php_func_decl.h"); -$translator->compileFile($cppFile); -$objectFile = $info['dirname'] . '/' . $info['filename'] . '.cc.o'; -if (!is_file($objectFile)) { - throw new Exception("compile error"); +// 编译所有 C++ 文件 +foreach ($sourceFiles as $cppFile) { + $translator->compileFile($cppFile); + $objectFile = $cppFile . '.o'; + if (!is_file($objectFile)) { + throw new Exception("compile error"); + } + $objectFiles[] = $objectFile; } -$translator->compileBinary($info['filename'], $objectFile); + +// 连接所有目标文件,生成可执行文件 +$translator->compileBinary($targetFile, $objectFiles); diff --git a/bin/extractor.php b/bin/extractor.php new file mode 100644 index 00000000..abb35a65 --- /dev/null +++ b/bin/extractor.php @@ -0,0 +1,157 @@ + [prefix] [output.json] + +参数: + file.cpp 要分析的 C/C++ 文件(必需) + prefix 函数名前缀,多个前缀用逗号分隔(默认: php_) + output.json 输出文件路径(默认: stdout) + +选项: + --help, -h 显示此帮助信息 + --pretty 美化 JSON 输出 + --batch 批量处理模式(从 stdin 读取文件列表) + +示例: + # 基本用法 + php extract_functions.php myfile.cpp + + # 指定前缀 + php extract_functions.php myfile.cpp php_ + + # 多个前缀 + php extract_functions.php myfile.cpp php_,swoole_,zend_ + + # 保存到文件 + php extract_functions.php myfile.cpp php_ output.json + + # 美化输出 + php extract_functions.php myfile.cpp php_ output.json --pretty + + # 批量处理 + find . -name "*.cpp" | php extract_functions.php --batch php_ + +特点: + ✓ 无需头文件 + ✓ 速度快 + ✓ 支持 C/C++ + ✓ 输出 JSON 格式 + ✓ 支持复杂的参数类型 + +USAGE; +} + +function main(array $argv): void +{ + // 解析命令行参数 + $options = [ + 'help' => false, + 'pretty' => false, + 'batch' => false, + ]; + + $args = []; + + for ($i = 1; $i < count($argv); $i++) { + $arg = $argv[$i]; + + if ($arg === '--help' || $arg === '-h') { + $options['help'] = true; + } elseif ($arg === '--pretty') { + $options['pretty'] = true; + } elseif ($arg === '--batch') { + $options['batch'] = true; + } else { + $args[] = $arg; + } + } + + // 显示帮助 + if ($options['help'] || (empty($args) && !$options['batch'])) { + showUsage(); + exit(0); + } + + // 创建提取器 + $extractor = new PhpAot\Php\Extractor(); + + // 批量模式 + if ($options['batch']) { + $prefixes = !empty($args[0]) ? explode(',', $args[0]) : ['php_']; + $files = []; + + while ($line = fgets(STDIN)) { + $file = trim($line); + if (!empty($file) && file_exists($file)) { + $files[] = $file; + } + } + + if (empty($files)) { + fprintf(STDERR, "错误: 未找到有效的文件\n"); + exit(1); + } + + $functions = $extractor->extractFromFiles($files, $prefixes); + + // 输出结果 + $jsonFlags = JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES; + if ($options['pretty']) { + $jsonFlags |= JSON_PRETTY_PRINT; + } + + echo json_encode($functions, $jsonFlags) . "\n"; + exit(0); + } + + // 单文件模式 + $filename = $args[0] ?? null; + $prefixesStr = $args[1] ?? 'php_'; + $output = $args[2] ?? null; + + if (!$filename) { + fprintf(STDERR, "错误: 未指定文件\n"); + showUsage(); + exit(1); + } + + // 解析前缀 + $prefixes = array_map('trim', explode(',', $prefixesStr)); + + try { + // 提取函数 + $functions = $extractor->extractFunctions($filename, $prefixes); + + // 准备 JSON 输出 + $jsonFlags = JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES; + if ($options['pretty']) { + $jsonFlags |= JSON_PRETTY_PRINT; + } + + $json = json_encode($functions, $jsonFlags); + + // 输出 + if ($output) { + file_put_contents($output, $json . "\n"); + fprintf(STDERR, "\033[0;32m已保存到: %s\033[0m\n", $output); + } else { + echo $json . "\n"; + } + + } catch (Exception $e) { + fprintf(STDERR, "\033[0;31m错误: %s\033[0m\n", $e->getMessage()); + exit(1); + } +} + +// 运行主函数 +if (php_sapi_name() === 'cli') { + main($argv); +} \ No newline at end of file diff --git a/src/Php/Extractor.php b/src/Php/Extractor.php new file mode 100644 index 00000000..aee60288 --- /dev/null +++ b/src/Php/Extractor.php @@ -0,0 +1,614 @@ +checkCtags(); + } + + /** + * 检查 ctags 是否可用 + */ + private function checkCtags(): void + { + $output = shell_exec("{$this->ctagsPath} --version 2>&1"); + + if ($output === null) { + $this->error("未找到 ctags 命令\n安装: sudo apt install universal-ctags"); + } + + $this->isUniversalCtags = stripos($output, 'Universal Ctags') !== false; + + if (!$this->isUniversalCtags) { + $this->warn("建议使用 Universal Ctags 以获得更好的支持"); + } + } + + /** + * 提取函数定义 + * + * @param string $filename 文件路径 + * @param array $prefixes 函数名前缀列表 + * @return array 函数列表 + */ + public function extractFunctions(string $filename, array $prefixes = ['php_']): array + { + if (!file_exists($filename)) { + throw new RuntimeException("文件不存在: {$filename}"); + } + + $this->info("分析文件: {$filename}"); + $this->info("函数前缀: " . implode(', ', $prefixes)); + + // 运行 ctags + $tags = $this->runCtags($filename); + + // 过滤和解析函数 + $functions = []; + foreach ($tags as $tag) { + if ($tag['kind'] !== 'function') { + continue; + } + + $funcName = $tag['name'] ?? ''; + + // 检查前缀 + $matched = false; + foreach ($prefixes as $prefix) { + if (str_starts_with($funcName, $prefix)) { + $matched = true; + break; + } + } + + if (!$matched) { + continue; + } + + // 解析函数详细信息 + $funcInfo = $this->parseFunction($filename, $tag); + if ($funcInfo) { + $functions[] = $funcInfo; + } + } + + $this->info("找到 " . count($functions) . " 个函数"); + + return $functions; + } + + /** + * 运行 ctags 命令 + */ + private function runCtags(string $filename): array + { + $cmd = sprintf( + '%s --output-format=json --fields=+nKSzZt --kinds-c++=f --extras=+q -f - %s 2>&1', + escapeshellcmd($this->ctagsPath), + escapeshellarg($filename) + ); + + $output = shell_exec($cmd); + + if ($output === null) { + throw new RuntimeException("ctags 执行失败"); + } + + // 解析 JSON 输出 + $tags = []; + $lines = explode("\n", trim($output)); + + foreach ($lines as $line) { + if (empty($line)) { + continue; + } + + $tag = json_decode($line, true); + if ($tag === null) { + continue; + } + + $tags[] = $tag; + } + + return $tags; + } + + /** + * 解析单个函数的详细信息 + */ + private function parseFunction(string $filename, array $tag): ?array + { + $funcName = $tag['name'] ?? ''; + $lineNum = $tag['line'] ?? 0; + + if (empty($funcName) || $lineNum < 1) { + return null; + } + + // 提取完整的函数签名 + $signature = $this->extractSignature($filename, $lineNum, $funcName); + + if (empty($signature)) { + return null; + } + + // 解析返回类型 + $returnType = $this->parseReturnType($signature, $funcName); + + // 解析参数 + $parameters = $this->parseParameters($signature, $funcName); + + return [ + 'name' => $funcName, + 'returnType' => $returnType, + 'signature' => $signature, + 'parameters' => $parameters, + 'location' => [ + 'file' => $filename, + 'line' => $lineNum + ], + 'scope' => $tag['scope'] ?? null, + 'scopeKind' => $tag['scopeKind'] ?? null + ]; + } + + /** + * 从源文件中提取完整的函数签名 + */ + private function extractSignature(string $filename, int $lineNum, string $funcName): string + { + $lines = file($filename, FILE_IGNORE_NEW_LINES); + + if ($lines === false || $lineNum > count($lines)) { + return ''; + } + + // 从函数声明行开始收集,直到遇到 { 或 ; + $signatureLines = []; + $maxLines = min($lineNum + 20, count($lines)); + + for ($i = $lineNum - 1; $i < $maxLines; $i++) { + $line = $lines[$i]; + $signatureLines[] = $line; + + // 检查是否到达函数体或声明结束 + if (strpos($line, '{') !== false || strpos($line, ';') !== false) { + break; + } + } + + // 合并并清理 + $signature = implode(' ', $signatureLines); + + // 移除 { 或 ; 之后的内容 + $signature = preg_replace('/[{;].*$/', '', $signature); + + // 合并多个空白字符 + $signature = preg_replace('/\s+/', ' ', $signature); + + // 清理首尾空白 + $signature = trim($signature); + + return $signature; + } + + /** + * 解析返回类型 + */ + private function parseReturnType(string $signature, string $funcName): string + { + // 匹配: <返回类型> <函数名>( + $pattern = '/^(.+?)\s+' . preg_quote($funcName, '/') . '\s*\(/'; + + if (preg_match($pattern, $signature, $matches)) { + $returnType = trim($matches[1]); + + // 移除可能的修饰符 + $returnType = preg_replace('/\b(static|inline|extern|virtual|explicit)\b/', '', $returnType); + $returnType = preg_replace('/\s+/', ' ', $returnType); + $returnType = trim($returnType); + + return $returnType ?: 'void'; + } + + return 'unknown'; + } + + /** + * 解析参数列表 + */ + private function parseParameters(string $signature, string $funcName): array + { + // 提取括号内的参数 + $pattern = '/' . preg_quote($funcName, '/') . '\s*\((.*?)\)/s'; + + if (!preg_match($pattern, $signature, $matches)) { + return []; + } + + $paramsStr = trim($matches[1]); + + // 空参数或 void + if (empty($paramsStr) || $paramsStr === 'void') { + return []; + } + + // 分割参数(处理嵌套的模板和括号) + $params = $this->splitParameters($paramsStr); + + $parameters = []; + foreach ($params as $param) { + $param = trim($param); + + if (empty($param)) { + continue; + } + + $paramInfo = $this->parseParameter($param); + if ($paramInfo) { + $parameters[] = $paramInfo; + } + } + + return $parameters; + } + + /** + * 智能分割参数(处理嵌套的模板和括号) + */ + private function splitParameters(string $paramsStr): array + { + $params = []; + $current = ''; + $depth = 0; + $length = strlen($paramsStr); + + for ($i = 0; $i < $length; $i++) { + $char = $paramsStr[$i]; + + if ($char === '<' || $char === '(' || $char === '[') { + $depth++; + $current .= $char; + } elseif ($char === '>' || $char === ')' || $char === ']') { + $depth--; + $current .= $char; + } elseif ($char === ',' && $depth === 0) { + $params[] = $current; + $current = ''; + } else { + $current .= $char; + } + } + + if (!empty($current)) { + $params[] = $current; + } + + return $params; + } + + /** + * 解析单个参数 + */ + private function parseParameter(string $param): ?array + { + $param = trim($param); + + // 移除默认值 + $param = preg_replace('/\s*=\s*.*$/', '', $param); + + // 尝试匹配: <类型> <名称> + // 支持复杂类型如: const char*, std::string&, int**, etc. + if (preg_match('/^(.+?)\s+(\w+)\s*$/', $param, $matches)) { + return [ + 'type' => trim($matches[1]), + 'name' => trim($matches[2]) + ]; + } + + // 只有类型,没有名称 + return [ + 'type' => $param, + 'name' => '' + ]; + } + + /** + * 批量提取多个文件 + */ + public function extractFromFiles(array $files, array $prefixes = ['php_']): array + { + $allFunctions = []; + + foreach ($files as $file) { + try { + $functions = $this->extractFunctions($file, $prefixes); + $allFunctions = array_merge($allFunctions, $functions); + } catch (Exception $e) { + $this->error("处理文件 {$file} 失败: " . $e->getMessage()); + } + } + + return $allFunctions; + } + + /** + * 输出信息 + */ + private function info(string $message): void + { + fprintf(STDERR, "\033[0;32m%s\033[0m\n", $message); + } + + /** + * 输出警告 + */ + private function warn(string $message): void + { + fprintf(STDERR, "\033[1;33m警告: %s\033[0m\n", $message); + } + + /** + * 输出错误并退出 + */ + private function error(string $message): void + { + fprintf(STDERR, "\033[0;31m错误: %s\033[0m\n", $message); + exit(1); + } + + /** + * 提取函数并添加额外信息 + */ + public function extractWithMetadata(string $filename, array $prefixes = ['php_']): array + { + $functions = $this->extractFunctions($filename, $prefixes); + + // 添加额外的元数据 + foreach ($functions as &$func) { + $func['metadata'] = $this->extractMetadata($filename, $func); + } + + return $functions; + } + + /** + * 提取函数的元数据(注释、属性等) + */ + private function extractMetadata(string $filename, array $func): array + { + $lineNum = $func['location']['line']; + $lines = file($filename, FILE_IGNORE_NEW_LINES); + + $metadata = [ + 'comments' => [], + 'attributes' => [], + 'isPHPFunction' => false, + 'isStatic' => false, + 'isInline' => false, + ]; + + // 提取函数前的注释 + $comments = $this->extractComments($lines, $lineNum); + $metadata['comments'] = $comments; + + // 检测 PHP 函数宏 + $metadata['isPHPFunction'] = $this->isPHPFunction($func['signature']); + + // 检测修饰符 + $signature = $func['signature']; + $metadata['isStatic'] = strpos($signature, 'static') !== false; + $metadata['isInline'] = strpos($signature, 'inline') !== false; + + // 提取文档注释中的标签 + $metadata['docTags'] = $this->parseDocTags($comments); + + return $metadata; + } + + /** + * 提取函数前的注释 + */ + private function extractComments(array $lines, int $lineNum): array + { + $comments = []; + $i = $lineNum - 2; // 从函数声明的前一行开始 + + // 向上查找注释 + while ($i >= 0) { + $line = trim($lines[$i]); + + // 空行 + if (empty($line)) { + $i--; + continue; + } + + // C++ 风格注释 + if (str_starts_with($line, '//')) { + array_unshift($comments, substr($line, 2)); + $i--; + continue; + } + + // C 风格注释结束 + if (str_ends_with($line, '*/')) { + $commentLines = [$line]; + $i--; + + // 继续向上查找注释开始 + while ($i >= 0) { + $commentLine = trim($lines[$i]); + array_unshift($commentLines, $commentLine); + + if (str_starts_with($commentLine, '/*')) { + break; + } + $i--; + } + + // 解析多行注释 + $comment = implode("\n", $commentLines); + $comment = preg_replace('#^/\*+\s*#', '', $comment); + $comment = preg_replace('#\s*\*+/$#', '', $comment); + $comment = preg_replace('#^\s*\*\s?#m', '', $comment); + + array_unshift($comments, trim($comment)); + $i--; + continue; + } + + // 遇到非注释行,停止 + break; + } + + return $comments; + } + + /** + * 检测是否是 PHP 函数宏定义 + */ + private function isPHPFunction(string $signature): bool + { + $phpMacros = [ + 'PHP_FUNCTION', + 'PHP_METHOD', + 'ZEND_FUNCTION', + 'ZEND_METHOD', + ]; + + foreach ($phpMacros as $macro) { + if (strpos($signature, $macro) !== false) { + return true; + } + } + + return false; + } + + /** + * 解析文档注释标签 + */ + private function parseDocTags(array $comments): array + { + $tags = []; + + foreach ($comments as $comment) { + // 匹配 @tag 格式 + if (preg_match_all('/@(\w+)\s+(.*)$/m', $comment, $matches, PREG_SET_ORDER)) { + foreach ($matches as $match) { + $tagName = $match[1]; + $tagValue = trim($match[2]); + + if (!isset($tags[$tagName])) { + $tags[$tagName] = []; + } + + $tags[$tagName][] = $tagValue; + } + } + } + + return $tags; + } + + /** + * 生成函数统计信息 + */ + public function generateStatistics(array $functions): array + { + $stats = [ + 'total' => count($functions), + 'byReturnType' => [], + 'byParameterCount' => [], + 'byPrefix' => [], + 'withComments' => 0, + 'isPHPFunction' => 0, + ]; + + foreach ($functions as $func) { + // 按返回类型统计 + $returnType = $func['returnType']; + $stats['byReturnType'][$returnType] = + ($stats['byReturnType'][$returnType] ?? 0) + 1; + + // 按参数数量统计 + $paramCount = count($func['parameters']); + $stats['byParameterCount'][$paramCount] = + ($stats['byParameterCount'][$paramCount] ?? 0) + 1; + + // 按前缀统计 + $name = $func['name']; + $prefix = preg_match('/^([a-z_]+_)/i', $name, $m) ? $m[1] : 'other'; + $stats['byPrefix'][$prefix] = + ($stats['byPrefix'][$prefix] ?? 0) + 1; + + // 有注释的函数 + if (!empty($func['metadata']['comments'])) { + $stats['withComments']++; + } + + // PHP 函数宏 + if ($func['metadata']['isPHPFunction'] ?? false) { + $stats['isPHPFunction']++; + } + } + + return $stats; + } + + /** + * 导出为 Markdown 文档 + */ + public function exportToMarkdown(array $functions, string $title = 'API 文档'): string + { + $md = "# {$title}\n\n"; + $md .= "生成时间: " . date('Y-m-d H:i:s') . "\n\n"; + $md .= "总计: " . count($functions) . " 个函数\n\n"; + $md .= "---\n\n"; + + foreach ($functions as $func) { + $md .= "## {$func['name']}\n\n"; + + // 签名 + $md .= "```c\n{$func['signature']}\n```\n\n"; + + // 返回类型 + $md .= "**返回类型**: `{$func['returnType']}`\n\n"; + + // 参数 + if (!empty($func['parameters'])) { + $md .= "**参数**:\n\n"; + foreach ($func['parameters'] as $param) { + $name = $param['name'] ?: '(unnamed)'; + $md .= "- `{$param['type']}` **{$name}**\n"; + } + $md .= "\n"; + } else { + $md .= "**参数**: 无\n\n"; + } + + // 注释 + if (!empty($func['metadata']['comments'])) { + $md .= "**说明**:\n\n"; + foreach ($func['metadata']['comments'] as $comment) { + $md .= $comment . "\n\n"; + } + } + + // 位置 + $md .= "**位置**: {$func['location']['file']}:{$func['location']['line']}\n\n"; + + $md .= "---\n\n"; + } + + return $md; + } +} \ No newline at end of file diff --git a/src/Php/FileScanner.php b/src/Php/FileScanner.php new file mode 100644 index 00000000..57343232 --- /dev/null +++ b/src/Php/FileScanner.php @@ -0,0 +1,63 @@ +directory = rtrim($directory, DIRECTORY_SEPARATOR); + $this->extensions = $extensions; + $this->excludePatterns = []; + } + + public function addExcludePattern(string $pattern): self + { + $this->excludePatterns[] = $pattern; + return $this; + } + + public function scan(): array + { + $files = []; + $iterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($this->directory, FilesystemIterator::SKIP_DOTS) + ); + + foreach ($iterator as $file) { + if ($file->isFile()) { + $extension = '.' . $file->getExtension(); + if (in_array($extension, $this->extensions)) { + $filePath = $file->getPathname(); + $excluded = false; + foreach ($this->excludePatterns as $pattern) { + if ($this->matchPattern($pattern, $filePath)) { + $excluded = true; + break; + } + } + if (!$excluded) { + $files[] = $filePath; + } + } + } + } + sort($files); + return $files; + } + + private function matchPattern(string $pattern, string $path): bool + { + return fnmatch($pattern, $path, FNM_PATHNAME); + } +} \ No newline at end of file diff --git a/src/Php/Translator.php b/src/Php/Translator.php index b02239a5..6de91202 100644 --- a/src/Php/Translator.php +++ b/src/Php/Translator.php @@ -8,6 +8,7 @@ use PhpParser\Node; use PhpParser\Node\Expr\Variable; use PhpParser\Node\Identifier; use PhpParser\Error; +use PhpParser\Node\NullableType; use PhpParser\NodeTraverser; use PhpParser\ParserFactory; use PhpParser\PrettyPrinter; @@ -256,9 +257,9 @@ class Translator extends \PhpAot\Core\Translator $this->phpxDir = $dir; } - private function doConvert(string $phpCode) + private function doConvert(string $phpCode): string { - $this->climate->info('do convert'); + $this->climate->info('do convert: ' . $this->file); $parser = (new ParserFactory())->createForNewestSupportedVersion(); $ast = $parser->parse($phpCode); @@ -317,7 +318,6 @@ class Translator extends \PhpAot\Core\Translator $this->formatCppCode($file); } - public function getLine($node): int { return $node->getLine(); @@ -389,9 +389,13 @@ class Translator extends \PhpAot\Core\Translator } } - $this->indentLevel++; - $stmts = $this->parseStmts($v->stmts); - $this->indentLevel--; + if ($v->stmts) { + $this->indentLevel++; + $stmts = $this->parseStmts($v->stmts); + $this->indentLevel--; + } else { + $stmts = ''; + } $functionDeclCode = $this->getReturnType() . ' ' . self::PREFIX . $name . '(' . $this->functionDef->params . ')'; @@ -418,9 +422,24 @@ class Translator extends \PhpAot\Core\Translator } } - private function parseScalarValue($expr): string + private function parseScalar(Node $expr) { - return $expr->getAttribute('rawValue'); + $type = $expr->getType(); + switch ($type) { + case 'Scalar_Int': + return $expr->value . 'L'; + case 'Scalar_Float': + return $this->parseScalarFloat($expr); + case 'Scalar_String': + if ($this->noLiteralStrings) { + return '"' . $this->escapeString($expr->value) . '"'; + } else { + $index = $this->literalStrings[$expr->value] ?? $this->addLiteralString($expr->value); + return self::LITERAL_STRINGS . '[' . $index . ']'; + } + default: + abort($expr); + } } private function parseIdentifier(Node $expr) @@ -433,16 +452,9 @@ class Translator extends \PhpAot\Core\Translator case 'Identifier': return $expr->name; case 'Scalar_Int': - return $expr->value . 'L'; case 'Scalar_Float': - return $this->parseScalarFloat($expr); case 'Scalar_String': - if ($this->noLiteralStrings) { - return '"' . $this->escapeString($expr->value) . '"'; - } else { - $index = $this->literalStrings[$expr->value] ?? $this->addLiteralString($expr->value); - return self::LITERAL_STRINGS . '[' . $index . ']'; - } + return $this->parseScalar($expr); case 'Expr_ConstFetch': return $this->parseConstFetch($expr); default: @@ -463,7 +475,7 @@ class Translator extends \PhpAot\Core\Translator $argInfo->type = $type; if (isset($param->default)) { $this->functionDef->argCountRequired = count($list) - 1; - $argInfo->default = $param->default->getAttribute('rawValue'); + $argInfo->default = $this->parseScalar($param->default); } $this->functionDef->argInfoList[] = $argInfo; } @@ -981,6 +993,9 @@ class Translator extends \PhpAot\Core\Translator if ($type == null) { return self::TYPE_VAR; } + if ($type instanceof NullableType) { + return self::TYPE_VAR; + } $name = $type->name; switch ($name) { case 'int': @@ -1054,12 +1069,13 @@ class Translator extends \PhpAot\Core\Translator shell_exec($cmd); } - public function compileBinary($targetFile, $objectFile): void + public function compileBinary(string $targetFile, array $objectFiles): void { $this->genGlobalVars(); if ($this->climate->arguments->defined('output')) { $targetFile = $this->climate->arguments->get('output'); } + $objectFile = implode(' ', $objectFiles); $cmd = $this->cppCompiler . ' main.cc global_vars.cc ' . $objectFile . ' -o ' . $targetFile . ' ' . $this->parseLdflags() . $this->parseLibs(); $this->addCompilationOption($cmd); $this->climate->comment($cmd); @@ -1874,7 +1890,7 @@ class Translator extends \PhpAot\Core\Translator $this->localVars = $localVars; goto _fail; } - $code .= $this->getIndent() . 'case ' . $this->parseScalarValue($case->cond) . ': {' . PHP_EOL; + $code .= $this->getIndent() . 'case ' . $this->parseScalar($case->cond) . ': {' . PHP_EOL; } $this->indentLevel++; $code .= $this->parseStmts($case->stmts); @@ -1909,8 +1925,12 @@ class Translator extends \PhpAot\Core\Translator { $list = []; foreach ($v->vars as $var) { - $type = $this->detectExprType($var->default); - $list[] = 'static ' . $type . ' ' . $this->parseIdentifier($var->var) . ' = ' . $this->parseIdentifier($var->default) . ';'; + if ($var->default) { + $type = $this->detectExprType($var->default); + $list[] = 'static ' . $type . ' ' . $this->parseIdentifier($var->var) . ' = ' . $this->parseIdentifier($var->default) . ';'; + } else { + $list[] = 'static ' . self::TYPE_VAR . ' ' . $this->parseIdentifier($var->var) . ';'; + } } return implode(PHP_EOL . $this->getIndent(), $list); } diff --git a/src/Php/Unsupported.php b/src/Php/Unsupported.php new file mode 100644 index 00000000..cc41db5f --- /dev/null +++ b/src/Php/Unsupported.php @@ -0,0 +1,8 @@ +getLang(); + $msg = 'Error: Unsupported ' . $lang . ' Syntax,'; + $msg .= ' Line: ' . $translator->getLine($v) . ', Type: ' . $translator->getType($v) . PHP_EOL; if ($translator->mode == 'cli') { - echo 'Error: Unsupported ' . $lang . ' Syntax,'; - echo ' Line: ' . $translator->getLine($v) . ', Type: ' . $translator->getType($v) . PHP_EOL; - if (DEBUG) { - debug_print_backtrace(); - var_dump($v); - } +// if (DEBUG) { +// var_dump($v); +// } } else { header('Content-Type: application/json'); echo json_encode($v, JSON_PRETTY_PRINT); } - die; + throw new Unsupported($msg); } function if_empty_debug($if_expr, $v)