StructGen: _get_struct_size/_get_align 去 opaque→8/ptr_size、未知→4 兜底并补 VectorType;LiteralStructType.fields→.elements(llvmlite 无 fields,必然 AttributeError) HandlesClassDef: union 载荷尺寸经不存在的 ir.DataLayout 静默退化成 8 字节;HandlesExprCall/ExprBuiltin: sizeof 各静默 0 分支改 fail-fast,数组元素 sizeof 统一走 _get_struct_size HandlesFor: nonlocal struct/array 堆分配不再退化成 malloc(8) vast/vast_c: 补一元 * / & 解析,装饰器后 extern/export 分派修复,C 侧报错带 token 与行号;StubGen.Converter: 宏展开丢尾随空行修 .pyi 缩进非法 其余为 rawptr 迁移 / fail-fast 口径等历史改动一并入库
374 lines
20 KiB
Python
374 lines
20 KiB
Python
from __future__ import annotations
|
||
|
||
import os
|
||
import sys
|
||
import json
|
||
import glob as glob_module
|
||
import argparse
|
||
import shutil
|
||
import subprocess
|
||
from typing import Any
|
||
|
||
from lib.core.VLogger import get_logger as _vlog
|
||
from lib.Projectrans.Phase1Generator import Phase1Generator
|
||
from lib.Projectrans.Phase2Translator import Phase2Translator
|
||
|
||
|
||
# ============================================================
|
||
# 全局 SHA1 映射内存存储器
|
||
#
|
||
# Phase1 生成 sha1_map 后写入此处(save_sha1_map),
|
||
# Phase2 从此处读取(Load_sha1_map / Phase2Translator._LoadSha1Map)。
|
||
# _sha1_map.txt 文件仅作为人类可读输出,不再作为机器分析数据源。
|
||
# ============================================================
|
||
_g_sha1_map_store: dict[str, str] = {}
|
||
|
||
|
||
def get_sha1_map_store() -> dict[str, str]:
|
||
"""返回全局 SHA1 映射内存存储器(供 Phase2Translator 直接访问)"""
|
||
return _g_sha1_map_store
|
||
|
||
|
||
def clear_sha1_map_store() -> None:
|
||
"""清空全局 SHA1 映射内存存储器"""
|
||
_g_sha1_map_store.clear()
|
||
|
||
|
||
def save_sha1_map(temp_dir: str, sha1_map: dict[str, str]) -> None:
|
||
"""将 SHA1 映射表保存到内存存储器,并输出人类可读的 _sha1_map.txt
|
||
|
||
机器分析使用内存存储器(_g_sha1_map_store),
|
||
_sha1_map.txt 仅作为人类可读输出,不再被机器读取。
|
||
"""
|
||
# 写入内存存储器(机器分析数据源)
|
||
_g_sha1_map_store.clear()
|
||
for sha1, rel in sha1_map.items():
|
||
_g_sha1_map_store[sha1] = rel
|
||
|
||
# 写入文件(仅人类可读输出)
|
||
map_path: str = os.path.join(temp_dir, '_sha1_map.txt')
|
||
with open(map_path, 'w', encoding='utf-8') as f:
|
||
for sha1, rel in sorted(sha1_map.items()):
|
||
f.write(f"{sha1}:{rel}\n")
|
||
|
||
|
||
def Load_sha1_map(temp_dir: str) -> dict[str, str]:
|
||
"""从内存存储器加载 SHA1 映射表
|
||
|
||
temp_dir 参数保留用于签名兼容性,但不再读取文件。
|
||
机器分析使用内存存储器(_g_sha1_map_store)。
|
||
"""
|
||
result: dict[str, str] = {}
|
||
for sha1, rel in _g_sha1_map_store.items():
|
||
result[sha1] = rel
|
||
return result
|
||
|
||
|
||
def Load_project_config(project_file: str) -> dict[str, Any]:
|
||
"""从 project.json 加载配置"""
|
||
with open(project_file, 'r', encoding='utf-8') as f:
|
||
return json.load(f)
|
||
|
||
|
||
def resolve_paths(config: dict[str, Any], project_file: str) -> dict[str, Any]:
|
||
"""将配置中的相对路径转换为绝对路径"""
|
||
project_dir: str = os.path.dirname(os.path.abspath(project_file))
|
||
result: dict[str, Any] = dict(config)
|
||
|
||
def resolve(p: str | None) -> str | None:
|
||
if p is None:
|
||
return None
|
||
if os.path.isabs(p):
|
||
return p
|
||
return os.path.normpath(os.path.join(project_dir, p))
|
||
|
||
if 'source_dir' in result:
|
||
result['source_dir'] = resolve(result.get('source_dir'))
|
||
elif 'sources' in result:
|
||
resolved_sources: list[str] = []
|
||
for src in result['sources']:
|
||
if '*' in src or '?' in src:
|
||
for matched in glob_module.glob(os.path.join(project_dir, src), recursive=True):
|
||
if os.path.isfile(matched):
|
||
resolved_sources.append(matched)
|
||
else:
|
||
resolved_sources.append(resolve(src))
|
||
result['sources'] = [s for s in resolved_sources if os.path.isfile(s)]
|
||
|
||
# 直接使用 output_dir / temp_dir(与新版本 TPV 一致,不再支持 build_dir)
|
||
result['temp_dir'] = resolve(config.get('temp_dir', './temp'))
|
||
result['output_dir'] = resolve(config.get('output_dir', './output'))
|
||
|
||
if 'includes' in result:
|
||
result['includes'] = [resolve(inc) for inc in result['includes']]
|
||
|
||
result['_project_dir'] = project_dir
|
||
return result
|
||
|
||
|
||
def main() -> None:
|
||
parser: argparse.ArgumentParser = argparse.ArgumentParser(
|
||
description='TransPyC 工程翻译器 - 基于 project.json 的两阶段翻译',
|
||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||
epilog='''
|
||
示例:
|
||
# 从 project.json 读取配置并执行(默认查找当前目录)
|
||
python ProjectTrans.py
|
||
|
||
# 指定 project.json 文件
|
||
python ProjectTrans.py --project ./TestProject/project.json
|
||
|
||
# 使用命令行参数覆盖 project.json 配置
|
||
python ProjectTrans.py --src ./src --temp ./decl --output ./out --phase all
|
||
|
||
# 仅运行阶段一
|
||
python ProjectTrans.py --phase 1
|
||
|
||
# 仅运行阶段二
|
||
python ProjectTrans.py --phase 2
|
||
|
||
# 清理临时目录
|
||
python ProjectTrans.py --clean
|
||
|
||
# 编译并立即执行
|
||
python Projectrans.py --project ./TestProject/project.json --clean --run
|
||
|
||
# 删除 includes.binary 预编译缓存并重新编译所有 includes
|
||
python Projectrans.py --project ./TestProject/project.json --rebuild-includes
|
||
'''
|
||
)
|
||
parser.add_argument('--project', help='project.json 路径(默认查找当前目录)')
|
||
parser.add_argument('--src', help='源文件目录(覆盖 project.json)')
|
||
parser.add_argument('--temp', help='声明接口临时目录(覆盖 project.json)')
|
||
parser.add_argument('--output', help='输出目录(覆盖 project.json)')
|
||
parser.add_argument('--phase', choices=['1', '2', 'all'], help='阶段: 1=生成声明, 2=翻译+编译, all=全部')
|
||
parser.add_argument('--cc', help='LLVM 编译器命令(覆盖 project.json)')
|
||
parser.add_argument('--clean', action='store_true', help='清理 output 和 temp 目录')
|
||
parser.add_argument('--run', action='store_true', help='编译成功后立即执行生成的可执行文件')
|
||
parser.add_argument('--rebuild-includes', action='store_true', help='删除 includes.binary 预编译缓存并重新编译所有 includes')
|
||
parser.add_argument('--clear-cache', action='store_true', help='清除 .transpyc_cache 全局缓存(.vpi/.stub.ll/.doc.json)')
|
||
parser.add_argument('--asan', action='store_true', help='ASan 模式:对全部 .ll 做 opt -passes=asan 插桩,并显式链接 clang64 的 ASan 运行库(mingw64 的 clang 没有 x86_64-w64-windows-gnu 目标的 ASan runtime,不能用 -fsanitize=address)')
|
||
parser.add_argument('--dwarf', action='store_true', help='函数级 DWARF 调试元数据:每模块一份 DICompileUnit/DIFile,每个 define 一份 DISubprogram 并挂 !dbg,函数入口第一条指令挂 DILocation。使 ASan/llvm-symbolizer 能把崩溃地址反查为 "<文件>.vp:函数起始行"(默认关闭;--asan 时自动开启)。开启时强制重译 App 模块(不复用不含 !llvm.dbg.cu 的旧 .ll/.obj),.ll/.obj 体积与编译时间略增')
|
||
parser.add_argument('--traceback', action='store_true', help='Ctrl+C 调用栈打印(仅 Windows/PE):链接完成后用 llvm-dwarfdump 把最终 exe 的 .debug_line/.debug_info 导出为 "<exe>.stackmap" 地址→源行旁文件(小端定长格式),供 exe 内运行时在 Ctrl+C/Ctrl+Break 时把 CRT 到当前的返回地址逐帧翻译为 "<文件>.vp:行/函数"。会自动开启 DWARF(同 --asan,否则行表为空);找不到 llvm-dwarfdump / 调用失败 / 解析不到任何行记录 / 目标不是 Windows PE 时直接报错终止编译')
|
||
parser.add_argument('--nowarn-register', action='store_true', help='抑制"外部函数返回类型未登记"类 WARN 输出(透传给自举编译的 TransPyV.exe)')
|
||
parser.add_argument('--hotspot-sites', nargs='?', const='', default=None, help='调用点热点统计:为每个匹配白名单的直接 call 点插入计数器,运行时 Ctrl+C 各模块按真实次数降序写入 <path>(默认为 hotspot_sites.txt);同时为每个函数定义加计时器,按自身耗时(self)降序写入同目录 hotspot_time.txt,行为 `<函数名> <self> <inc> <进入次数>`(单位 tick,x86_64 上为序列化 lfence; rdtsc);self 已扣除被插桩子调用的 inc,但不含未插桩被调方(间接调用、strcmp/memcpy 等无定义外部函数),它们计入调用者的 self')
|
||
parser.add_argument('--hotspot-callees', default=None, help='--hotspot-sites 的白名单被调函数基名(逗号分隔),默认内存/字符串族 strcmp,strncmp,strchr,strlen,strstr,memcpy,memset,memmove,strcpy,strcat')
|
||
parser.add_argument('--hotspot-sites-all', action='store_true', help='--hotspot-sites 对全部直接 call 点插桩(忽略白名单;体积/开销大,用于深度排查)')
|
||
|
||
args: argparse.Namespace = parser.parse_args()
|
||
|
||
# ASan 模式(--asan):直接经 args 传给 Phase2Translator(compile_ll_to_obj
|
||
# 先 opt -passes=asan 插桩,链接显式 -l: clang64 的 libclang_rt.asan_dynamic*)。
|
||
# 不再用 TPC_ASAN 环境变量透传——环境变量在 worker 子进程/外部调用链下不可靠。
|
||
if args.asan:
|
||
_vlog().info('ASan 模式已启用(opt -passes=asan + clang64 libclang_rt.asan_dynamic*)', 'asan')
|
||
|
||
# Ctrl+C 调用栈(--traceback):链接后用 llvm-dwarfdump 导出 <exe>.stackmap。
|
||
# 不在此处做工具/平台校验——真正的 fail-fast 落在链接成功后的旁文件导出步骤
|
||
# (那里才有最终 exe 路径可判 PE)。此处仅提示已启用。
|
||
if args.traceback:
|
||
_vlog().info('Ctrl+C 调用栈模式已启用(链接后用 llvm-dwarfdump 导出 <exe>.stackmap)', 'traceback')
|
||
|
||
# 函数级 DWARF 调试元数据(--dwarf):默认关闭(硬要求——普通构建不承担
|
||
# 任何风险,生成的 .ll 与改动前逐字节一致)。--asan 时自动开启:ASan 报告
|
||
# 本身就需要行号才能定位,且 ASan 构建已是显式选择的非默认路径。
|
||
# --traceback 同理自动开启:旁文件的"地址→源行"完全来自 DWARF 行表,
|
||
# 没有 DI 元数据时 .debug_line 为空、旁文件必为空。
|
||
dwarf_enabled: bool = bool(args.dwarf or args.asan or args.traceback)
|
||
if dwarf_enabled:
|
||
_auto_src: str = '--asan' if args.asan else ('--traceback' if args.traceback else '')
|
||
_vlog().info(
|
||
'函数级 DWARF 调试元数据已启用:每个 .vp 模块一份 DICompileUnit + 每函数 DISubprogram/DILocation'
|
||
+ (f'(由 {_auto_src} 自动开启)' if _auto_src and not args.dwarf else ''),
|
||
'dwarf')
|
||
|
||
project_file: str | None = args.project
|
||
if not project_file:
|
||
candidates: list[str] = ['project.json', './project.json']
|
||
for c in candidates:
|
||
if os.path.exists(c):
|
||
project_file = c
|
||
break
|
||
|
||
if project_file and os.path.exists(project_file):
|
||
_vlog().info(f"读取配置文件: {project_file}")
|
||
config: dict[str, Any] = Load_project_config(project_file)
|
||
project_dir: str = os.path.dirname(os.path.abspath(project_file))
|
||
_vlog().info(f"项目目录: {project_dir}")
|
||
else:
|
||
_vlog().error("未找到 project.json,请使用 --project 指定")
|
||
sys.exit(1)
|
||
|
||
resolved: dict[str, Any] = resolve_paths(config, project_file)
|
||
src: str | list[str] | None = args.src or resolved.get('source_dir') or resolved.get('sources')
|
||
temp_dir: str | None = args.temp or resolved.get('temp_dir')
|
||
output_dir: str | None = args.output or resolved.get('output_dir')
|
||
phase: str = args.phase or 'all'
|
||
cc_cmd: str = args.cc or resolved.get('compiler', {}).get('cmd', 'llc')
|
||
cc_flags: list[str] = resolved.get('compiler', {}).get('flags', ['-filetype=obj'])
|
||
slice_level: int = resolved.get('options', {}).get('slice_level', 3)
|
||
project_include_dirs: list[str] = resolved.get('includes', [])
|
||
# includes 只认项目配置:同步到 Phase2Translator.PROJECT_INCLUDE_DIRS,
|
||
# 供 builtins.vp 扫描等静态路径在 phase1/2 全程使用(禁止仓库硬编码回退)
|
||
try:
|
||
from lib.Projectrans.Phase2Translator import Phase2Translator
|
||
Phase2Translator.PROJECT_INCLUDE_DIRS = [os.path.abspath(d) for d in project_include_dirs if os.path.isdir(os.path.join(project_dir, d)) or os.path.isdir(d)]
|
||
except Exception:
|
||
pass
|
||
target_triple: str | None = resolved.get('target', {}).get('triple')
|
||
target_datalayout: str | None = resolved.get('target', {}).get('datalayout')
|
||
# 调用点热点统计(--hotspot-sites):None=未启用;''=启用但未给路径(默认
|
||
# hotspot_sites.txt);其余为显式路径。一律绝对化,保证运行时(exe 工作目录为
|
||
# output_dir)各模块处理器追加写入的路径与预期一致。
|
||
hotspot_sites_file: str | None = args.hotspot_sites
|
||
if hotspot_sites_file is not None:
|
||
if hotspot_sites_file == '':
|
||
hotspot_sites_file = 'hotspot_sites.txt'
|
||
hotspot_sites_file = os.path.abspath(hotspot_sites_file)
|
||
# 白名单覆盖:逗号分隔 → 去空白后的基名列表(None=用 BaseGen 默认白名单)
|
||
hotspot_callees: list[str] | None = None
|
||
if args.hotspot_callees:
|
||
hotspot_callees = [c.strip() for c in args.hotspot_callees.split(',') if c.strip()]
|
||
|
||
if isinstance(src, list):
|
||
if len(src) == 1 and os.path.isdir(src[0]):
|
||
src = src[0]
|
||
else:
|
||
src = src[0] if len(src) == 1 else os.path.dirname(os.path.commonpath(src)) if all(os.path.exists(p) for p in src) else src[0]
|
||
|
||
_vlog().info(f"源目录: {src}")
|
||
_vlog().info(f"临时目录: {temp_dir}")
|
||
_vlog().info(f"输出目录: {output_dir}")
|
||
_vlog().info(f"阶段: {phase}")
|
||
_vlog().info(f"编译器: {cc_cmd} {' '.join(cc_flags)}")
|
||
if target_triple:
|
||
_vlog().info(f"目标三元组: {target_triple}")
|
||
if target_datalayout:
|
||
_vlog().info(f"数据布局: {target_datalayout}")
|
||
if hotspot_sites_file:
|
||
_vlog().info(f"调用点热点统计: {hotspot_sites_file}(按真实调用次数降序落盘,含 main 的模块启动时清空一次);每函数计时: {os.path.join(os.path.dirname(hotspot_sites_file), 'hotspot_time.txt')}")
|
||
|
||
# 每次编译自动清理子进程性能剖析输出(PROF 目录 *.prof,硬编码路径)
|
||
from lib.Projectrans.Phase2Translator import _CleanProfFiles, PROF_DIR
|
||
_CleanProfFiles()
|
||
_vlog().info(f"PROF 目录已清理: {PROF_DIR}")
|
||
|
||
if args.clean:
|
||
if output_dir and os.path.exists(output_dir):
|
||
for f in os.listdir(output_dir):
|
||
fpath: str = os.path.join(output_dir, f)
|
||
try:
|
||
if os.path.isfile(fpath) or os.path.islink(fpath):
|
||
os.remove(fpath)
|
||
elif os.path.isdir(fpath):
|
||
shutil.rmtree(fpath, ignore_errors=True)
|
||
except:
|
||
pass
|
||
_vlog().info(f"output: 已清空 {output_dir}")
|
||
if temp_dir and os.path.exists(temp_dir):
|
||
for f in os.listdir(temp_dir):
|
||
fpath: str = os.path.join(temp_dir, f)
|
||
try:
|
||
if os.path.isfile(fpath) or os.path.islink(fpath):
|
||
os.remove(fpath)
|
||
elif os.path.isdir(fpath):
|
||
shutil.rmtree(fpath, ignore_errors=True)
|
||
except:
|
||
pass
|
||
_vlog().info(f"temp: 已清空 {temp_dir}")
|
||
_vlog().info("清理完成")
|
||
if (not phase or phase == 'all') and not args.project:
|
||
return
|
||
|
||
if args.clear_cache:
|
||
# 清除 .transpyc_cache 全局缓存
|
||
cache_dir: str = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), '.transpyc_cache')
|
||
if os.path.isdir(cache_dir):
|
||
shutil.rmtree(cache_dir, ignore_errors=True)
|
||
os.makedirs(cache_dir, exist_ok=True)
|
||
_vlog().info(f"已清除全局缓存: {cache_dir}")
|
||
else:
|
||
_vlog().info(f"全局缓存目录不存在: {cache_dir}")
|
||
|
||
if phase in ('1', 'all'):
|
||
if isinstance(src, list):
|
||
_vlog().error("阶段一需要指定源目录,不支持文件列表")
|
||
sys.exit(1)
|
||
all_include_dirs: list[str] = list(project_include_dirs) if project_include_dirs else []
|
||
gen: Phase1Generator = Phase1Generator(src, temp_dir, include_dirs=all_include_dirs, target_triple=target_triple, target_datalayout=target_datalayout)
|
||
gen.run()
|
||
save_sha1_map(temp_dir, gen.sha1_map)
|
||
|
||
if phase in ('2', 'all'):
|
||
if not output_dir:
|
||
_vlog().error("阶段二需要指定 --output 目录")
|
||
sys.exit(1)
|
||
if not os.path.exists(temp_dir):
|
||
_vlog().error(f"声明目录不存在,请先运行阶段一: {temp_dir}")
|
||
sys.exit(1)
|
||
if isinstance(src, list):
|
||
_vlog().error("阶段二需要指定源目录,不支持文件列表")
|
||
sys.exit(1)
|
||
linker_cmd: str | None = resolved.get('linker', {}).get('cmd')
|
||
linker_flags: list[str] = resolved.get('linker', {}).get('flags', [])
|
||
linker_output: str | None = resolved.get('linker', {}).get('output')
|
||
# 显式静态链接文件列表(.a/.obj 等),相对 project.vpj 目录解析为绝对路径
|
||
static_libs: list[str] = []
|
||
for lib in resolved.get('linker', {}).get('static_libs', []) or []:
|
||
static_libs.append(lib if os.path.isabs(lib) else os.path.normpath(os.path.join(project_dir, lib)))
|
||
# 显式动态链接文件列表(.dll/.so 等),相对 project.vpj 目录解析为绝对路径;
|
||
# 链接时作为输入文件(GNU ld 从 DLL/SO 提取导入符号),运行时需 DLL/SO 在搜索路径
|
||
dynamic_libs: list[str] = []
|
||
for lib in resolved.get('linker', {}).get('dynamic_libs', []) or []:
|
||
dynamic_libs.append(lib if os.path.isabs(lib) else os.path.normpath(os.path.join(project_dir, lib)))
|
||
target_triple = resolved.get('target', {}).get('triple')
|
||
target_datalayout = resolved.get('target', {}).get('datalayout')
|
||
force_recompile_includes: bool = bool(resolved.get('options', {}).get('force_recompile_includes', False))
|
||
if args.rebuild_includes:
|
||
force_recompile_includes = True
|
||
all_inc_dirs: list[str] = list(project_include_dirs)
|
||
seen_dirs: set[str] = set()
|
||
for inc_dir in all_inc_dirs:
|
||
abs_inc: str = os.path.abspath(inc_dir)
|
||
if abs_inc in seen_dirs:
|
||
continue
|
||
seen_dirs.add(abs_inc)
|
||
if not os.path.isdir(abs_inc):
|
||
continue
|
||
inc_parent: str = os.path.dirname(abs_inc)
|
||
inc_base: str = os.path.basename(abs_inc)
|
||
precompiled_dir: str = os.path.join(inc_parent, inc_base + '.binary')
|
||
if os.path.isdir(precompiled_dir):
|
||
cleared: int = 0
|
||
for fname in os.listdir(precompiled_dir):
|
||
fpath: str = os.path.join(precompiled_dir, fname)
|
||
try:
|
||
if os.path.isfile(fpath) or os.path.islink(fpath):
|
||
os.remove(fpath)
|
||
cleared += 1
|
||
except:
|
||
pass
|
||
_vlog().info(f"includes.binary: 已清空 {precompiled_dir} ({cleared} 个文件)")
|
||
trans: Phase2Translator = Phase2Translator(src, temp_dir, output_dir, compile_cmd=cc_cmd, compile_flags=cc_flags, linker_cmd=linker_cmd, linker_flags=linker_flags, linker_output=linker_output, static_libs=static_libs, dynamic_libs=dynamic_libs, include_dirs=project_include_dirs, target_triple=target_triple, target_datalayout=target_datalayout, force_recompile_includes=force_recompile_includes, asan_mode=args.asan, dwarf_enabled=dwarf_enabled, traceback_enabled=args.traceback)
|
||
trans.sha1_map = Load_sha1_map(temp_dir)
|
||
trans.slice_level = slice_level
|
||
# 调用点热点统计与每函数计时(--hotspot-sites):None=未启用(零影响)
|
||
trans.hotspot_sites_file = hotspot_sites_file
|
||
trans.hotspot_sites_all = bool(args.hotspot_sites_all)
|
||
trans.hotspot_callees = hotspot_callees
|
||
trans.run()
|
||
|
||
if args.run and linker_output and not linker_output.endswith('.bin'):
|
||
exe_path: str = os.path.join(output_dir, linker_output)
|
||
if os.path.isfile(exe_path):
|
||
_vlog().info(f"\n[运行] 执行: {exe_path}")
|
||
try:
|
||
result: subprocess.CompletedProcess = subprocess.run([exe_path], cwd=output_dir)
|
||
_vlog().info(f"[运行] 退出码: {result.returncode}")
|
||
except Exception as e:
|
||
_vlog().error(f"[运行] 执行失败: {e}")
|
||
sys.exit(1)
|
||
else:
|
||
_vlog().error(f"[运行] 找不到可执行文件: {exe_path}")
|
||
sys.exit(1)
|