Stage 2f took CoffFile from nothing to fully ported: image headers, the section table, the CodeView record naming the PDB, the export table, the exception table with its RUNTIME_FUNCTION chaining, the COFF symbol table, and the DWARF subprogram DIEs. Every method. Then stage 2g stopped, for a reason worth stating precisely, because it decides whether the whole port reaches its goal.
The part that went well
Seven Rust tests written against the same DLLs the C++ suite uses passed on the first run — including libtest.dll's 38 exception-table entries with their exact addresses and sizes, and dllmain.dll's chained unwind info.
The interesting piece is RUNTIME_FUNCTION merging. LLVM exposes no accessor for the Exception Table — llvm-objdump walks it by hand too, and the C++ says so in a comment — so both implementations reinterpret a data directory as an array of twelve-byte records, follow UNW_FLAG_CHAININFO to each entry's primary, and merge entries that are both adjacent and share a primary.
Two differences, both in the Rust's favour and both cheap: the array is bounds-checked, and the chain walk is capped at 64 links. The C++ loop is unbounded, so a cyclic chain in a corrupt image would hang it. That is not a bug anyone has hit; it is the sort of thing you notice only because porting forces you to write the loop yourself.
The bug: two predicates in one header
The comparison reported 53 symbols against 52. A count alone says nothing useful, so I made the mismatch message list what each side has and the other does not:
Fatal: CoffFile backends disagree in CoffFile::LoadDebugSymbols on symbol count: cpp=53 rust=52
only in cpp:
0x62642300 size=5 "register_frame_ctor"
only in rust: -
That turned a bisect into a two-minute read, and it is the single best change I made to the machinery all week.
register_frame_ctor is a static function. I had implemented the symbol filter from COFFSymbolRef::isFunctionDefinition, which requires the symbol to be external:
// llvm/Object/COFF.h
bool isFunctionDefinition() const {
return isExternal() && getBaseType() == IMAGE_SYM_TYPE_NULL &&
getComplexType() == IMAGE_SYM_DTYPE_FUNCTION &&
!isReservedSectionNumber(getSectionNumber());
}
But CoffFileImpl does not call that. It calls getSymbolType(), and COFFObjectFile::getSymbolType returns ST_Function on the complex type alone. Two plausible-looking predicates for "is this a function", in the same LLVM header, and I picked the one that is not in use.
I did not misread the C++. I read it correctly, followed the call to a helper with an obviously right-sounding name, and implemented that. Nothing about the source would have told me; only running both against the same bytes did. This is the third time in four stages that a value-level comparison caught something a careful reading did not.
The part that stopped: PDB
The plan flagged PdbFileLlvm as the long pole a week ago, for the right reason — Orbit reads PDBs on Linux, for Wine and Proton modules, so it cannot simply be conditioned away. What it did not know was why it would be hard. Now it is measured rather than guessed.
The pdb crate reads Orbit's test file exactly:
What PdbFileTest asserts | What the pdb crate gives | |
|---|---|---|
| GUID | afd69a4f-7f39-4e50-88fc-34477bd0bae3 | exact |
| Age | 1 | exact |
| Procedure symbols | 5,459 | read |
Address 0x18000eea0 | section 1, offset 0xdea0 | exact |
Size 0x2b | 43 | exact |
Name "PrintHelloWorldInternal()" | "PrintHelloWorldInternal" | missing () |
Everything structural is there. What is missing is two characters — and those two characters are the whole problem.
// PdbFileLlvm.cpp
symbol_info.set_demangled_name(llvm::demangle(proc.Name.str()));
llvm::StringRef argument_list = RetrieveArgumentList(proc); // -> "()"
if (!argument_list.empty()) {
symbol_info.set_demangled_name(absl::StrCat(symbol_info.demangled_name(), argument_list.data()));
}
RetrieveArgumentList resolves the procedure's type in the TPI stream and calls LazyRandomTypeCollection::getTypeName on its argument list. That is LLVM's CodeView type-name renderer. For a no-argument function it produces (). For anything else it recursively renders the parameter types — pointers, references, const, class names, nested templates.
Reaching parity means reimplementing that renderer in Rust. It is a bounded, nameable project. It is also a project, and it has the same shape as the line-info problem that stopped stage 2d: two independent implementations rendering the same structure as different strings.
Why this one has no escape hatch
Post 02 established a rule: structure is compared strictly, rendering is counted and reported, because a demangled name is shown to a person and the port only promises to produce a demangling. That rule does not apply here, twice over:
PdbFileTestasserts the exact string"PrintHelloWorldInternal()". A test file is never edited during a port — that rule is what makes every other number in this log mean anything.- These names are used for function matching, not just display. Two overloads differ only by their argument lists, which is precisely why the C++ goes to the TPI stream for them in the first place.
So LLVM stays, and here is exactly which parts
//bazel/deps:llvm or it does not; four of six libraries having Rust replacements buys nothing while the other two are reachable. This is the honest status of the goal the whole port was aimed at, six stages in.Where things stand
| Measure | Baseline | Now | Reading |
|---|---|---|---|
| Structural symbol agreement | — | 16,312,372 | 4,709 ELF and 7 PE binaries, zero mismatches |
CoffFile methods delegating | all | 0 | Fully ported |
ElfFile methods delegating | all | 3 | All line info, held back in post 03 |
| Rust unit tests | 0 | 61 | Against the C++ suite's own testdata |
| External Rust crates | 0 | 11 | object, gimli, addr2line, flate2 and their trees |
Files including an llvm/ header | 11 | 9 | Unchanged since post 02, and it will stay 9 |
//bazel/deps:llvm dependents | 1 | 1 | The goal, unmoved. Blocked on the CodeView renderer |
The honest summary, six stages in
Two thirds of ObjectUtils now runs on Rust and is verified against sixteen million symbols from every binary on the machine. Along the way the port found and fixed six real divergences, three of which would have shipped silently, and produced a set of measurements nobody had before.
It has not achieved the thing it was for. //bazel/deps:llvm still has one dependent, and the dependency count is still higher than when this started. Both of those stay true until either someone writes a CodeView type-name renderer, or the team decides that PdbFileLlvm can be conditioned off on Linux — which is a product question about Wine and Proton support, not an engineering one, and not mine to answer.
What the log can say is that the question is now specific. "Port the backend to Rust" was a mood; "reimplement LazyRandomTypeCollection::getTypeName for LF_ARGLIST" is a ticket.