ObjectUtils is the module the whole port is aimed at, because //bazel/deps:llvm has exactly one dependent and this is it. Two stages landed: the ELF metadata that ElfFileImpl::Initialize computes, and the two symbol-table loaders. Twelve of the seventeen ElfFile methods now come from Rust; five still forward to a C++ instance the Rust one owns.
A prerequisite nobody would have guessed
Before any Rust could go behind ElfFile, the interface had to stop naming LLVM. include/ObjectUtils/ElfFile.h included llvm/Object/ObjectFile.h, and CreateElfFile had an overload taking an llvm::object::OwningBinary. An interface that names llvm types cannot have a non-LLVM implementation behind it, and every one of ObjectUtils's sixteen dependents was seeing those headers.
This turned out to be free. grep for callers of the LLVM-typed overload found none outside src/ObjectUtils — every external caller uses the path-only form. So both overloads moved to an internal ObjectFileLlvm.h and the public headers lost their LLVM includes. Files in the tree that include an llvm/ header went from 11 to 9, with no behaviour change and no test touched.
This is a cleanup that stands on its own merits and had been sitting there for years. It is the kind of thing a port surfaces not because Rust is better but because the exercise forces you to ask what an interface actually promises.
The easy half: metadata
Sections, dynamic entries and program headers, through the object crate. Twenty Rust tests, written against the same testdata files the C++ suite uses, asserting the same values ElfFileTest asserts — load bias, executable segment offset, image size, all four object segments of both a dynamic and a static binary, the build id, the soname, and that the hand-written CRC-32 matches llvm::crc32 on hello_world_elf.debug.
All twenty passed on the first run. That is not a claim about my code; it is a claim about the object crate, which reproduces llvm::object's answers on Orbit's own testdata without adjustment. Its entire transitive dependency tree is one crate, memchr.
The speed was the surprise. I expected parity and was ready to report a regression:
| File | Size | llvm::object | object | |
|---|---|---|---|---|
hello_world_elf | 16 KB | 10.5 µs | 0.8 µs | 13.3× |
hello_world_static_elf | 765 KB | 11.4 µs | 0.6 µs | 19.9× |
hello_world_elf_with_debug_info | 17 KB | 12.7 µs | 0.5 µs | 24.6× |
libtest-1.0.so | 16 KB | 13.3 µs | 2.1 µs | 6.5× |
libc.debug | 4.2 MB | 33.6 µs | 0.7 µs | 50.0× |
The speedup grows with file size, which tells you the mechanism: llvm::object::createObjectFile does its work up front, and the object crate is lazy. On a 4 MB file LLVM pays for structure Orbit never asks about.
It does not mean ORBIT_OBJECT_BACKEND=rust is faster today. While methods still delegate, the Rust ElfFile constructs a C++ one too, so the rust backend is necessarily slower end to end. The table times the two parsers directly, which is the only honest comparison until the delegate is gone. That is what the delegation counter in the metrics is for.
The hard half: three demanglers
Symbol tables looked like more of the same — read .symtab or .dynsym, keep defined STT_FUNC entries, record address, size and name. The reading was straightforward. The naming took three attempts, and ORBIT_OBJECT_BACKEND=both found each failure within seconds of the code compiling.
cpp_demangle — the crate symbolic and most of the Rust ecosystem use — drops the char* const& parameter. That is not a formatting difference; it is a wrong answer, in both 0.4 and 0.5. Two Rust dependencies were added and then removed again.Then the corpus said parity was impossible
With __cxa_demangle plus a small spacing normaliser — it emits the pre-C++11 char_traits<char> > and operator<< < where LLVM emits the modern forms — all 68 ObjectUtilsTests cases passed in all three modes.
Then I ran the same comparison over 4,719 real binaries, and it aborted on the second file. Not on structure: on rendering.
mangled _ZSt14__copy_move_a1ILb1EPN4absl7debian912crc_internal12CrcCordState9PrefixCrcE...
llvm _ZSt14__copy_move_a1ILb1EPN4absl7debian912crc_internal12CrcCordState9PrefixCrcE...
libstdc++ __gnu_cxx::__enable_if<std::__is_random_access_iter<absl::debian9::...>>::__type
std::__copy_move_a1<true, ...>(...)
Here LLVM is the one that gave up and returned the symbol unchanged. Elsewhere they disagree on lambda notation — LLVM writes 'lambda'(), libstdc++ writes {lambda()#1} — and on whether to print a return type. None of that is reachable by normalisation, and demanding bug-compatibility would mean reproducing LLVM's limitations on purpose.
So the contract got split
This is the judgement call in the whole stage, and it is worth stating plainly rather than burying:
- Structure is strict. Address, size, hotpatchability, which symbols are present, and in what order. Any mismatch aborts the process with both values printed.
- Rendering is reported. The demangled name is a rendering of the mangled name. The port commits to producing a demangling of every symbol, not to reproducing LLVM's exact text. Differences are counted and logged.
Over the corpus:
elf files seen 4719
loaded 4709
symbols compared 944291
structural disagreements 0
rendering differences 2197 (0.2327%)
Zero structural disagreements across nearly a million symbols in /usr/lib/x86_64-linux-gnu and /usr/bin. The curated testdata is 34 files; it would never have found the demangler problems, and it would never have given that number either.
Hurdles
The five-byte padding
After the demangling matched, both mode failed on is_hotpatchable for __cxx_global_var_init. I had read LoadHotpatchableAddresses, which parses __patchable_function_entries, and implemented it faithfully — then missed that the caller does not look up the symbol's address at all. IsHotpatchable looks up address - 5, because the recorded addresses point at the first byte of the padding rather than the function entry.
Two lines of C++, in a different function from the one I was porting. Nothing but a value-level comparison would have caught it: the types are identical and both answers are plausible booleans.
Off by one in the normaliser
"operator<< <" should become "operator<<<". I erased index 11 instead of 10, deleting the < rather than the space, and produced operator<< char. The comparison named the exact symbol and printed both strings, so it took one test run to see.
The mainstream crate has a correctness bug
cpp_demangle is what symbolic-demangle wraps and what most of the Rust ecosystem reaches for. On a constructor template whose parameter is a reference to a template parameter it silently drops the parameter, in both 0.4.5 and 0.5.1.
The useful part is not that a crate has a bug; it is how it was found. A dozen curated test binaries did not contain such a symbol. The C++ standard library, linked into a test file that had been in the tree for years, did.
"Port it to Rust" was the wrong answer for one component
The demangler moved back to C++, and that is the right outcome. The goal is not maximal Rust; it is removing LLVM. abi::__cxa_demangle costs nothing — libstdc++ is already linked — and it is more correct than the Rust alternative. Two Rust dependencies were added and then removed.
A port that cannot reach this conclusion for any component is not evaluating, it is converting.
The header split, again
Same cycle as ModuleUtils in post 01, at larger scale: the Rust shim implements ElfFile and therefore needs ObjectUtils's headers, while ObjectUtils needs the shim to dispatch to it. A headers-only ObjectUtilsHeaders target breaks it. Predicted in the plan, which is mildly satisfying.
The evidence, including what got worse
| Measure | Baseline | Now | Reading |
|---|---|---|---|
| Structural symbol agreement | — | 944,291 / 944,291 | Across 4,709 real binaries |
| ELF metadata parse, 4 MB file | 33.6 µs | 0.7 µs | Parsers compared directly, not end to end |
Files including an llvm/ header | 11 | 9 | The two public ObjectUtils headers |
ElfFile methods on Rust | 0 | 12 of 17 | Five still delegate: unwind ranges and the three line-info methods |
| New external Rust crates | — | 2 | object and memchr. cpp_demangle and rustc-demangle were added and removed |
deps(//src/ObjectUtils) | 5,843 | 8,362 | Worse by 2,519. rules_rust and crate_universe are in the graph now |
ObjectUtils implementation lines | 3,680 | 3,886 | Worse by 206. Both implementations, by design |
The dependency count is worse than it was after post 01 and will stay worse until stage 2h. Nothing about that is surprising — the plan says the payoff is deleting //bazel/deps:llvm, and it still has one dependent — but a log that only reported the good half would not be worth reading.
Next
Stage 2c is the unwind ranges: walking .eh_frame and .debug_frame FDEs with gimli to synthesise symbols for functions that have none. Then 2d, DWARF line info, which is the last thing standing between here and deleting llvm::DebugInfoDWARF.
The delegation counter is at five. It has never gone up.