Files
ViperC/App/lib/Phase/BuildPipeline.vp
T
TermiNexus 0ebe2e0752 feat(backend): 新增 MLIR 后端开关/产物隔离/降级管线/模块别名(默认 llvm 逐字不变)
- Config: BackendTarget/BackendIsMlir/set_backend/is_mlir_backend/backend_dir_suffix/alias_module_name;读取 options.backend(回退 options.target)

- main.vp: 新增 --backend llvm|mlir(CLI 优先级最高,默认 llvm),加载工程后、resolve_paths 前生效

- 产物隔离: temp/output 追加 _mlir 后缀;includes 缓存追加后端后缀(.binary[_asan][_mlir])

- BuildPipeline: MLIR 降级 mlir-opt(校验) -> mlir-translate --mlir-to-llvmir,覆盖串行/并行/--phase2command,且置于 ASan opt 之前;LlcTask 增 MlirOptCmd/MlirTranslateCmd 字段

- 别名: ResolveModuleToPath 与 _FindSha1ByModName 经 Config.alias_module_name 把 llvmlite 重定向到 mlirlite(调用点零改动)
2026-10-03 07:53:04 +08:00

2398 lines
102 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import lib.core.Gen.GenApi as GenApi
import t, c
from stdint import *
import stdio
import string
import stdlib
import memhub
import viperlib
import vthreading
import w32.fileio as fileio
import w32.win32file
import w32.win32base
import w32.win32process as win32process
import w32.win32sync as win32sync
import subprocess
import viperlib
import ast
import lib.core.VLogger as VLogger
import lib.core.Gen.Translator as HandlesTranslator
import lib.core.Gen.DebugInfoGen as DebugInfoGen
import lib.Stub.StubMerger as StubMerger
import lib.Stub.StubGen.Converter as StubConverter
import lib.Projectrans.Config as Config
import lib.core.Gen.Generator as GenGenerator
import lib.core.Gen.Expr.ExprGenCls as ExprGenCls
import lib.Stub.StubMerger.StubMergerCollect as StubMergerCollect
# ============================================================
# BuildPipeline - 编译管线
#
# 负责将 LLVM IR 编译为可执行文件:
# 1. 写 .ll 文件到 temp 目录
# 2. 调用 llc 编译 .ll → .obj
# 3. 调用 clang++ 链接 .obj → .exe
# ============================================================
# 全局 mbuddy 指针
_mbuddy: memhub.MemBuddy*
# ASan 模式(--asan):对 .ll 先 opt -passes=asan 插桩;ASan 运行库由
# project.vpj 的 linker.static_libs / dynamic_libs 显式声明(TPV 不注入)
_asan_mode: bool = 0
# Traceback 模式(--traceback):强制编入 includes/traceback.vp 并注入启动期
# Install() 构造器;llc 保留帧指针(-frame-pointer=all);链接 dbghelp;
# 链接成功后导出 <exe>.stackmap 地址→源行旁文件。
_traceback_mode: bool = 0
# --phase2command xxx.bat:不执行 llc/链接命令,只把 Phase B/B+/C 的
# llc(含 ASan opt)与 clang 链接命令写入指定 .bat 路径(由用户手动执行)。
# None = 正常执行(默认)。
_phase2_cmd_path: str = None
# bat 文件是否已写入 header(@echo off),首次写入时置 1
_phase2_bat_init: int = 0
# MLIR 后端(Config.is_mlir_backend())降级步骤:mlirlite 发射的是 MLIR 文本,
# 而 opt/llc 只接受 LLVM IR,故在两者之前插入:
# mlir-opt <in> -o <in>.mlir.opt (语法/语义校验)
# mlir-translate --mlir-to-llvmir <in>.mlir.opt -o <in>.ll.local (降级回 LLVM IR)
# 降级产物 <...>.ll.local 与输入 .ll 同目录,不占用 <sha1>.ll 缓存键。
MLIR_OPT_CMD: str = "mlir-opt"
MLIR_TRANSLATE_CMD: str = "mlir-translate"
def _ir_sibling_path(ir_path: str, suffix: str) -> str:
"""由 <...>.ll 派生同目录邻居路径:去掉末尾 ".ll" 后拼 suffix(如 ".mlir.opt")。
从 _mbuddy 分配(与 build_sliced_path 同池,随 pool reset 回收,无需手动 free)。
"""
if ir_path is None or suffix is None:
return None
n: sizet = string.strlen(ir_path)
if n < 3:
return None
suf_len: sizet = string.strlen(suffix)
buf: str = _mbuddy.alloc(n + suf_len + 1)
if buf is None:
return None
string.strncpy(buf, ir_path, n - 3)
buf[n - 3] = '\0'
string.strcat(buf, suffix)
return buf
# 源代码缓冲区大小(1MB)
SRC_BUF_SIZE: t.CDefine = 1048576 # 2^20 = 1MB
# .vpi 存根缓冲区大小(256KB,与 Phase1 一致)
PYI_BUF_SIZE: sizet = 262144
# ============================================================
# _llc_frame_ptr_flag - llc extra flag for --traceback
#
# Returns " -frame-pointer=all" (leading space so it concatenates into the llc
# flag sequence) while --traceback is on; "" otherwise, keeping the llc command
# byte-identical to the pre-switch behaviour.
# ============================================================
def _llc_frame_ptr_flag() -> str:
if _traceback_mode != 0:
return " -frame-pointer=all"
return ""
# ============================================================
# _append_traceback_dbghelp - append -ldbghelp for --traceback
#
# traceback.vp's main walk path uses StackWalk64 / SymInitialize /
# SymFunctionTableAccess64 / SymGetModuleBase64 (all in dbghelp); without this
# flag the link fails with undefined symbols. Libraries must follow the objects,
# so this is appended to linker_flags (not prepended).
# ============================================================
def _append_traceback_dbghelp(linker_flags: str) -> str:
if _traceback_mode == 0 or linker_flags is None:
return linker_flags
need: sizet = string.strlen(linker_flags) + 16
buf: str = _mbuddy.alloc(need)
if buf is None:
VLogger.error("--traceback: -ldbghelp 链接参数缓冲分配失败,终止编译", "TRACEBACK")
return linker_flags
viperlib.snprintf(buf, need, "%s -ldbghelp", linker_flags)
return buf
# ============================================================
# _tb_basename_is_traceback - is this source file includes/traceback.vp
# ============================================================
def _tb_basename_is_traceback(file_path: str) -> int:
if file_path is None:
return 0
flen: sizet = string.strlen(file_path)
if flen < 12:
return 0
if flen > 12:
prev: char = file_path[flen - 13]
if prev != '/' and prev != '\\':
return 0
return 1 if string.strcmp(file_path + (flen - 12), "traceback.vp") == 0 else 0
# ============================================================
# TranslateFileGetTrans - 翻译单个文件,返回 Translator 对象(供调用者 dump_ir)
#
# 读取文件 → AST 解析 → .vpi 生成 → LLVM IR 翻译。命名空间隔离:includes 文件宽松模式(全部可见),用户文件严格模式(仅本地+import)。
#
# Args: mb 内存池;file_path 源文件路径;sha1_val 文件内容的 SHA1 前16字符(用于函数名混淆);current_package 当前文件所属包名(用于解析相对导入),None 表示顶级模块;declare_only 0=全量翻译(默认),1=仅注册 struct/enum/union(不翻译方法体);temp_dir temp 目录路径(非 None 时在翻译前生成 .vpi 存根文件)
# Returns: Translator 对象(None 失败)
# ============================================================
def TranslateFileGetTrans(mb: memhub.MemBuddy*, file_path: str,
sha1_val: str,
current_package: str = None,
declare_only: int = 0,
temp_dir: str = None,
pre_allow_dep_inject: int = 0) -> rawptr[HandlesTranslator.Translator]:
"""翻译文件,返回 Translator 对象(调用者负责 dump_ir),None 失败
current_package: 当前文件所属包名(用于解析相对导入),None 表示顶级模块
declare_only: 0=全量翻译(默认),1=仅注册 struct/enum/union(不翻译方法体)
temp_dir: temp 目录路径(非 None 时在翻译前生成 .vpi 存根文件)
pre_allow_dep_inject: 仅 **App 预注册**(Phase A-pre)传 1:声明期也处理导入语句并
注入依赖 .vpi,使类字段注解与阶段 A 同上下文(详见 Translator._pre_allow_dep_inject)。
includes 预注册(Phase A-pre-inc)必须保持 0。
"""
if file_path is None: return None
f: rawptr[fileio.File] = fileio.File(file_path, fileio.MODE.R)
if f.closed: return None
# read_all 的容量参数含终止符(最多读 cap-1 字节), 文件更长会被**静默截断**;
# 这里先按文件大小拒绝, 否则拿截断后的源码去 parse 只会得到莫名其妙的语法错误
if f.size() >= SRC_BUF_SIZE:
VLogger.error(f"源文件超过 {SRC_BUF_SIZE} 字节上限, 拒绝读取: {file_path}", "fileio")
f.close()
return None
src_buf: bytes = stdlib.malloc(SRC_BUF_SIZE)
if src_buf is None:
f.close()
return None
bytes_read: LONG = f.read_all(src_buf, SRC_BUF_SIZE)
f.close()
if bytes_read <= 0:
stdlib.free(src_buf)
return None
if bytes_read < SRC_BUF_SIZE:
src_buf[bytes_read] = 0
lx: ast.Lexer* = ast.new_lexer(mb)
ast._lexer_init(lx, src_buf, mb)
tokens: ast.Token* = ast.tokenize(lx)
tree: ast.AST* = ast.parse_tokens(mb, tokens)
# parse 完成后 AST 树已独立,释放源代码缓冲区(避免 1MB/文件泄漏)
stdlib.free(src_buf)
if tree is None:
# fail-fast 定位:以 Python 风格输出解析错误 traceback(仿 fatal_error)
# Traceback (most recent call last):
# File "<path>", line N
# <source line>
# SyntaxError: <message>
GenApi.dump_parse_traceback(file_path)
err_line: int = ast.get_parse_err_line()
err_col: int = ast.get_parse_err_col()
VLogger.error(f"解析失败: {file_path} (line {err_line} col {err_col})", "PARSE")
return None
# 生成 .vpi 存根文件(从 AST 直接生成,在 tr.translate 之前,与 Phase1 一致)
# .vpi 供其他模块跨模块查询函数签名和 CTypedef 定义
if temp_dir is not None:
pyi_buf: bytes = stdlib.malloc(PYI_BUF_SIZE)
if pyi_buf is not None:
pyi_buf[0] = 0
# rel_path 传 file_path(本文件绝对路径):vpi 生成期间 fatal_error
# 的报错归因需指向本源文件(Phase A 预注册/翻译时 file_path 已可用),
# 且绝对路径可被 fatal_error 的 _read_source_line 直接打开打印源码行。
pyi_pos: sizet = StubConverter._GeneratePyiFromAst(mb, tree, file_path, pyi_buf, PYI_BUF_SIZE)
if pyi_pos > 0:
# 写入 {sha1}.vpi 到 temp_dir(切片路径)
td_len_vpi: sizet = len(temp_dir)
pyi_path: str = StubMergerCollect._sliced_path(temp_dir, td_len_vpi, sha1_val, "vpi")
if pyi_path is not None:
pf: rawptr[fileio.File] = fileio.File(pyi_path, fileio.MODE.W)
if not pf.closed:
pf.write(pyi_buf, pyi_pos)
pf.close()
stdlib.free(pyi_path)
stdlib.free(pyi_buf)
tr: rawptr[HandlesTranslator.Translator] = mb.alloc(HandlesTranslator.Translator.__sizeof__())
tr.__before_init__()
tr.__init__()
tr.ModuleSha1 = sha1_val
tr.CurrentPackage = current_package
tr._declare_only = declare_only
# App 预注册(Phase A-pre)专用开关:置 1 时声明期也做导入处理 + 依赖 vpi 注入
# (详见 Translator._pre_allow_dep_inject)。includes 预注册(Phase A-pre-inc)
# 必须传 0,否则依赖环自锁(见 Translator 内 _declare_only==0 门处注释)。
tr._pre_allow_dep_inject = pre_allow_dep_inject
# P2: 复用全局 gen 单例(App/main.vp 创建;HandlesType 等旧模块函数经
# 延迟 import 委托到 gen 方法)。单例未注册时兜底创建(防御直接调用路径)。
gen: rawptr[GenGenerator.Generator] = GenGenerator._g_gen
if gen is None:
gen = mb.alloc(GenGenerator.Generator.__sizeof__())
if gen is None:
VLogger.error("Generator alloc failed", "GEN")
return None
gen.__before_init__()
gen.__init__(mb)
GenGenerator._g_gen = gen
# P2-P5: 注入各 Handles 模块级单例(委托函数经本地 _g_gen 访问,
# 避免函数体 import 包模块与类/模块名歧义)
GenApi._g_gen = gen
# P6: 注入语句/赋值/变量/导入/翻译驱动模块的单例
# 设置当前文件名(供报错使用)
GenApi.set_current_file_type(file_path)
# --traceback: mirror the switch (BuildPipeline._traceback_mode) into the
# Gen-layer DWARF emitter. 0 => every DI entry point is a no-op and the IR
# stays byte-identical to the pre-DWARF output.
DebugInfoGen.SetEnabled(_traceback_mode)
# 同步到 HandlesImports(import 时 SHA1 检查报错需要当前文件名,循环依赖无法直接读 HandlesType)
GenApi.set_current_file_imports(file_path)
# 清空调用栈(每个文件翻译开始前重置)
GenApi.clear_trace()
# 模块切换:清空 CDefine 常量表,确保每个模块的 CDefine 常量正确隔离
GenApi.clear_cdefine_constants()
# 模块切换:清空 CTypedef 表,确保每个模块的 CTypedef 正确隔离
GenApi.clear_ctypedefs()
# 命名空间隔离:includes 文件宽松模式(全部可见),用户文件严格模式(仅本地+import)
strict_mode: int = 1
if string.strstr(file_path, "includes") is not None:
strict_mode = 0
GenApi.reset_visible_structs(mb, strict_mode)
# 提取 docstring(预注册阶段所有 App/includes 文件都被翻译,保证跨模块 __doc__ 在正式翻译前已登记;查重幂等)
# 必须在 tr.translate 之前扫描:translate 期间 mb 池大量分配 IR 节点,AST 节点所在池块可能被复用覆盖(ASan 下遍历 tree 会崩溃)。
if gen is not None:
gen.ExprGenCls.extract_module_docstrings(tree, sha1_val)
ret: int = tr.translate(tree)
if ret != 0:
VLogger.error(f"文件翻译失败: {file_path}", "Translate")
return None
# --traceback: when this file is includes/traceback.vp, inject the startup
# Install() ctor into its IR (before the caller dumps stub/text.ll). The
# injector lives in the Gen layer, which already depends on llvmlite, so
# BuildPipeline gains no cross-module llvmlite symbol dependency.
if _traceback_mode != 0 and _tb_basename_is_traceback(file_path) != 0:
HandlesTranslator.InjectTracebackInstallCtor(tr)
return tr
class BuildResult:
Success: int
OutputPath: str
ErrorMsg: str
def __new__(self) -> t.CPtr:
return t.CPtr(_mbuddy.alloc(BuildResult.__sizeof__()))
def __init__(self):
self.Success = 0
self.OutputPath = None
self.ErrorMsg = None
def ensure_dir(path: str) -> int:
"""递归创建目录(类似 mkdir -p),目录已存在视为成功
Args:
path: 目录路径(支持 / 或 \\ 分隔符)
Returns:
0 成功(包括目录已存在),非 0 失败
"""
if path is None: return 1
path_len: sizet = len(path)
if path_len == 0: return 0
# 热点快速路径(PROF:ensure_dir 占 4.5%,22256 次调用每次逐级 CreateDirectoryA):
# 末级目录已存在则直接成功返回,省去 1 次分配 + N+1 次 CreateDirectoryA 系统调用。
# 语义不变:原实现把"目录已存在"(CreateDirectoryA 失败)也视为成功。
attr: ULONG = w32.win32file.GetFileAttributesA(path)
if attr != 0xFFFFFFFF and (attr & w32.win32file.FILE_ATTRIBUTE_DIRECTORY) != 0:
return 0
# 复制路径到可写缓冲区(逐级截断用)
buf: bytes = _mbuddy.alloc(path_len + 1)
if buf is None: return 1
string.strcpy(buf, path)
# 遇到分隔符时临时截断,创建每一层目录
# CreateDirectoryA 在目录已存在时返回 0(失败),忽略即可
plen: int = path_len
for i in range(plen):
ch: int = buf[i]
if ch == 47 or ch == 92: # '/' = 47, '\\' = 92
saved: int = ch
buf[i] = '\0'
w32.win32file.CreateDirectoryA(buf, None)
buf[i] = saved
# 创建最终目录
w32.win32file.CreateDirectoryA(buf, None)
return 0
# ============================================================
# build_sliced_path - 构建切片路径并确保目录存在(自动调用 ensure_dir)
#
# 按 Config.Sha1SliceLevel 分散到 SHA1 前缀子目录:level=0 → {base_dir}/{sha1}.{ext};level=1 → {base_dir}/b7/{sha1}.{ext};level=2 → {base_dir}/b7/90/{sha1}.{ext}
#
# Args: base_dir 基础目录(如 temp_dir);sha1 SHA1 字符串;ext 文件扩展名(如 "stub.ll")
# Returns: 完整文件路径(mbuddy 分配),None 失败
# ============================================================
def build_sliced_path(base_dir: str, sha1: str, ext: str) -> str:
"""构建切片路径并确保目录存在"""
if base_dir is None or sha1 is None or ext is None:
return None
subdir: str = Config.slice_subdir(sha1, Config.Sha1SliceLevel)
base_len: sizet = len(base_dir)
sha1_len: sizet = len(sha1)
ext_len: sizet = len(ext)
if subdir is not None:
sub_len: sizet = len(subdir)
# ensure_dir({base_dir}/{subdir})
dir_path: str = _mbuddy.alloc(base_len + sub_len + 2)
if dir_path is not None:
viperlib.snprintf(dir_path, base_len + sub_len + 2, "%s/%s", base_dir, subdir)
ensure_dir(dir_path)
# 文件路径: {base_dir}/{subdir}/{sha1}.{ext}
path_len: sizet = base_len + sub_len + sha1_len + ext_len + 4
path: str = _mbuddy.alloc(path_len)
if path is None: return None
viperlib.snprintf(path, path_len, "%s/%s/%s.%s", base_dir, subdir, sha1, ext)
return path
else:
# 无切片: {base_dir}/{sha1}.{ext}
path_len = base_len + sha1_len + ext_len + 3
path = _mbuddy.alloc(path_len)
if path is None: return None
viperlib.snprintf(path, path_len, "%s/%s.%s", base_dir, sha1, ext)
return path
def write_ir_to_file(ir_buf: bytes, ir_len: sizet, output_dir: str, module_name: str) -> int:
"""将 IR 缓冲区写入 .ll 文件
Args:
ir_buf: IR 文本缓冲区
ir_len: IR 文本长度
output_dir: 输出目录(temp 或 output)
module_name: 模块名(SHA1)
Returns:
0 成功,非 0 失败
"""
if ir_buf is None or output_dir is None or module_name is None: return 1
# 构造切片路径: output_dir/{sha1前缀}/{module_name}.ll
path: str = build_sliced_path(output_dir, module_name, "ll")
if path is None: return 1
# 打开文件写入
f: rawptr[fileio.File] = fileio.File(path, fileio.MODE.W)
if f.closed: return 1
written: int64 = f.write(ir_buf, ir_len)
f.close()
if written < 0: return 1
return 0
def compile_ll_to_obj(ir_path: str, output_dir: str, module_name: str, cc_cmd: str, cc_flags: str) -> int:
"""调用 llc 将 .ll 编译为 .obj
Args:
ir_path: .ll 文件路径
output_dir: 输出目录
module_name: 模块名(SHA1,用于生成 .obj 文件名)
cc_cmd: 编译器命令(如 "llc")
cc_flags: 编译器参数(如 "-filetype=obj -relocation-model=pic")
Returns:
0 成功,非 0 失败
"""
if ir_path is None or cc_cmd is None or output_dir is None or module_name is None:
return 1
# 构造切片 .obj 路径: output_dir/{sha1前缀}/{module_name}.obj
obj_path: str = build_sliced_path(output_dir, module_name, "obj")
if obj_path is None: return 1
# ASan 模式(_asan_mode=1):先 opt -passes=asan 插桩再 llc。
# -asan-globals=false 关闭全局插桩:TPV 对全局变量(如各模块 _g_gen)
# 用直接地址寻址,ASan 全局 redzone 重定位后引用错位会读到垃圾指针。
compile_input: str = ir_path
# ---- MLIR 后端降级:mlir-opt(校验)→ mlir-translate(降级回 LLVM IR)----
# 必须在 ASan opt 与 llc 之前,并把 compile_input 替换为 <...>.ll.local。
if Config.is_mlir_backend() != 0:
ll_local_path: str = _ir_sibling_path(ir_path, ".ll.local")
mlir_opt_out: str = _ir_sibling_path(ir_path, ".mlir.opt")
if ll_local_path is None or mlir_opt_out is None:
return 1
mo_len: sizet = len(MLIR_OPT_CMD) + len(ir_path) + len(mlir_opt_out) + 16
cmd_mo: str = _mbuddy.alloc(mo_len)
if cmd_mo is None:
return 1
viperlib.snprintf(cmd_mo, mo_len, "%s %s -o %s", MLIR_OPT_CMD, ir_path, mlir_opt_out)
mt_len: sizet = len(MLIR_TRANSLATE_CMD) + len(mlir_opt_out) + len(ll_local_path) + 40
cmd_mt: str = _mbuddy.alloc(mt_len)
if cmd_mt is None:
return 1
viperlib.snprintf(cmd_mt, mt_len, "%s --mlir-to-llvmir %s -o %s", MLIR_TRANSLATE_CMD, mlir_opt_out, ll_local_path)
if _phase2_cmd_path is not None:
# 只写命令(须排在下面的 ASan/llc 命令之前)
_phase2_write_cmd(cmd_mo)
_phase2_write_cmd(cmd_mt)
else:
mo_rc: int = _run_cmd_wait(cmd_mo)
if mo_rc != 0:
VLogger.error(f"mlir-opt 失败 (module={module_name}, exit={mo_rc}, cmd={cmd_mo})", "MLIR")
return 1
mt_rc: int = _run_cmd_wait(cmd_mt)
if mt_rc != 0:
VLogger.error(f"mlir-translate 失败 (module={module_name}, exit={mt_rc}, cmd={cmd_mt})", "MLIR")
return 1
compile_input = ll_local_path
# --phase2command 模式:不执行 opt/llc,把 opt(ASan)+ llc 命令写入 bat
if _phase2_cmd_path is not None:
p2_opt_cmd: str = None
if _asan_mode != 0:
p2_asan: str = build_sliced_path(output_dir, module_name, "asan.ll")
if p2_asan is not None:
p2_olen: sizet = len(p2_asan) + len(compile_input) + 192
p2_opt_cmd = _mbuddy.alloc(p2_olen)
if p2_opt_cmd is not None:
viperlib.snprintf(p2_opt_cmd, p2_olen,
"opt -passes=forceattrs,asan -force-attribute=sanitize_address -asan-globals=false -o %s %s",
p2_asan, compile_input)
compile_input = p2_asan
llc_fp: str = _llc_frame_ptr_flag()
p2_clen: sizet = len(cc_cmd) + len(cc_flags) + len(llc_fp) + len(obj_path) + len(compile_input) + 64
p2_cmd: bytes = _mbuddy.alloc(p2_clen)
if p2_cmd is not None:
viperlib.snprintf(p2_cmd, p2_clen, "%s %s%s -o %s %s",
cc_cmd, cc_flags, llc_fp, obj_path, compile_input)
if p2_opt_cmd is not None:
_phase2_write_cmd(p2_opt_cmd)
if p2_cmd is not None:
_phase2_write_cmd(p2_cmd)
return 0
if _asan_mode != 0:
asan_path: str = build_sliced_path(output_dir, module_name, "asan.ll")
if asan_path is None:
return 1
opt_cmd_len: sizet = len(asan_path) + len(compile_input) + 192
opt_cmd: bytes = _mbuddy.alloc(opt_cmd_len)
if opt_cmd is None:
return 1
viperlib.snprintf(opt_cmd, opt_cmd_len,
"opt -passes=forceattrs,asan -force-attribute=sanitize_address -asan-globals=false -o %s %s",
asan_path, compile_input)
opt_ret: int = stdlib.system(opt_cmd)
if opt_ret != 0:
VLogger.error(f"opt ASan 插桩失败 (module={module_name})", "ASAN")
return 1
compile_input = asan_path
# 构造命令: llc -filetype=obj -o {obj_path} ir_path
llc_fp2: str = _llc_frame_ptr_flag()
cmd_len: sizet = len(cc_cmd) + len(cc_flags) + len(llc_fp2) + len(obj_path) + len(compile_input) + 64
cmd: bytes = _mbuddy.alloc(cmd_len)
if cmd is None:
return 1
viperlib.snprintf(cmd, cmd_len, "%s %s%s -o %s %s",
cc_cmd, cc_flags, llc_fp2, obj_path, compile_input)
# 用 CreateProcessA 直启 llc(避开 cmd/c 在 asan 下挂起),子进程继承父控制台:
# stdout|stderr 直接显示;失败时打印模块 sha1 + 退出码 + 完整命令(退出码同步返回)
sys_ret: int = _run_cmd_wait(cmd)
if sys_ret != 0:
VLogger.error(f"编译失败 (module={module_name}, exit={sys_ret}, cmd={cmd})", "LLC")
return 1
return 0
# ============================================================
# llc 并行编译(线程异步,Phase B 多模块同时跑 llc)
#
# 线程安全约束:
# - LlcTask 及命令串一律 stdlib.malloc:主池 mb 会被 reset_to(mark2) 回收,线程运行期间字符串悬空 → 概率崩溃
# - worker 只调 _run_cmd_wait(内部仅 stdlib.malloc/CreateProcessA/GetLastError/CloseHandle),不做任何池分配(MemBuddy 分配非线程安全)
# - 依赖 CreateProcessA + Win32 句柄接口的多线程安全性
# ============================================================
# llc 并行最大线程数(参考 TPC Phase2Translator n_workers=os.cpu_count(),本机 16 核)
LLC_MAX_THREADS: t.CDefine = 16
class LlcTask:
"""llc 异步任务:命令串(stdlib.malloc)+ 模块 SHA1 + 结果"""
OptCmd: bytes # ASan 插桩命令(非 ASan 为 None)
LlcCmd: bytes # llc 命令
ModuleSha1: bytes # 模块 SHA1(报错用)
Result: int # worker 写:0 成功,非 0 失败
# ExitCode 追加在结尾:既有字段偏移不变;LlcTask.__sizeof__() 随之增大,
# 所有构造/初始化点均按 __sizeof__() 算首址并对整条 memset,无需改偏移。
ExitCode: int # worker 写:失败命令退出码(成功为 0)
# MLIR 后端降级命令(非 MLIR 为 None);同样追加在结尾,不影响既有偏移。
MlirOptCmd: bytes # mlir-opt 校验命令
MlirTranslateCmd: bytes # mlir-translate 降级命令
def init_llc_task(task: LlcTask | t.CPtr, ir_path: str, output_dir: str,
module_name: str, cc_cmd: str, cc_flags: str) -> int:
"""填充 llc 任务(全部 stdlib.malloc,线程运行期不被回收),0 成功"""
if task is None or ir_path is None or output_dir is None or module_name is None:
return 1
string.memset(task, 0, LlcTask.__sizeof__())
task.Result = -1
obj_path: str = build_sliced_path(output_dir, module_name, "obj")
if obj_path is None:
return 1
# 模块 SHA1
sl: sizet = len(module_name)
task.ModuleSha1 = stdlib.malloc(sl + 1)
if task.ModuleSha1 is None:
return 1
string.strcpy(task.ModuleSha1, module_name)
# llc 输入选择:ASan 模式必须喂插桩后的 .asan.ll,与串行路径
# compile_ll_to_obj 的 compile_input 同口径(否则 opt 生成了插桩产物
# 却被 llc 忽略,App 侧 obj 里 0 处 __asan,内存排查半盲)
compile_input: str = ir_path
# MLIR 后端降级:worker 先跑 mlir-opt(校验)→ mlir-translate(降级回 LLVM IR),
# 之后的 ASan OptCmd / LlcCmd 输入必须是 <...>.ll.local。
if Config.is_mlir_backend() != 0:
ll_local_path: str = _ir_sibling_path(ir_path, ".ll.local")
mlir_opt_out: str = _ir_sibling_path(ir_path, ".mlir.opt")
if ll_local_path is None or mlir_opt_out is None:
return 1
mo_len: sizet = len(MLIR_OPT_CMD) + len(ir_path) + len(mlir_opt_out) + 16
task.MlirOptCmd = stdlib.malloc(mo_len)
if task.MlirOptCmd is None:
return 1
viperlib.snprintf(task.MlirOptCmd, mo_len, "%s %s -o %s", MLIR_OPT_CMD, ir_path, mlir_opt_out)
mt_len: sizet = len(MLIR_TRANSLATE_CMD) + len(mlir_opt_out) + len(ll_local_path) + 40
task.MlirTranslateCmd = stdlib.malloc(mt_len)
if task.MlirTranslateCmd is None:
return 1
viperlib.snprintf(task.MlirTranslateCmd, mt_len, "%s --mlir-to-llvmir %s -o %s",
MLIR_TRANSLATE_CMD, mlir_opt_out, ll_local_path)
compile_input = ll_local_path
# ASan 模式:opt 插桩命令(worker 先跑 MLIR 降级/opt 再跑 llc,故 LlcCmd 的输入
# 必须指向 asan_path;失败即置 Result)
if _asan_mode != 0:
asan_path: str = build_sliced_path(output_dir, module_name, "asan.ll")
if asan_path is None:
return 1
opt_len: sizet = len(asan_path) + len(compile_input) + 192
task.OptCmd = stdlib.malloc(opt_len)
if task.OptCmd is None:
return 1
viperlib.snprintf(task.OptCmd, opt_len,
"opt -passes=forceattrs,asan -force-attribute=sanitize_address"
" -asan-globals=false -o %s %s",
asan_path, compile_input)
compile_input = asan_path
# llc 命令(输入 = compile_input:非 ASan 为 .ll,ASan 为 .asan.ll)
llc_fp3: str = _llc_frame_ptr_flag()
cmd_len: sizet = len(cc_cmd) + len(cc_flags) + len(llc_fp3) + len(obj_path) + len(compile_input) + 64
task.LlcCmd = stdlib.malloc(cmd_len)
if task.LlcCmd is None:
return 1
viperlib.snprintf(task.LlcCmd, cmd_len, "%s %s%s -o %s %s",
cc_cmd, cc_flags, llc_fp3, obj_path, compile_input)
return 0
def free_llc_task(task: LlcTask | t.CPtr):
"""释放任务的命令串;记录本身是任务数组的连续槽(非独立 malloc),
由调用方对数组整体 stdlib.free(此前对槽 free 导致 double-free)"""
if task is None:
return
if task.OptCmd is not None:
stdlib.free(task.OptCmd)
task.OptCmd = None
if task.LlcCmd is not None:
stdlib.free(task.LlcCmd)
task.LlcCmd = None
if task.MlirOptCmd is not None:
stdlib.free(task.MlirOptCmd)
task.MlirOptCmd = None
if task.MlirTranslateCmd is not None:
stdlib.free(task.MlirTranslateCmd)
task.MlirTranslateCmd = None
if task.ModuleSha1 is not None:
stdlib.free(task.ModuleSha1)
task.ModuleSha1 = None
def _phase2_write_cmd(cmd: str) -> None:
"""--phase2command 模式:把命令追加写入 bat(不执行)
首次调用写入 header(@echo off + 说明),后续命令以 \r\n 分行追加,
与 cmd.exe 批处理格式一致。bat 路径由模块级 _phase2_cmd_path 指定。
"""
global _phase2_bat_init
if _phase2_cmd_path is None or cmd is None:
return
f: rawptr[fileio.File] = fileio.File(_phase2_cmd_path, fileio.MODE.A)
if f.closed:
VLogger.error("phase2command: bat 打开失败", "phase2cmd")
return
if _phase2_bat_init == 0:
_phase2_bat_init = 1
hdr1: str = "@echo off\r\n"
hdr2: str = "rem TransPyV --phase2command generated commands\r\n"
f.write(hdr1, string.strlen(hdr1))
f.write(hdr2, string.strlen(hdr2))
f.write(cmd, string.strlen(cmd))
f.write("\r\n", 2)
f.close()
def _run_cmd_wait(cmd: str) -> int:
"""CreateProcessA 同步执行命令并等待退出,返回退出码。
替代 stdlib.system:Windows asan 环境下 cmd /c 经 CreateProcess 受
RTL allocator hook 干扰可能挂起(LLVM issue #111166 同类,ClassArrayTest
llc 固定卡死根因);CreateProcessA 直接执行目标程序并继承父进程控制台
(stdout/stderr 直接显示,同 system 语义)。全 stdlib.malloc/栈分配,
worker 线程安全。CreateProcessA 返回 0 时立即取 GetLastError 并以
VLogger.warning 打印(外部命令未启动)。
"""
if cmd is None:
return -1
cl_len: sizet = len(cmd)
cmd_line: bytes = stdlib.malloc(cl_len + 1)
if cmd_line is None:
return -1
string.strcpy(cmd_line, cmd)
sa: w32.win32base.SECURITY_ATTRIBUTES = w32.win32base.SECURITY_ATTRIBUTES()
si: win32process.STARTUPINFOA = win32process.STARTUPINFOA()
pi: win32process.PROCESS_INFORMATION = win32process.PROCESS_INFORMATION()
# T() 构造仅 alloca 不 memset:STARTUPINFOA 未设字段(dwFlags 等)全为
# 栈垃圾,多线程并发调用时垃圾随机 → dwFlags 垃圾位让 CreateProcessA
# 用垃圾句柄作子进程 stdio → 子进程行为不定/父进程线程崩溃(ClassArrayTest
# e5fb worker 崩溃、edd8 正常的原因)。si/sa/pi 全部显式清零。
string.memset(t.CPtr(c.Addr(si)), 0, win32process.STARTUPINFOA.__sizeof__())
string.memset(t.CPtr(c.Addr(sa)), 0, w32.win32base.SECURITY_ATTRIBUTES.__sizeof__())
string.memset(t.CPtr(c.Addr(pi)), 0, win32process.PROCESS_INFORMATION.__sizeof__())
sa.nLength = w32.win32base.SECURITY_ATTRIBUTES.__sizeof__()
sa.lpSecurityDescriptor = t.CPtr(0)
sa.bInheritHandle = 1
si.cb = win32process.STARTUPINFOA.__sizeof__()
# create_flags=0:子进程继承父控制台(输出直接显示,同 system 语义);
# si.dwFlags 保持 memset 后的 0(不置 STARTF_USESTDHANDLES、不设 std 句柄)
ok: int = win32process.CreateProcessA(
t.CPtr(0), cmd_line, t.CPtr(0), t.CPtr(0), 1, 0,
t.CPtr(0), t.CPtr(0), t.CPtr(c.Addr(si)), t.CPtr(c.Addr(pi)))
# CreateProcessA 失败诊断:GetLastError 必须在返回后立即取(此后 stdlib.free/
# CloseHandle/SetConsoleTextAttribute 等都会覆盖它)。用 warning 不用 error——
# VLogger.error 会 sys.exit(1),而本函数可能在 worker 线程里执行;warning 只走
# CRT printf + 控制台着色,不做 MemBuddy 池分配,满足 worker 线程安全约束。
if ok == 0:
cp_err: uint32 = w32.win32base.GetLastError()
VLogger.warning(f"CreateProcessA 失败 (GetLastError={cp_err}) 外部命令未启动: cmd={cmd}", "LLC")
stdlib.free(cmd_line)
if ok == 0:
return -1
win32sync.WaitForSingleObject(pi.hProcess, w32.win32base.INFINITE)
exit_code: uint32 = 0
win32process.GetExitCodeProcess(pi.hProcess, t.CPtr(c.Addr(exit_code)))
w32.win32base.CloseHandle(pi.hProcess)
w32.win32base.CloseHandle(pi.hThread)
return int(exit_code)
def _llc_task_worker(arg: t.CPtr) -> t.CPtr:
"""llc 线程入口:只调 _run_cmd_wait(),不做任何池分配(线程安全)
Result 仅在命令链执行完后写入(初始 -1;join 超时保持 -1 → 报失败);
ExitCode 记录失败命令的退出码,供主线程打印"""
task: LlcTask | t.CPtr = (LlcTask | t.CPtr)(arg)
if task is None:
return t.CPtr(0)
# MLIR 后端:先校验(mlir-opt)→ 降级(mlir-translate),再 ASan opt → llc
if task.MlirOptCmd is not None:
mo_rc: int = _run_cmd_wait(task.MlirOptCmd)
if mo_rc != 0:
task.ExitCode = mo_rc
task.Result = 1
return t.CPtr(0)
if task.MlirTranslateCmd is not None:
mt_rc: int = _run_cmd_wait(task.MlirTranslateCmd)
if mt_rc != 0:
task.ExitCode = mt_rc
task.Result = 1
return t.CPtr(0)
if task.OptCmd is not None:
opt_rc: int = _run_cmd_wait(task.OptCmd)
if opt_rc != 0:
task.ExitCode = opt_rc
task.Result = 1
return t.CPtr(0)
llc_rc: int = _run_cmd_wait(task.LlcCmd)
task.ExitCode = llc_rc
if llc_rc != 0:
task.Result = 1
else:
task.Result = 0
return t.CPtr(0)
def run_llc_parallel(tasks: bytes, task_count: int, max_threads: int) -> int:
"""并行执行 llc:分批 max_threads 个线程,全部 join 后检查结果
tasks: LlcTask 记录连续数组(stdlib.malloc),每个记录由 init_llc_task 填充。
Returns: 0 全部成功;非 0 有失败(已打印失败模块与命令)
"""
if tasks is None or task_count <= 0:
return 0
# --phase2command 模式:不 spawn 线程,把 opt(ASan)+ llc 命令全部写入 bat
if _phase2_cmd_path is not None:
for pi2 in range(task_count):
tsk_p: LlcTask | t.CPtr = (LlcTask | t.CPtr)(uint64(tasks) + sizet(pi2) * LlcTask.__sizeof__())
if tsk_p is None:
continue
if tsk_p.MlirOptCmd is not None:
_phase2_write_cmd(tsk_p.MlirOptCmd)
if tsk_p.MlirTranslateCmd is not None:
_phase2_write_cmd(tsk_p.MlirTranslateCmd)
if tsk_p.OptCmd is not None:
_phase2_write_cmd(tsk_p.OptCmd)
if tsk_p.LlcCmd is not None:
_phase2_write_cmd(tsk_p.LlcCmd)
VLogger.info(f"phase2command 模式:{task_count} 条 llc 命令已写入 {_phase2_cmd_path}", "phase2cmd")
return 0
if max_threads <= 0:
max_threads = 1
# 线程句柄数组:list[str](str=bytes=i8*,elem_size=8 显式)存句柄值;list[t.CPtr] 特化下 append 会读零页崩溃(原因待查),list[str] 全库验证稳定。
# start() 后立即取 handle 存入,join/close 直接对句柄操作,不依赖 Thread 对象存续。
handles: list[str]* = list[str](_mbuddy, 8)
if handles is None:
VLogger.error("run_llc_parallel: 线程句柄数组分配失败", "PhaseB")
return 1
slot_size: sizet = LlcTask.__sizeof__()
ret: int = 0
# 分批执行:batch_start 按 max_threads 步进(每批 spawn+join+close 一批句柄)。
for batch_start in range(0, task_count, max_threads):
batch_n: int = max_threads
if batch_start + batch_n > task_count:
batch_n = task_count - batch_start
# 本批 spawn
for j in range(batch_n):
tsk: LlcTask | t.CPtr = (LlcTask | t.CPtr)(uint64(tasks) + sizet(batch_start + j) * slot_size)
th: rawptr[vthreading.Thread] = vthreading.Thread(_llc_task_worker, t.CPtr(tsk))
th.start()
# start() 后立即取出句柄(Thread 对象栈地址可能被后续构造复用,
# join 时再读 self.handle 可能已被 close 清空/覆盖)
handles.append(t.CPtr(th.handle))
for j in range(batch_n):
h_j: t.CPtr = t.CPtr(handles.get(batch_start + j))
wr: uint32 = win32sync.WaitForSingleObject(h_j, 600000)
if wr != 0:
VLogger.error("llc 线程 join 超时/失败(600 秒未完成)", "PhaseB")
w32.win32base.CloseHandle(h_j)
# 本批结果检查
for j in range(batch_n):
tsk2: LlcTask | t.CPtr = (LlcTask | t.CPtr)(uint64(tasks) + sizet(batch_start + j) * slot_size)
if tsk2 is not None and tsk2.Result != 0:
# 失败:打印模块 sha1 + 退出码 + 完整命令(.ll 路径即命令末参数)
VLogger.error(f"llc 编译失败: {(tsk2.ModuleSha1 if tsk2.ModuleSha1 is not None else "?")} (exit={tsk2.ExitCode} cmd={(tsk2.LlcCmd if tsk2.LlcCmd is not None else "?")})", "LLC")
ret = 1
return ret
def _CollectObjFilesImpl(base_dir: str, bd_len: sizet,
out_buf: bytes, out_pos: sizet, out_size: sizet) -> sizet:
"""递归扫描 base_dir,收集 .obj 文件路径到 out_buf,返回新的 out_pos
支持切片子目录(如 dir/43/{sha1}.obj)。
"""
find_data: w32.win32file.WIN32_FIND_DATAA | t.CPtr = _mbuddy.alloc(w32.win32file.WIN32_FIND_DATAA.__sizeof__())
if find_data is None:
VLogger.warning("_CollectObjFilesImpl 分配 find_data 失败,少收集 .obj", "collect")
return out_pos
string.memset(find_data, 0, w32.win32file.WIN32_FIND_DATAA.__sizeof__())
pattern: bytes = _mbuddy.alloc(bd_len + 8)
if pattern is None:
VLogger.warning("_CollectObjFilesImpl 分配 pattern 失败,少收集 .obj", "collect")
return out_pos
viperlib.snprintf(pattern, bd_len + 8, "%s/*", base_dir)
handle: w32.win32base.HANDLE = w32.win32file.FindFirstFileA(pattern, find_data)
_mbuddy.free(pattern)
if handle == w32.win32base.INVALID_HANDLE_VALUE:
VLogger.warning("_CollectObjFilesImpl 无法打开目录,少收集 .obj", "collect")
return out_pos
while True:
fname: str = find_data.cFileName
if fname is not None:
fname_len: sizet = len(fname)
if fname_len > 0:
# 跳过 . 和 ..
is_dot: int = 0
if fname_len == 1 and fname[0] == '.':
is_dot = 1
elif fname_len == 2 and fname[0] == '.' and fname[1] == '.':
is_dot = 1
if is_dot == 0:
attrs: ULONG = find_data.dwFileAttributes
is_dir: int = 0
if (attrs & w32.win32file.FILE_ATTRIBUTE_DIRECTORY) != 0:
is_dir = 1
if is_dir != 0:
# 递归扫描子目录
sub_dir: bytes = _mbuddy.alloc(bd_len + fname_len + 2)
if sub_dir is not None:
viperlib.snprintf(sub_dir, bd_len + fname_len + 2, "%s/%s", base_dir, fname)
sub_len: sizet = len(sub_dir)
out_pos = _CollectObjFilesImpl(sub_dir, sub_len, out_buf, out_pos, out_size)
_mbuddy.free(sub_dir)
else:
# 检查是否是 .obj 文件
if fname_len > 4 and fname[fname_len - 4] == '.' \
and fname[fname_len - 3] == 'o' and fname[fname_len - 2] == 'b' \
and fname[fname_len - 1] == 'j':
need: sizet = bd_len + 1 + fname_len + 2
if out_pos + need >= out_size:
break
if out_pos > 0:
out_buf[out_pos] = ' '
out_pos += 1
viperlib.snprintf(out_buf + out_pos, need, "%s/%s", base_dir, fname)
out_pos += bd_len + 1 + fname_len
if w32.win32file.FindNextFileA(handle, find_data) == 0:
break
w32.win32file.FindClose(handle)
return out_pos
def collect_obj_files(includes_binary_dir: str, out_buf: bytes, out_size: sizet) -> sizet:
"""递归扫描 includes_binary_dir 目录,收集所有 .obj 文件路径到 out_buf
支持切片子目录(如 includes_binary_dir/43/{sha1}.obj)。
Args:
includes_binary_dir: includes.binary 目录路径
out_buf: 输出缓冲区(用于存放空格分隔的 .obj 文件路径)
out_size: 输出缓冲区大小
Returns:
写入的字节数(不含 null 终止符),0 表示无文件或错误
"""
if includes_binary_dir is None or out_buf is None or out_size == 0:
return 0
out_buf[0] = '\0'
dir_len: sizet = len(includes_binary_dir)
out_pos: sizet = _CollectObjFilesImpl(includes_binary_dir, dir_len, out_buf, 0, out_size)
out_buf[out_pos] = '\0'
return out_pos
def _append_host_export_implib(linker_flags: str, implib_path: str) -> str:
"""宿主导出模式:为链接参数追加 `--out-implib=<绝对路径>`(REPL 片段导入宿主用)
触发条件 = linker_flags 显式含 "--export-all-symbols"(工程级开关,宿主的
project.vpj 里声明)。此时 exe 会带导出表,REPL 片段 DLL 需要一份 import lib
才能把 IR 里的跨模块引用(形如 `<16位hex>.<函数名>`,见 .text.ll 的
`define i32 @"<sha1>.utf8_is_valid"`)解析到宿主的导出符号。
为什么路径由本层计算而不写进 project.vpj:Config 对 linker.flags 既不做
%VAR% 展开、也不按 project_dir 解析(Config.vpj:180-230 的 _join_flags_array
只做拼接;:570-632 只重写 -T 后的脚本路径;env 展开只发生在 static_libs/
dynamic_libs/includes/build_dir 等字段),而 out-implib 的值是相对
clang 进程 cwd 解析的 ⇒ 只有本层能从 output_dir + linker_output 算出绝对路径。
未开启工程的 linker_flags 不含该标志 ⇒ 原样返回,链接命令逐字节不变。
"""
if linker_flags is None or implib_path is None:
return linker_flags
if string.strstr(linker_flags, "--export-all-symbols") is None:
return linker_flags
extra: sizet = string.strlen(" -Wl,--out-implib=") + string.strlen(implib_path) + 1
buf: str = _mbuddy.alloc(string.strlen(linker_flags) + extra)
if buf is None:
VLogger.warning("宿主导出:import lib 参数缓冲分配失败(片段将无法导入宿主符号)", "link")
return linker_flags
viperlib.snprintf(buf, string.strlen(linker_flags) + extra,
"%s -Wl,--out-implib=%s", linker_flags, implib_path)
VLogger.info(f"宿主导出:产出 import lib {implib_path}", "link")
return buf
def link_obj_to_exe(output_dir: str, module_name: str, linker_cmd: str, linker_flags: str, linker_output: str,
includes_binary_dir: str, linker_static_libs: str = None, linker_dynamic_libs: str = None) -> int:
"""调用 clang++ 链接 .obj 为 .exe
Args:
output_dir: 输出目录(包含 .obj 文件)
module_name: 模块名(SHA1)
linker_cmd: 链接器命令(如 "clang++")
linker_flags: 链接器参数
linker_output: 输出文件名(如 "test.exe")
includes_binary_dir: includes.binary 目录路径(链接时附加预编译 .obj)
linker_static_libs: 显式静态链接文件(.a/.obj,空格分隔绝对路径,可为 None)
linker_dynamic_libs: 显式动态链接文件(.dll/.so,空格分隔绝对路径,可为 None)
Returns:
0 成功,非 0 失败
"""
if output_dir is None or linker_cmd is None or module_name is None:
return 1
# 构造切片 .obj 路径: output_dir/{sha1前缀}/{module_name}.obj
obj_path: str = build_sliced_path(output_dir, module_name, "obj")
if obj_path is None:
return 1
# 宿主 exe 导出(工程 linker.flags 含 --export-all-symbols 时)同步产出 import lib:
# {output_dir}/{linker_output}.dll.a —— REPL 片段链接时把它加进命令行即可导入
# 宿主(同一进程/同一份 includes 代码与数据)。未开启的工程不受影响。
if linker_output is not None:
implib_len: sizet = string.strlen(output_dir) + string.strlen(linker_output) + 8
implib_path: str = _mbuddy.alloc(implib_len)
if implib_path is not None:
viperlib.snprintf(implib_path, implib_len, "%s/%s.dll.a", output_dir, linker_output)
linker_flags = _append_host_export_implib(linker_flags, implib_path)
# --traceback: link dbghelp (after the host-export rewrite, so both survive)
linker_flags = _append_traceback_dbghelp(linker_flags)
# 收集 includes.binary 的 .obj 文件路径
EXTRA_BUF_SIZE: sizet = 32768
extra_objs: bytes = _mbuddy.alloc(EXTRA_BUF_SIZE)
if extra_objs is None:
return 1
extra_len: sizet = 0
if includes_binary_dir is not None:
extra_len = collect_obj_files(includes_binary_dir, extra_objs, EXTRA_BUF_SIZE)
if extra_len > 0:
VLogger.info(f"附加 {extra_len} 字节的 includes.binary .obj 文件", "link")
else:
VLogger.warning(f"includes.binary 无 .obj 文件: {includes_binary_dir}", "link")
# 构造命令: clang++ {obj_path} extra_objs [static_libs/dynamic_libs] -o {output_dir}/{linker_output} linker_flags
# 注意: .obj 与显式指定的 .a/.obj/.dll/.so 都必须在 -l 库标志之前,否则链接器无法解析符号依赖。
# ASan 运行库不由 TPV 硬编码(clang 无 x86_64-w64-windows-gnu 的 ASan 运行库,借用 clang64 会牵进 MSVC 专有指令),
# 改由 project.vpj 的 linker.static_libs / linker.dynamic_libs 显式声明,main.vp 在 --asan 下用 Config.asan_runtime_declared 校验。
use_flags: str = linker_flags
static_libs: str = linker_static_libs
if static_libs is None:
static_libs = ""
if linker_dynamic_libs is not None and len(linker_dynamic_libs) > 0:
# 合并 static + dynamic 为单个空格分隔字符串(Viper str += 拼接
# 会生成非法 add i8* IR,须用 snprintf 构造)
merged_sl: str = _mbuddy.alloc(len(static_libs) + len(linker_dynamic_libs) + 2)
if merged_sl is not None:
if len(static_libs) > 0:
viperlib.snprintf(merged_sl, len(static_libs) + len(linker_dynamic_libs) + 2,
"%s %s", static_libs, linker_dynamic_libs)
else:
viperlib.snprintf(merged_sl, len(static_libs) + len(linker_dynamic_libs) + 2,
"%s", linker_dynamic_libs)
static_libs = merged_sl
sl_len: sizet = len(static_libs)
cmd_len: sizet = len(linker_cmd) + len(obj_path) + len(use_flags) + len(output_dir) + len(linker_output) + extra_len + sl_len + 128
cmd: bytes = _mbuddy.alloc(cmd_len)
if cmd is None:
return 1
if sl_len > 0 and extra_len > 0:
viperlib.snprintf(cmd, cmd_len, "%s %s %s %s -o %s/%s %s",
linker_cmd, obj_path, extra_objs, static_libs,
output_dir, linker_output, use_flags)
elif sl_len > 0:
viperlib.snprintf(cmd, cmd_len, "%s %s %s -o %s/%s %s",
linker_cmd, obj_path, static_libs,
output_dir, linker_output, use_flags)
elif extra_len > 0:
viperlib.snprintf(cmd, cmd_len, "%s %s %s -o %s/%s %s",
linker_cmd, obj_path, extra_objs,
output_dir, linker_output, use_flags)
else:
viperlib.snprintf(cmd, cmd_len, "%s %s -o %s/%s %s",
linker_cmd, obj_path, output_dir, linker_output,
use_flags)
# --phase2command 模式:不执行链接,命令写入 bat
if _phase2_cmd_path is not None:
_phase2_write_cmd(cmd)
return 0
result: rawptr[subprocess.CompletedProcess] = subprocess.run(cmd, True, True)
if result is None:
VLogger.error("subprocess.run 返回 None", "link")
return 1
if result.returncode != 0:
VLogger.error(f"链接失败,返回码: {result.returncode}", "link")
VLogger.error(f"命令: {cmd}", "link")
# 显示链接器错误输出
# 注意: subprocess 在 Windows 下将 stderr 合并到 stdout,result.stderr 总是 None
if result.stdout is not None:
VLogger.error(result.stdout, "link")
else:
VLogger.error("无输出捕获", "link")
if result.stderr is not None:
VLogger.error(result.stderr, "link")
return 1
return 0
def asm_to_obj(asm_path: str, sha1_val: str, output_dir: str,
triple: str) -> int:
"""用 llvm-mc 汇编 .s 源文件为 .obj(切片路径)
裸机项目(VKernel 等)含 .s 汇编文件(如 Kernel/intr/isr.s 定义
isr0/syscall_entry/_per_cpu 等符号)。.s 不经过 TPV 翻译/llc,
直接用配套的 llvm-mc 汇编为 .obj,obj 路径与其他模块一致
(output_dir/{sha1前缀}/{sha1}.obj),参与 Phase C 链接。
Args:
asm_path: .s 源文件完整路径
sha1_val: 文件内容 SHA1 前16字符(切片路径用)
output_dir: 输出目录
triple: 目标 triple(如 x86_64-none-elf)
Returns:
0 成功,非 0 失败
"""
if asm_path is None or sha1_val is None or output_dir is None:
return 1
obj_path: str = build_sliced_path(output_dir, sha1_val, "obj")
if obj_path is None:
return 1
trip: str = triple
if trip is None or trip[0] == '\0':
trip = "x86_64-none-elf"
cmd_len: sizet = len("llvm-mc -triple=") + len(trip) \
+ len(" -filetype=obj -o ") + len(obj_path) + len(asm_path) + 64
cmd: bytes = _mbuddy.alloc(cmd_len)
if cmd is None:
return 1
viperlib.snprintf(cmd, cmd_len, "llvm-mc -triple=%s -filetype=obj -o %s %s",
trip, obj_path, asm_path)
# --phase2command 模式:命令写入 bat(与 llc/链接一致)
if _phase2_cmd_path is not None:
_phase2_write_cmd(cmd)
return 0
result: rawptr[subprocess.CompletedProcess] = subprocess.run(cmd, True, True)
if result is None:
VLogger.error("llvm-mc subprocess.run 返回 None", "asm")
return 1
if result.returncode != 0:
if result.stdout is not None:
stdio.printf("%s\n", result.stdout)
if result.stderr is not None:
stdio.printf("%s\n", result.stderr)
VLogger.error(f"llvm-mc 汇编失败,返回码: {result.returncode}, 命令: {cmd}", "asm")
return 1
return 0
def compile_module_to_obj(ir_buf: bytes, ir_len: sizet,
temp_dir: str, output_dir: str, module_name: str,
cc_cmd: str, cc_flags: str) -> int:
"""编译 IR 到 .obj(不链接)
Args:
ir_buf: LLVM IR 文本缓冲区
ir_len: IR 文本长度
temp_dir: 临时目录
output_dir: 输出目录
module_name: 模块名(SHA1)
cc_cmd: 编译器命令
cc_flags: 编译器参数
Returns:
0 成功,非 0 失败
"""
if ir_buf is None or temp_dir is None or output_dir is None or module_name is None:
return 1
# Step 1: 写 .ll 文件
ret: int = write_ir_to_file(ir_buf, ir_len, temp_dir, module_name)
if ret != 0:
VLogger.error(f"写 .ll 失败: {module_name}", "compile")
return 1
# Step 2: 构造切片 .ll 路径并编译 → .obj
ir_path: str = build_sliced_path(temp_dir, module_name, "ll")
if ir_path is None:
return 1
ret = compile_ll_to_obj(ir_path, output_dir, module_name, cc_cmd, cc_flags)
if ret != 0:
VLogger.error(f"llc 编译失败: {module_name}", "compile")
return 1
return 0
def link_objs_to_exe(obj_paths: str, obj_paths_len: sizet,
linker_cmd: str, linker_flags: str, linker_output: str,
includes_binary_dir: str, linker_static_libs: str = None, linker_dynamic_libs: str = None) -> int:
"""链接多个 .obj 文件为 .exe
Args:
obj_paths: 空格分隔的 .obj 文件完整路径字符串
obj_paths_len: obj_paths 长度
linker_cmd: 链接器命令(如 "clang++")
linker_flags: 链接器参数
linker_output: 输出文件完整路径
includes_binary_dir: includes.binary 目录路径(链接时附加预编译 .obj)
linker_static_libs: 显式静态链接文件(.a/.obj,空格分隔绝对路径,可为 None)
Returns:
0 成功,非 0 失败
"""
if obj_paths is None or obj_paths_len == 0 or linker_cmd is None:
return 1
# 去重 .obj 路径:同一模块的 obj 可能被多个收集环节加入(Phase B+ 新编译的 obj_paths 与 Phase C reachable 收集指向同一文件);
# 同一路径在链接命令中出现两次即重复符号(无 --allow-multiple-definition 的工程会 ld.lld 报错),故保留首次出现、后续跳过。
if obj_paths_len > 0:
dedup: bytes = _mbuddy.alloc(obj_paths_len + 1)
if dedup is not None:
dedup[0] = '\0'
dpos: sizet = 0
dsrc_i: sizet = 0
while dsrc_i < obj_paths_len:
# 跳过前导空格
while dsrc_i < obj_paths_len and obj_paths[dsrc_i] == ' ':
dsrc_i += 1
if dsrc_i >= obj_paths_len:
break
dstart: sizet = dsrc_i
while dsrc_i < obj_paths_len and obj_paths[dsrc_i] != ' ':
dsrc_i += 1
dtlen: sizet = dsrc_i - dstart
# 检查 dedup 中是否已有相同 token
ddup: int = 0
dj: sizet = 0
while dj < dpos:
while dj < dpos and dedup[dj] == ' ':
dj += 1
if dj >= dpos:
break
ds2: sizet = dj
while dj < dpos and dedup[dj] != ' ':
dj += 1
dt2len: sizet = dj - ds2
if dt2len == dtlen:
deq: int = 1
dk: sizet = 0
while dk < dtlen:
if dedup[ds2 + dk] != obj_paths[dstart + dk]:
deq = 0
break
dk += 1
if deq != 0:
ddup = 1
break
if ddup == 0:
if dpos > 0:
dedup[dpos] = ' '
dpos += 1
string.memcpy(dedup + dpos, obj_paths + dstart, dtlen)
dpos += dtlen
dedup[dpos] = '\0'
if dpos > 0 and dpos != obj_paths_len:
obj_paths = dedup
obj_paths_len = dpos
# 收集 includes.binary 的 .obj 文件路径
EXTRA_BUF_SIZE: sizet = 32768
extra_objs: bytes = _mbuddy.alloc(EXTRA_BUF_SIZE)
if extra_objs is None:
return 1
extra_len: sizet = 0
if includes_binary_dir is not None:
extra_len = collect_obj_files(includes_binary_dir, extra_objs, EXTRA_BUF_SIZE)
# 构造命令: clang++ obj_paths extra_objs -o linker_output linker_flags
# ASan 运行库同样不再由 TPV 注入(在 link_obj_to_exe 里已有说明):
# 由 project.vpj 声明,--asan 下由 main.vp 校验。
# 宿主导出:linker_output 是完整输出路径 ⇒ import lib 取名 <linker_output>.dll.a
if linker_output is not None:
implib_len2: sizet = string.strlen(linker_output) + 8
implib_path2: str = _mbuddy.alloc(implib_len2)
if implib_path2 is not None:
viperlib.snprintf(implib_path2, implib_len2, "%s.dll.a", linker_output)
linker_flags = _append_host_export_implib(linker_flags, implib_path2)
# --traceback: link dbghelp (after the host-export rewrite)
linker_flags = _append_traceback_dbghelp(linker_flags)
use_flags: str = linker_flags
static_libs2: str = linker_static_libs
if static_libs2 is None:
static_libs2 = ""
if linker_dynamic_libs is not None and len(linker_dynamic_libs) > 0:
# 合并 static + dynamic(Viper str += 拼接生成非法 IR,用 snprintf)
merged_sl2: str = _mbuddy.alloc(len(static_libs2) + len(linker_dynamic_libs) + 2)
if merged_sl2 is not None:
if len(static_libs2) > 0:
viperlib.snprintf(merged_sl2, len(static_libs2) + len(linker_dynamic_libs) + 2,
"%s %s", static_libs2, linker_dynamic_libs)
else:
viperlib.snprintf(merged_sl2, len(static_libs2) + len(linker_dynamic_libs) + 2,
"%s", linker_dynamic_libs)
static_libs2 = merged_sl2
sl2_len: sizet = len(static_libs2)
cmd_len: sizet = len(linker_cmd) + obj_paths_len + len(use_flags) + len(linker_output) + extra_len + sl2_len + 128
cmd: bytes = _mbuddy.alloc(cmd_len)
if cmd is None:
return 1
# linker.static_libs 显式静态链接文件位于 .obj 之后、-o 之前
if sl2_len > 0 and extra_len > 0:
viperlib.snprintf(cmd, cmd_len, "%s %s %s %s -o %s %s",
linker_cmd, obj_paths, extra_objs, static_libs2,
linker_output, use_flags)
elif sl2_len > 0:
viperlib.snprintf(cmd, cmd_len, "%s %s %s -o %s %s",
linker_cmd, obj_paths, static_libs2,
linker_output, use_flags)
elif extra_len > 0:
viperlib.snprintf(cmd, cmd_len, "%s %s %s -o %s %s",
linker_cmd, obj_paths, extra_objs,
linker_output, use_flags)
else:
viperlib.snprintf(cmd, cmd_len, "%s %s -o %s %s",
linker_cmd, obj_paths, linker_output, use_flags)
# 用 subprocess.run 捕获 clang++ 的 stdout/stderr 输出
# 之前用 stdlib.system() 无法捕获具体错误(undefined reference 等),
# 导致 1.txt 中只看到"链接失败,返回码: 1"而看不到未解析符号
# --phase2command 模式:不执行链接,命令写入 bat
if _phase2_cmd_path is not None:
_phase2_write_cmd(cmd)
return 0
result: rawptr[subprocess.CompletedProcess] = subprocess.run(cmd, True, True)
if result is None:
VLogger.error("subprocess.run 返回 None", "link")
return 1
if result.returncode != 0:
# 先用 stdio.printf 直接输出 clang++ 的完整错误输出(含 undefined reference)
# 必须在 VLogger.error 之前输出,因为 VLogger.error 会调用 sys.exit(1) 终止进程
if result.stdout is not None:
stdio.printf("=== clang++ 链接错误输出 ===\n%s\n", result.stdout)
if result.stderr is not None:
stdio.printf("=== clang++ stderr ===\n%s\n", result.stderr)
VLogger.error(f"链接失败,返回码: {result.returncode}, 命令: {cmd}", "link")
return 1
return 0
# ============================================================
# --traceback: <exe>.stackmap sidecar writer (address -> source line)
#
# Ported from TPC Phase2Translator._WriteStackMap. Runs after a successful
# link: llvm-dwarfdump parses the final PE's .debug_line/.debug_info, every
# address is normalised to a runtime RVA (addr - ImageBase) and the result is
# written as <exe>.stackmap for the runtime traceback.vp handler.
#
# Layout (all little-endian):
# header 20B = "TPCS" | u32 version=1 | u32 entry_count | u32 str_count | u32 str_bytes
# entries = entry_count * 20B, strictly ascending rva:
# u64 rva | u32 file_off | u32 line | u32 func_off
# pool = str_bytes UTF-8 bytes, '\0'-terminated strings, offset 0 is an
# empty-string sentinel, func_off==0 means "no function name"
#
# Every failure aborts the build (VLogger.error exits); there is no fallback.
# ============================================================
# row record: u64 rva | u64 file_path | u32 line | u32 pad
TB_ROW_SIZE: t.CDefine = 24
TB_ROW_OFF_FILE: t.CDefine = 8
TB_ROW_OFF_LINE: t.CDefine = 16
# func record: u64 low_rva | u64 high_rva | u64 name
TB_FUNC_SIZE: t.CDefine = 24
# dir record: u32 idx | u32 pad | u64 name
TB_DIR_SIZE: t.CDefine = 16
# file record: u32 idx | u32 dir_index | u64 name | u64 resolved_path
TB_FILE_SIZE: t.CDefine = 32
# initial capacities for the (growable) row / func tables
TB_ROW_CAP0: t.CDefine = 65536
TB_FUNC_CAP0: t.CDefine = 65536
# fixed per-CU table capacities (exceeding either aborts the build)
TB_DIR_CAP: t.CDefine = 4096
TB_FILE_CAP: t.CDefine = 65536
class TbPeLayout:
Ok: int
ImageBase: uint64
SizeOfImage: uint64
TextRva: uint64
def __new__(self) -> t.CPtr:
return t.CPtr(_mbuddy.alloc(TbPeLayout.__sizeof__()))
def __init__(self):
self.Ok = 0
self.ImageBase = 0
self.SizeOfImage = 0
self.TextRva = 0
class TbDump:
RowBuf: bytes
RowCap: int
RowCount: int
RawRowCount: int
FuncBuf: bytes
FuncCap: int
FuncCount: int
def __new__(self) -> t.CPtr:
return t.CPtr(_mbuddy.alloc(TbDump.__sizeof__()))
def __init__(self):
self.RowBuf = None
self.RowCap = 0
self.RowCount = 0
self.RawRowCount = 0
self.FuncBuf = None
self.FuncCap = 0
self.FuncCount = 0
class TbPool:
Pool: bytes
PoolLen: int
Offs: bytes
OffCount: int
def __new__(self) -> t.CPtr:
return t.CPtr(_mbuddy.alloc(TbPool.__sizeof__()))
def __init__(self):
self.Pool = None
self.PoolLen = 0
self.Offs = None
self.OffCount = 0
# ---- little-endian byte helpers (no struct.pack in Viper) ----
def _tb_u8(buf: bytes, off: sizet) -> uint32:
return uint32(int(buf[off]) & 0xFF)
def _tb_rd_u16(buf: bytes, off: sizet) -> uint32:
return _tb_u8(buf, off) | (_tb_u8(buf, off + 1) << 8)
def _tb_rd_u32(buf: bytes, off: sizet) -> uint32:
b0: uint32 = _tb_u8(buf, off)
b1: uint32 = _tb_u8(buf, off + 1)
b2: uint32 = _tb_u8(buf, off + 2)
b3: uint32 = _tb_u8(buf, off + 3)
return b0 | (b1 << 8) | (b2 << 16) | (b3 << 24)
def _tb_rd_u64(buf: bytes, off: sizet) -> uint64:
lo: uint64 = uint64(_tb_rd_u32(buf, off))
hi: uint64 = uint64(_tb_rd_u32(buf, off + 4))
return lo | (hi << 32)
def _tb_wr_u32(buf: bytes, off: sizet, v: uint64):
buf[off] = char(int(v & 0xFF))
buf[off + 1] = char(int((v >> 8) & 0xFF))
buf[off + 2] = char(int((v >> 16) & 0xFF))
buf[off + 3] = char(int((v >> 24) & 0xFF))
def _tb_wr_u64(buf: bytes, off: sizet, v: uint64):
_tb_wr_u32(buf, off, v & 0xFFFFFFFF)
_tb_wr_u32(buf, off + 4, (v >> 32) & 0xFFFFFFFF)
# ---- tiny string helpers (no re module) ----
def _tb_cc(s: str, i: sizet) -> int:
"""Byte value at s[i] as 0..255 (chars in DWARF text are ASCII)."""
if s is None:
return 0
return int(s[i]) & 0xFF
def _tb_contains(s: str, sub: str) -> int:
if s is None or sub is None:
return 0
return 1 if string.strstr(s, sub) is not None else 0
def _tb_startswith(s: str, pfx: str) -> int:
if s is None or pfx is None:
return 0
pl: sizet = string.strlen(pfx)
if string.strlen(s) < pl:
return 0
return 1 if string.strncmp(s, pfx, pl) == 0 else 0
def _tb_ltrim(s: str) -> str:
if s is None:
return None
i: sizet = 0
while _tb_cc(s, i) == 32 or _tb_cc(s, i) == 9:
i += 1
return s + i
def _tb_len_trimmed(s: str) -> sizet:
if s is None:
return 0
n: sizet = string.strlen(s)
while n > 0:
c: int = _tb_cc(s, n - 1)
if c == 32 or c == 9 or c == 13:
n -= 1
else:
break
return n
def _tb_dup_span(mb: memhub.MemBuddy*, src: str, n: sizet) -> str:
"""Copy src[0..n) into a fresh NUL-terminated string."""
if src is None:
return None
out: str = mb.alloc(n + 1)
if out is None:
return None
string.strncpy(out, src, n)
out[n] = '\0'
return out
def _tb_unquote(mb: memhub.MemBuddy*, v: str) -> str:
"""Trim trailing spaces and strip one pair of surrounding quotes (TPC _Unquote)."""
if v is None:
return None
n: sizet = _tb_len_trimmed(v)
if n >= 2 and _tb_cc(v, 0) == 34 and _tb_cc(v, n - 1) == 34:
return _tb_dup_span(mb, v + 1, n - 2)
return _tb_dup_span(mb, v, n)
def _tb_is_abs_path(s: str) -> int:
if s is None:
return 0
c0: int = _tb_cc(s, 0)
if c0 == 0:
return 0
if c0 == 47 or c0 == 92:
return 1
if _tb_cc(s, 1) == 58:
return 1
return 0
def _tb_join(mb: memhub.MemBuddy*, base: str, name: str) -> str:
"""Join base and name with a separator unless base already ends in one."""
if base is None or string.strlen(base) == 0:
return name
if name is None:
return base
bl: sizet = string.strlen(base)
nl: sizet = string.strlen(name)
buf: str = mb.alloc(bl + nl + 2)
if buf is None:
return name
last: int = _tb_cc(base, bl - 1)
if last == 47 or last == 92:
viperlib.snprintf(buf, bl + nl + 2, "%s%s", base, name)
else:
viperlib.snprintf(buf, bl + nl + 2, "%s/%s", base, name)
return buf
def _tb_parse_dec(s: str) -> int:
"""Parse a leading decimal integer, skipping leading blanks.
dwarfdump right-aligns the bracketed indices to a 3-wide field
(`file_names[ 1]:` / `include_directories[ 12]`), so `_tb_bracket_idx`
hands us a string whose first char is a space. Without the blank skip the
digit loop never runs and every index parses as 0 -> every file record's
pidx becomes 0 while a row's fidx is the real 1..N fit -> `_tb_file_rec`
never matches and EVERY row degrades to the "??" placeholder. Measured on
Test/LLvmLiteTest: all 9146 stackmap records carried the single '??' file
string while function names and line numbers were correct.
"""
v: int = 0
i: sizet = 0
while _tb_cc(s, i) == 32 or _tb_cc(s, i) == 9:
i += 1
while _tb_cc(s, i) >= 48 and _tb_cc(s, i) <= 57:
v = v * 10 + (_tb_cc(s, i) - 48)
i += 1
return v
def _tb_bracket_idx(s: str) -> int:
"""Parse the N in '[N]' (first bracket group of the line)."""
i: sizet = 0
while _tb_cc(s, i) != 0:
if _tb_cc(s, i) == 91:
return _tb_parse_dec(s + i + 1)
i += 1
return -1
def _tb_after_bracket(s: str) -> str:
i: sizet = 0
while _tb_cc(s, i) != 0:
if _tb_cc(s, i) == 93:
return s + i + 1
i += 1
return None
def _tb_value_after_sep(s: str) -> str:
"""Return the text after the first '=' or ':' (left-trimmed), or None."""
if s is None:
return None
i: sizet = 0
while _tb_cc(s, i) != 0:
c: int = _tb_cc(s, i)
if c == 61 or c == 58:
return _tb_ltrim(s + i + 1)
i += 1
return None
def _tb_hex_digit(ch: int) -> int:
if ch >= 48 and ch <= 57:
return ch - 48
if ch >= 97 and ch <= 102:
return ch - 87
if ch >= 65 and ch <= 70:
return ch - 55
return -1
def _tb_hex_in_parens(s: str, key: str) -> uint64:
"""Parse the 0x... value inside the parentheses right after key (DW_AT_low_pc (0x...)).
Returns 0 when the strict "( 0x" form is absent; DWARF prints the address form
that way, and a 0 result is harmless (out of the image VA range -> dropped).
"""
p: str = string.strstr(s, key)
if p is None:
return 0
p = p + string.strlen(key)
while _tb_cc(p, 0) == 32 or _tb_cc(p, 0) == 9:
p = p + 1
if _tb_cc(p, 0) != 40:
return 0
p = p + 1
while _tb_cc(p, 0) == 32:
p = p + 1
if _tb_cc(p, 0) != 48 or _tb_cc(p, 1) != 120:
return 0
q: str = p + 2
v: uint64 = 0
i: sizet = 0
hd: int = _tb_hex_digit(_tb_cc(q, 0))
while hd >= 0:
v = (v << 4) | uint64(hd)
i += 1
hd = _tb_hex_digit(_tb_cc(q, i))
return v
def _tb_quoted_after(mb: memhub.MemBuddy*, s: str, key: str) -> str:
"""Extract the first double-quoted string following key (DW_AT_name ("..."))."""
p: str = string.strstr(s, key)
if p is None:
return None
p = p + string.strlen(key)
q: str = string.strchr(p, 40)
if q is None:
return None
q = q + 1
while _tb_cc(q, 0) == 32:
q = q + 1
if _tb_cc(q, 0) != 34:
return None
q = q + 1
start: str = q
n: sizet = 0
while _tb_cc(q, n) != 0:
c: int = _tb_cc(q, n)
if c == 92:
if _tb_cc(q, n + 1) == 0:
break
n += 2
continue
if c == 34:
break
n += 1
return _tb_dup_span(mb, start, n)
def _tb_dir_lookup(dirs: bytes, count: int, idx: int) -> str:
i: int = 0
while i < count:
if int(_tb_rd_u32(dirs, sizet(i) * TB_DIR_SIZE)) == idx:
return rawptr(_tb_rd_u64(dirs, sizet(i) * TB_DIR_SIZE + 8))
i += 1
return None
def _tb_file_rec(files: bytes, count: int, idx: int) -> int:
i: int = 0
while i < count:
if int(_tb_rd_u32(files, sizet(i) * TB_FILE_SIZE)) == idx:
return i
i += 1
return -1
def _tb_resolve_file(mb: memhub.MemBuddy*, files: bytes, fcount: int,
dirs: bytes, dcount: int, frec: int) -> str:
"""Resolve a file-table entry to a path (absolute name wins, else dir join)."""
roff: sizet = sizet(frec) * TB_FILE_SIZE
cached: uint64 = _tb_rd_u64(files, roff + 24)
if cached != 0:
return rawptr(cached)
name: str = rawptr(_tb_rd_u64(files, roff + 8))
path: str = name
if name is not None and _tb_is_abs_path(name) == 0:
didx: int = int(_tb_rd_u32(files, roff + 4))
base: str = _tb_dir_lookup(dirs, dcount, didx)
if base is not None and string.strlen(base) > 0:
if _tb_is_abs_path(base) == 0:
root: str = _tb_dir_lookup(dirs, dcount, 0)
if root is not None and _tb_is_abs_path(root) != 0:
base = _tb_join(mb, root, base)
path = _tb_join(mb, base, name)
if path is None or string.strlen(path) == 0:
path = "??"
_tb_wr_u64(files, roff + 24, t.CUInt64T(path))
return path
def _tb_grow_rec(mb: memhub.MemBuddy*, buf: bytes, old_cap: int,
rec_size: int, new_cap: int) -> bytes:
nb: bytes = mb.alloc(sizet(new_cap) * sizet(rec_size))
if nb is None:
return None
string.memcpy(nb, buf, sizet(old_cap) * sizet(rec_size))
mb.free(buf)
return nb
def _tb_emit_row(mb: memhub.MemBuddy*, d: TbDump | t.CPtr,
addr: uint64, path: str, line: int) -> int:
if d is None:
return 1
if d.RowCount >= d.RowCap:
ncap: int = d.RowCap * 2
nb: bytes = _tb_grow_rec(mb, d.RowBuf, d.RowCap, TB_ROW_SIZE, ncap)
if nb is None:
VLogger.error("--traceback: DWARF 行记录表扩容失败,终止编译", "TRACEBACK")
return 1
d.RowBuf = nb
d.RowCap = ncap
off: sizet = sizet(d.RowCount) * TB_ROW_SIZE
_tb_wr_u64(d.RowBuf, off, addr)
_tb_wr_u64(d.RowBuf, off + TB_ROW_OFF_FILE, t.CUInt64T(path))
_tb_wr_u32(d.RowBuf, off + TB_ROW_OFF_LINE, uint64(line))
d.RowCount = d.RowCount + 1
d.RawRowCount = d.RawRowCount + 1
return 0
def _tb_emit_func(mb: memhub.MemBuddy*, d: TbDump | t.CPtr,
low: uint64, high: uint64, name: str) -> int:
if d is None:
return 1
if d.FuncCount >= d.FuncCap:
ncap: int = d.FuncCap * 2
nb: bytes = _tb_grow_rec(mb, d.FuncBuf, d.FuncCap, TB_FUNC_SIZE, ncap)
if nb is None:
VLogger.error("--traceback: DWARF 子程序表扩容失败,终止编译", "TRACEBACK")
return 1
d.FuncBuf = nb
d.FuncCap = ncap
off: sizet = sizet(d.FuncCount) * TB_FUNC_SIZE
_tb_wr_u64(d.FuncBuf, off, low)
_tb_wr_u64(d.FuncBuf, off + 8, high)
_tb_wr_u64(d.FuncBuf, off + 16, t.CUInt64T(name))
d.FuncCount = d.FuncCount + 1
return 0
def _tb_func_name_for(fbuf: bytes, fcount: int, rva: uint64) -> str:
"""bisect_right over the (sorted) func low_pc table, then range check."""
if fbuf is None or fcount <= 0:
return None
lo: int = 0
hi: int = fcount
while lo < hi:
mid: int = lo + (hi - lo) / 2
if _tb_rd_u64(fbuf, sizet(mid) * TB_FUNC_SIZE) <= rva:
lo = mid + 1
else:
hi = mid
k: int = lo - 1
if k < 0:
return None
low: uint64 = _tb_rd_u64(fbuf, sizet(k) * TB_FUNC_SIZE)
high: uint64 = _tb_rd_u64(fbuf, sizet(k) * TB_FUNC_SIZE + 8)
if low <= rva and rva < high:
return rawptr(_tb_rd_u64(fbuf, sizet(k) * TB_FUNC_SIZE + 16))
return None
def _tb_intern(p: TbPool | t.CPtr, s: str) -> uint64:
"""Intern s into the pool (linear scan; keeps offset 0 as the empty sentinel)."""
if p is None or s is None:
return 0
k: int = 0
while k < p.OffCount:
o: uint64 = _tb_rd_u64(p.Offs, sizet(k) * 8)
if string.strcmp(p.Pool + sizet(o), s) == 0:
return o
k += 1
off: uint64 = uint64(p.PoolLen)
n: sizet = string.strlen(s)
string.memcpy(p.Pool + sizet(off), s, n)
p.Pool[sizet(off) + n] = '\0'
p.PoolLen = p.PoolLen + int(n) + 1
_tb_wr_u64(p.Offs, sizet(p.OffCount) * 8, off)
p.OffCount = p.OffCount + 1
return off
def _tb_read_pe_layout(mb: memhub.MemBuddy*, exe_path: str) -> TbPeLayout | t.CPtr:
"""Read (ImageBase, SizeOfImage, .text RVA) from the PE headers."""
lay: TbPeLayout | t.CPtr = TbPeLayout()
if lay is None:
return None
f: rawptr[fileio.File] = fileio.File(exe_path, fileio.MODE.R)
if f.closed:
return lay
HEAD: sizet = 4096
head: bytes = mb.alloc(HEAD)
if head is None:
f.close()
return lay
got: LONG = f.read_all(head, HEAD)
f.close()
if got < 0x40:
return lay
if _tb_cc(head, 0) != 77 or _tb_cc(head, 1) != 90:
return lay
e_lfanew: uint64 = uint64(_tb_rd_u32(head, 0x3C))
if e_lfanew + 24 > uint64(got):
return lay
if _tb_cc(head, sizet(e_lfanew)) != 80 or _tb_cc(head, sizet(e_lfanew) + 1) != 69:
return lay
coff: sizet = sizet(e_lfanew) + 4
nsec: uint64 = uint64(_tb_rd_u16(head, coff + 2))
optsz: uint64 = uint64(_tb_rd_u16(head, coff + 16))
opt: sizet = coff + 20
if uint64(opt) + 32 > uint64(got):
return lay
magic: uint64 = uint64(_tb_rd_u16(head, opt))
img_base: uint64 = 0
if magic == 0x20B:
img_base = _tb_rd_u64(head, opt + 24)
elif magic == 0x10B:
img_base = uint64(_tb_rd_u32(head, opt + 28))
else:
return lay
size_img: uint64 = uint64(_tb_rd_u32(head, opt + 56))
sec_off: sizet = opt + sizet(optsz)
text_rva: uint64 = 0x1000
min_rva: uint64 = 0xFFFFFFFF
found_text: int = 0
have_sec: int = 0
si: uint64 = 0
while si < nsec:
base: sizet = sec_off + sizet(si) * 40
if uint64(base) + 40 > uint64(got):
break
va: uint64 = uint64(_tb_rd_u32(head, base + 12))
have_sec = 1
if va < min_rva:
min_rva = va
if _tb_cc(head, base) == 46 and _tb_cc(head, base + 1) == 116 \
and _tb_cc(head, base + 2) == 101 and _tb_cc(head, base + 3) == 120 \
and _tb_cc(head, base + 4) == 116:
text_rva = va
found_text = 1
si += 1
if found_text == 0 and have_sec != 0 and min_rva != 0xFFFFFFFF:
text_rva = min_rva
lay.ImageBase = img_base
lay.SizeOfImage = size_img
lay.TextRva = text_rva
lay.Ok = 1
return lay
def _tb_parse_dump(mb: memhub.MemBuddy*, dump: str) -> TbDump | t.CPtr:
"""Parse llvm-dwarfdump text into a row table (addr, path, line) and a func table."""
d: TbDump | t.CPtr = TbDump()
if d is None:
return None
LINE_CAP: sizet = 8192
d.RowBuf = mb.alloc(sizet(TB_ROW_CAP0) * TB_ROW_SIZE)
d.RowCap = TB_ROW_CAP0
d.FuncBuf = mb.alloc(sizet(TB_FUNC_CAP0) * TB_FUNC_SIZE)
d.FuncCap = TB_FUNC_CAP0
dirs: bytes = mb.alloc(sizet(TB_DIR_CAP) * TB_DIR_SIZE)
files: bytes = mb.alloc(sizet(TB_FILE_CAP) * TB_FILE_SIZE)
linebuf: bytes = mb.alloc(LINE_CAP)
if d.RowBuf is None or d.FuncBuf is None or dirs is None or files is None or linebuf is None:
VLogger.error("--traceback: dwarfdump 解析缓冲分配失败,终止编译", "TRACEBACK")
return None
in_line: int = 0
dir_count: int = 0
file_count: int = 0
pkind: int = 0 # 0=none, 1=file, 2=dir
pidx: int = 0
pname: str = None
pdir: int = 0
in_prog: int = 0
cur_low: uint64 = 0
cur_high: uint64 = 0
cur_has_low: int = 0
cur_has_high: int = 0
cname: str = None
dlen: sizet = string.strlen(dump)
pos: sizet = 0
while pos < dlen:
ls: sizet = pos
while pos < dlen and _tb_cc(dump, pos) != 10:
pos += 1
le: sizet = pos
if pos < dlen:
pos += 1
if le > ls and _tb_cc(dump, le - 1) == 13:
le -= 1
ll: sizet = le - ls
if ll == 0:
continue
if ll >= LINE_CAP:
ll = LINE_CAP - 1
string.strncpy(linebuf, dump + ls, ll)
linebuf[ll] = '\0'
st: str = _tb_ltrim(linebuf)
# ---- line-table section gate ----
if _tb_contains(st, "debug_line contents") != 0:
in_line = 1
continue
if _tb_contains(st, "contents:") != 0 and _tb_contains(st, "debug_") != 0:
in_line = 0
continue
if in_line != 0:
if _tb_startswith(st, "debug_line[") != 0:
# new CU: commit pending file, reset both tables
if pkind == 1 and pname is not None and file_count < TB_FILE_CAP:
_tb_wr_u32(files, sizet(file_count) * TB_FILE_SIZE, uint64(pidx))
_tb_wr_u32(files, sizet(file_count) * TB_FILE_SIZE + 4, uint64(pdir))
_tb_wr_u64(files, sizet(file_count) * TB_FILE_SIZE + 8, t.CUInt64T(pname))
_tb_wr_u64(files, sizet(file_count) * TB_FILE_SIZE + 24, uint64(0))
file_count += 1
pkind = 0
pname = None
pdir = 0
dir_count = 0
file_count = 0
continue
if _tb_startswith(st, "file_names[") != 0:
if pkind == 1 and pname is not None and file_count < TB_FILE_CAP:
_tb_wr_u32(files, sizet(file_count) * TB_FILE_SIZE, uint64(pidx))
_tb_wr_u32(files, sizet(file_count) * TB_FILE_SIZE + 4, uint64(pdir))
_tb_wr_u64(files, sizet(file_count) * TB_FILE_SIZE + 8, t.CUInt64T(pname))
_tb_wr_u64(files, sizet(file_count) * TB_FILE_SIZE + 24, uint64(0))
file_count += 1
pname = None
pdir = 0
pkind = 1
pidx = _tb_bracket_idx(st)
continue
if _tb_startswith(st, "directory_names[") != 0 or _tb_startswith(st, "include_directories[") != 0:
if pkind == 1 and pname is not None and file_count < TB_FILE_CAP:
_tb_wr_u32(files, sizet(file_count) * TB_FILE_SIZE, uint64(pidx))
_tb_wr_u32(files, sizet(file_count) * TB_FILE_SIZE + 4, uint64(pdir))
_tb_wr_u64(files, sizet(file_count) * TB_FILE_SIZE + 8, t.CUInt64T(pname))
_tb_wr_u64(files, sizet(file_count) * TB_FILE_SIZE + 24, uint64(0))
file_count += 1
pname = None
pdir = 0
pidx = _tb_bracket_idx(st)
val: str = _tb_value_after_sep(_tb_after_bracket(st))
if val is not None and _tb_len_trimmed(val) > 0:
if dir_count < TB_DIR_CAP:
_tb_wr_u32(dirs, sizet(dir_count) * TB_DIR_SIZE, uint64(pidx))
_tb_wr_u64(dirs, sizet(dir_count) * TB_DIR_SIZE + 8, t.CUInt64T(_tb_unquote(mb, val)))
dir_count += 1
pkind = 0
else:
pkind = 2
continue
if _tb_startswith(st, "name:") != 0 and pkind != 0:
val2: str = _tb_unquote(mb, _tb_ltrim(st + 5))
if pkind == 1:
pname = val2
else:
if dir_count < TB_DIR_CAP and val2 is not None:
_tb_wr_u32(dirs, sizet(dir_count) * TB_DIR_SIZE, uint64(pidx))
_tb_wr_u64(dirs, sizet(dir_count) * TB_DIR_SIZE + 8, t.CUInt64T(val2))
dir_count += 1
pkind = 0
continue
if _tb_startswith(st, "dir_index:") != 0 and pkind == 1:
# `"dir_index:"` 含冒号共 10 字符:st + 9 指向冒号本身,解析恒得 0
# (目录索引全丢 → 相对文件名拼不出绝对路径,File 行只剩裸文件名)。
pdir = _tb_parse_dec(_tb_ltrim(st + 10))
continue
if _tb_startswith(st, "Address") != 0 and _tb_contains(st, "Column") != 0:
if pkind == 1 and pname is not None and file_count < TB_FILE_CAP:
_tb_wr_u32(files, sizet(file_count) * TB_FILE_SIZE, uint64(pidx))
_tb_wr_u32(files, sizet(file_count) * TB_FILE_SIZE + 4, uint64(pdir))
_tb_wr_u64(files, sizet(file_count) * TB_FILE_SIZE + 8, t.CUInt64T(pname))
_tb_wr_u64(files, sizet(file_count) * TB_FILE_SIZE + 24, uint64(0))
file_count += 1
pkind = 0
pname = None
pdir = 0
continue
if _tb_cc(linebuf, 0) == 48 and _tb_cc(linebuf, 1) == 120:
if pkind == 1 and pname is not None and file_count < TB_FILE_CAP:
_tb_wr_u32(files, sizet(file_count) * TB_FILE_SIZE, uint64(pidx))
_tb_wr_u32(files, sizet(file_count) * TB_FILE_SIZE + 4, uint64(pdir))
_tb_wr_u64(files, sizet(file_count) * TB_FILE_SIZE + 8, t.CUInt64T(pname))
_tb_wr_u64(files, sizet(file_count) * TB_FILE_SIZE + 24, uint64(0))
file_count += 1
pkind = 0
pname = None
pdir = 0
ri: sizet = 2
addr: uint64 = 0
hd: int = _tb_hex_digit(_tb_cc(linebuf, ri))
while hd >= 0:
addr = (addr << 4) | uint64(hd)
ri += 1
hd = _tb_hex_digit(_tb_cc(linebuf, ri))
while ri < ll and (_tb_cc(linebuf, ri) == 32 or _tb_cc(linebuf, ri) == 9):
ri += 1
lno: int = 0
while ri < ll and _tb_cc(linebuf, ri) >= 48 and _tb_cc(linebuf, ri) <= 57:
lno = lno * 10 + (_tb_cc(linebuf, ri) - 48)
ri += 1
while ri < ll and (_tb_cc(linebuf, ri) == 32 or _tb_cc(linebuf, ri) == 9):
ri += 1
while ri < ll and _tb_cc(linebuf, ri) >= 48 and _tb_cc(linebuf, ri) <= 57:
ri += 1
while ri < ll and (_tb_cc(linebuf, ri) == 32 or _tb_cc(linebuf, ri) == 9):
ri += 1
fidx: int = 0
while ri < ll and _tb_cc(linebuf, ri) >= 48 and _tb_cc(linebuf, ri) <= 57:
fidx = fidx * 10 + (_tb_cc(linebuf, ri) - 48)
ri += 1
if lno > 0:
frec: int = _tb_file_rec(files, file_count, fidx)
fpath: str = None
if frec >= 0:
fpath = _tb_resolve_file(mb, files, file_count, dirs, dir_count, frec)
if fpath is None:
fpath = "??"
if _tb_emit_row(mb, d, addr, fpath, lno) != 0:
return None
continue
# ---- DWARF debug_info subprogram parsing ----
if _tb_contains(linebuf, "DW_TAG_") != 0:
if in_prog != 0 and cur_has_low != 0 and cname is not None and string.strlen(cname) > 0:
hi: uint64 = cur_low + 1
if cur_has_high != 0:
hi = cur_high
if hi <= cur_low:
hi = cur_low + cur_high
if hi > cur_low:
if _tb_emit_func(mb, d, cur_low, hi, cname) != 0:
return None
in_prog = 0
cur_low = 0
cur_high = 0
cur_has_low = 0
cur_has_high = 0
cname = None
if _tb_contains(linebuf, "DW_TAG_subprogram") != 0:
in_prog = 1
continue
if in_prog == 0:
continue
if _tb_contains(linebuf, "DW_AT_low_pc") != 0:
cur_low = _tb_hex_in_parens(linebuf, "DW_AT_low_pc")
cur_has_low = 1
continue
if _tb_contains(linebuf, "DW_AT_high_pc") != 0:
cur_high = _tb_hex_in_parens(linebuf, "DW_AT_high_pc")
cur_has_high = 1
continue
if _tb_contains(linebuf, "DW_AT_name") != 0 and cname is None:
cname = _tb_quoted_after(mb, linebuf, "DW_AT_name")
continue
# trailing subprogram
if in_prog != 0 and cur_has_low != 0 and cname is not None and string.strlen(cname) > 0:
hi2: uint64 = cur_low + 1
if cur_has_high != 0:
hi2 = cur_high
if hi2 <= cur_low:
hi2 = cur_low + cur_high
if hi2 > cur_low:
if _tb_emit_func(mb, d, cur_low, hi2, cname) != 0:
return None
return d
def WriteStackMap(mb: memhub.MemBuddy*, exe_path: str) -> int:
"""--traceback: export <exe>.stackmap after a successful link. 0 = ok."""
if mb is None or exe_path is None:
VLogger.error("--traceback: WriteStackMap 参数为空,终止编译", "TRACEBACK")
return 1
lay: TbPeLayout | t.CPtr = _tb_read_pe_layout(mb, exe_path)
if lay is None or lay.Ok == 0:
VLogger.error(f"--traceback: 只支持 Windows PE 产物,但 {exe_path} 不是有效 PE(这是 --traceback 的要求)", "TRACEBACK")
return 1
size_of_image: uint64 = lay.SizeOfImage
if size_of_image == 0:
VLogger.error(f"--traceback: 读到的 PE.SizeOfImage 非正({size_of_image}),无法界定 RVA 域(这是 --traceback 的要求)", "TRACEBACK")
return 1
img_lo: uint64 = lay.ImageBase
img_hi: uint64 = lay.ImageBase + size_of_image
text_rva: uint64 = lay.TextRva
# ---- 1) run llvm-dwarfdump (bare name: CreateProcessA resolves it via PATH) ----
cmd_len: sizet = string.strlen(exe_path) + 64
cmd: bytes = mb.alloc(cmd_len)
if cmd is None:
VLogger.error("--traceback: dwarfdump 命令缓冲分配失败,终止编译", "TRACEBACK")
return 1
viperlib.snprintf(cmd, cmd_len, "llvm-dwarfdump --debug-line --debug-info \"%s\"", exe_path)
res: rawptr[subprocess.CompletedProcess] = subprocess.run(cmd, True, True)
if res is None or res.returncode != 0:
rc: int = -1
if res is not None:
rc = res.returncode
VLogger.error(f"--traceback: 调用 llvm-dwarfdump 失败(返回码 {rc};请确认 PATH 内含 LLVM 工具),终止编译", "TRACEBACK")
return 1
dump: str = res.stdout
if dump is None:
VLogger.error("--traceback: llvm-dwarfdump 无输出,终止编译", "TRACEBACK")
return 1
d: TbDump | t.CPtr = _tb_parse_dump(mb, dump)
if d is None:
return 1
if d.RawRowCount == 0:
VLogger.error(f"--traceback: 未能从 {exe_path} 解析出任何 DWARF 行记录(.debug_line 为空?这是 --traceback 的要求)", "TRACEBACK")
return 1
# ---- 2) rows: normalise to RVA, same rva keeps the largest line ----
n: int = d.RowCount
dropped_rows: int = 0
kept: int = 0
i: int = 0
while i < n:
a: uint64 = _tb_rd_u64(d.RowBuf, sizet(i) * TB_ROW_SIZE)
if a < img_lo or a >= img_hi:
dropped_rows += 1
i += 1
continue
if kept != i:
_tb_wr_u64(d.RowBuf, sizet(kept) * TB_ROW_SIZE, a - img_lo)
_tb_wr_u64(d.RowBuf, sizet(kept) * TB_ROW_SIZE + TB_ROW_OFF_FILE,
_tb_rd_u64(d.RowBuf, sizet(i) * TB_ROW_SIZE + TB_ROW_OFF_FILE))
_tb_wr_u32(d.RowBuf, sizet(kept) * TB_ROW_SIZE + TB_ROW_OFF_LINE,
uint64(_tb_rd_u32(d.RowBuf, sizet(i) * TB_ROW_SIZE + TB_ROW_OFF_LINE)))
else:
_tb_wr_u64(d.RowBuf, sizet(kept) * TB_ROW_SIZE, a - img_lo)
kept += 1
i += 1
tmp: bytes = mb.alloc(TB_ROW_SIZE)
if tmp is None:
VLogger.error("--traceback: 行记录排序缓冲分配失败,终止编译", "TRACEBACK")
return 1
si2: int = 1
while si2 < kept:
string.memcpy(tmp, d.RowBuf + sizet(si2) * TB_ROW_SIZE, TB_ROW_SIZE)
sj: int = si2
while sj > 0:
if _tb_rd_u64(d.RowBuf, sizet(sj - 1) * TB_ROW_SIZE) <= _tb_rd_u64(tmp, 0):
break
string.memcpy(d.RowBuf + sizet(sj) * TB_ROW_SIZE,
d.RowBuf + sizet(sj - 1) * TB_ROW_SIZE, TB_ROW_SIZE)
sj -= 1
string.memcpy(d.RowBuf + sizet(sj) * TB_ROW_SIZE, tmp, TB_ROW_SIZE)
si2 += 1
uniq: int = 0
i = 0
while i < kept:
rv: uint64 = _tb_rd_u64(d.RowBuf, sizet(i) * TB_ROW_SIZE)
bl: uint64 = uint64(_tb_rd_u32(d.RowBuf, sizet(i) * TB_ROW_SIZE + TB_ROW_OFF_LINE))
bp: uint64 = _tb_rd_u64(d.RowBuf, sizet(i) * TB_ROW_SIZE + TB_ROW_OFF_FILE)
j: int = i + 1
while j < kept and _tb_rd_u64(d.RowBuf, sizet(j) * TB_ROW_SIZE) == rv:
lj: uint64 = uint64(_tb_rd_u32(d.RowBuf, sizet(j) * TB_ROW_SIZE + TB_ROW_OFF_LINE))
if lj > bl:
bl = lj
bp = _tb_rd_u64(d.RowBuf, sizet(j) * TB_ROW_SIZE + TB_ROW_OFF_FILE)
j += 1
if uniq != i:
_tb_wr_u64(d.RowBuf, sizet(uniq) * TB_ROW_SIZE, rv)
_tb_wr_u64(d.RowBuf, sizet(uniq) * TB_ROW_SIZE + TB_ROW_OFF_FILE, bp)
_tb_wr_u32(d.RowBuf, sizet(uniq) * TB_ROW_SIZE + TB_ROW_OFF_LINE, bl)
uniq += 1
i = j
if dropped_rows > 0:
VLogger.warning(f"--traceback: 丢弃 {dropped_rows} 条无法归一化的 DWARF 行记录(地址不在影像 VA 区间,多为 COMDAT 去重副本)", "TRACEBACK")
if uniq == 0:
VLogger.error(f"--traceback: 从 {exe_path} 得到的行记录全部无法归一到运行期 RVA 域,终止编译", "TRACEBACK")
return 1
# ---- 3) subprograms: normalise + sort by low_rva ----
fcount: int = d.FuncCount
fkept: int = 0
dropped_funcs: int = 0
fi: int = 0
while fi < fcount:
low: uint64 = _tb_rd_u64(d.FuncBuf, sizet(fi) * TB_FUNC_SIZE)
if low < img_lo or low >= img_hi:
dropped_funcs += 1
fi += 1
continue
high: uint64 = _tb_rd_u64(d.FuncBuf, sizet(fi) * TB_FUNC_SIZE + 8)
lo_rva: uint64 = low - img_lo
hi_rva: uint64 = high - img_lo
if hi_rva <= lo_rva:
hi_rva = lo_rva + 1
if hi_rva > size_of_image:
hi_rva = size_of_image
_tb_wr_u64(d.FuncBuf, sizet(fkept) * TB_FUNC_SIZE, lo_rva)
_tb_wr_u64(d.FuncBuf, sizet(fkept) * TB_FUNC_SIZE + 8, hi_rva)
_tb_wr_u64(d.FuncBuf, sizet(fkept) * TB_FUNC_SIZE + 16,
_tb_rd_u64(d.FuncBuf, sizet(fi) * TB_FUNC_SIZE + 16))
fkept += 1
fi += 1
ftmp: bytes = mb.alloc(TB_FUNC_SIZE)
if ftmp is None:
VLogger.error("--traceback: 子程序排序缓冲分配失败,终止编译", "TRACEBACK")
return 1
fj: int = 1
while fj < fkept:
string.memcpy(ftmp, d.FuncBuf + sizet(fj) * TB_FUNC_SIZE, TB_FUNC_SIZE)
fk: int = fj
while fk > 0:
if _tb_rd_u64(d.FuncBuf, sizet(fk - 1) * TB_FUNC_SIZE) <= _tb_rd_u64(ftmp, 0):
break
string.memcpy(d.FuncBuf + sizet(fk) * TB_FUNC_SIZE,
d.FuncBuf + sizet(fk - 1) * TB_FUNC_SIZE, TB_FUNC_SIZE)
fk -= 1
string.memcpy(d.FuncBuf + sizet(fk) * TB_FUNC_SIZE, ftmp, TB_FUNC_SIZE)
fj += 1
if dropped_funcs > 0:
VLogger.warning(f"--traceback: 丢弃 {dropped_funcs} 个无法归一化的 DWARF 子程序(low_pc 不在影像 VA 区间)", "TRACEBACK")
# ---- 4) string pool: exact upper bound, then intern (offset 0 = sentinel) ----
pool_need: sizet = 1
i = 0
while i < uniq:
pth: str = rawptr(_tb_rd_u64(d.RowBuf, sizet(i) * TB_ROW_SIZE + TB_ROW_OFF_FILE))
pool_need += string.strlen(pth) + 1
i += 1
i = 0
while i < fkept:
nm0: str = rawptr(_tb_rd_u64(d.FuncBuf, sizet(i) * TB_FUNC_SIZE + 16))
if nm0 is not None:
pool_need += string.strlen(nm0) + 1
i += 1
p: TbPool | t.CPtr = TbPool()
if p is None:
VLogger.error("--traceback: 字符串池对象分配失败,终止编译", "TRACEBACK")
return 1
p.Pool = mb.alloc(pool_need)
p.Offs = mb.alloc(sizet(uniq + fkept) * 8 + 64)
if p.Pool is None or p.Offs is None:
VLogger.error("--traceback: 字符串池缓冲分配失败,终止编译", "TRACEBACK")
return 1
p.Pool[0] = '\0'
p.PoolLen = 1
_tb_wr_u64(p.Offs, 0, uint64(0))
p.OffCount = 1
# file_off / func_off per unique row (u32 each)
ro: bytes = mb.alloc(sizet(uniq) * 8 + 8)
if ro is None:
VLogger.error("--traceback: 记录偏移缓冲分配失败,终止编译", "TRACEBACK")
return 1
i = 0
while i < uniq:
pth2: str = rawptr(_tb_rd_u64(d.RowBuf, sizet(i) * TB_ROW_SIZE + TB_ROW_OFF_FILE))
foff: uint64 = _tb_intern(p, pth2)
noff: uint64 = 0
rv2: uint64 = _tb_rd_u64(d.RowBuf, sizet(i) * TB_ROW_SIZE)
nm2: str = _tb_func_name_for(d.FuncBuf, fkept, rv2)
if nm2 is not None:
noff = _tb_intern(p, nm2)
_tb_wr_u32(ro, sizet(i) * 8, foff)
_tb_wr_u32(ro, sizet(i) * 8 + 4, noff)
i += 1
# ---- 5) self-checks (mirror the runtime LoadMapFile validation) ----
prev_rva: uint64 = 0
outside: int = 0
plausible: int = 0
ci: int = 0
while ci < uniq:
cv: uint64 = _tb_rd_u64(d.RowBuf, sizet(ci) * TB_ROW_SIZE)
if ci > 0 and cv <= prev_rva:
VLogger.error(f"--traceback 自检失败:记录 RVA 非严格升序(第 {ci} 条会破坏运行期二分查找),终止编译", "TRACEBACK")
return 1
prev_rva = cv
if cv >= size_of_image:
outside += 1
if cv >= text_rva and cv < size_of_image:
plausible += 1
ci += 1
if outside > 0:
VLogger.error(f"--traceback 自检失败:有 {outside} 条记录的 RVA 落在 [0, SizeOfImage=0x{size_of_image:x}) 之外,终止编译", "TRACEBACK")
return 1
if plausible <= 0:
VLogger.error("--traceback 自检失败:没有任何记录的 RVA 落在运行期可能出现的区间(RVA 口径疑似错误),终止编译", "TRACEBACK")
return 1
# file_off must be >= 1: offset 0 is the empty-string sentinel and the runtime
# rejects a whole map whose record file_off is 0.
ci = 0
while ci < uniq:
fo: uint64 = uint64(_tb_rd_u32(ro, sizet(ci) * 8))
if fo == 0:
VLogger.error("--traceback 自检失败:存在 file_off==0 的记录(空路径,运行期会拒绝该旁文件),终止编译", "TRACEBACK")
return 1
ci += 1
# ---- 6) serialise and write <exe>.stackmap ----
total: sizet = 20 + sizet(uniq) * 20 + sizet(p.PoolLen)
out: bytes = mb.alloc(total)
if out is None:
VLogger.error("--traceback: 旁文件输出缓冲分配失败,终止编译", "TRACEBACK")
return 1
out[0] = 'T'
out[1] = 'P'
out[2] = 'C'
out[3] = 'S'
_tb_wr_u32(out, 4, uint64(1))
_tb_wr_u32(out, 8, uint64(uniq))
_tb_wr_u32(out, 12, uint64(p.OffCount))
_tb_wr_u32(out, 16, uint64(p.PoolLen))
ri2: int = 0
while ri2 < uniq:
roff2: sizet = 20 + sizet(ri2) * 20
rv3: uint64 = _tb_rd_u64(d.RowBuf, sizet(ri2) * TB_ROW_SIZE)
ln3: uint64 = uint64(_tb_rd_u32(d.RowBuf, sizet(ri2) * TB_ROW_SIZE + TB_ROW_OFF_LINE))
_tb_wr_u64(out, roff2, rv3)
_tb_wr_u32(out, roff2 + 8, uint64(_tb_rd_u32(ro, sizet(ri2) * 8)))
_tb_wr_u32(out, roff2 + 12, ln3)
_tb_wr_u32(out, roff2 + 16, uint64(_tb_rd_u32(ro, sizet(ri2) * 8 + 4)))
ri2 += 1
string.memcpy(out + 20 + sizet(uniq) * 20, p.Pool, sizet(p.PoolLen))
map_len: sizet = string.strlen(exe_path) + 16
map_path: str = mb.alloc(map_len)
if map_path is None:
VLogger.error("--traceback: 旁文件路径缓冲分配失败,终止编译", "TRACEBACK")
return 1
viperlib.snprintf(map_path, map_len, "%s.stackmap", exe_path)
mf: rawptr[fileio.File] = fileio.File(map_path, fileio.MODE.W)
if mf.closed:
VLogger.error(f"--traceback: 写出旁文件失败(无法打开 {map_path}),终止编译", "TRACEBACK")
return 1
mf.write(out, total)
mf.close()
VLogger.success(f"--traceback: 已写出地址→源行旁文件 {map_path}({uniq} 条记录,{total} 字节)", "TRACEBACK")
return 0
def run_pipeline(ir_buf: bytes, ir_len: sizet,
temp_dir: str, output_dir: str, module_name: str,
cc_cmd: str, cc_flags: str,
linker_cmd: str, linker_flags: str, linker_output: str,
includes_binary_dir: str, linker_static_libs: str = None, linker_dynamic_libs: str = None) -> BuildResult | t.CPtr:
"""执行完整编译管线
Args:
ir_buf: LLVM IR 文本缓冲区
ir_len: IR 文本长度
temp_dir: 临时目录
output_dir: 输出目录
module_name: 模块名
cc_cmd: 编译器命令
cc_flags: 编译器参数
linker_cmd: 链接器命令
linker_flags: 链接器参数
linker_output: 输出文件名
includes_binary_dir: includes.binary 目录路径(链接时附加预编译 .obj)
linker_static_libs: 显式静态链接文件(.a/.obj,空格分隔绝对路径,可为 None)
Returns:
BuildResult 对象
"""
result: BuildResult | t.CPtr = BuildResult()
if result is None:
return None
# Step 0: 确保 temp/output 目录存在(自动创建,避免写文件失败)
ensure_dir(temp_dir)
ensure_dir(output_dir)
# Step 1: 写 .ll 文件
ret: int = write_ir_to_file(ir_buf, ir_len, temp_dir, module_name)
if ret != 0:
result.Success = 0
result.ErrorMsg = "写 .ll 文件失败"
return result
# Step 2: llc 编译 .ll → .obj
# 构造切片 .ll 文件路径
ir_path: str = build_sliced_path(temp_dir, module_name, "ll")
if ir_path is None:
result.Success = 0
result.ErrorMsg = "内存分配失败"
return result
ret = compile_ll_to_obj(ir_path, output_dir, module_name, cc_cmd, cc_flags)
if ret != 0:
result.Success = 0
result.ErrorMsg = "llc 编译失败"
return result
# Step 3: clang++ 链接 .obj → .exe(附加 includes.binary 预编译 .obj 与 static_libs/dynamic_libs)
ret = link_obj_to_exe(output_dir, module_name, linker_cmd, linker_flags, linker_output,
includes_binary_dir, linker_static_libs, linker_dynamic_libs)
if ret != 0:
result.Success = 0
result.ErrorMsg = "链接失败"
return result
result.Success = 1
return result