Cross-language LTO with rustc and clang-cl
LLVM docs describe clang-cl
as “a driver program for clang that attempts to be compatible with MSVC’s cl.exe”. It is a binary you can use to compile C/C++ code that has the same commandline interface as MSVC’s cl.exe . Why do I mention this? A couple months ago, I found myself with a very big rust binary, one which linked with OpenCV, a C++ library which I was only using to find a smaller image inside a bigger image, so I figured “Hey, I’ve heard of this really cool thing called cross-language LTO, which Firefox uses, that lets the compiler perform LTO across the language barrier. Maybe that will help trim down the size?” since I thought the library was way bigger than I needed. That thought led me down a week-long witch hunt to get that working, only to find that it did not help the size in any significant way. After that, I tried making a small library in pure rust to handle the one task I was using OpenCV for. That was a lot of fun, but it did not help either. I had tried using cargo bloat but it did not support PDBs, and eventually I sent a patch to help with that, and using that I found where the size was coming from: I was including a few raw BMP files in my binary via include_bytes!("/path/")
🤦♂️.
But lets forget about all that for a bit, and focus on the topic of this blog post: Getting cross language LTO between rustc and clang-cl to work. Even if it did not help me solve anything, it might be the right tool for someone else. For the short version, you can look at the rustc docs on it which I added after my little misadventure.
So, LTO. It stands for “Link Time Optimization”. What that means, at least for clang, and as far as I know also MSVC, is that instead of having the compiler generate optimized machine code object files for each individual unit of compilation, it generates a sort of “intermediate” representation which is not machine code and not fully optimized, and only optimizes it and generates the final machine code once the code has reached the linker, in the final step. Why? Because that way, the linker can optimize across different “units of compilation”. That means, for example, inlining. If you want to read a better and more detailed explanation, while I was working on this I found this great LLVM blog post about cross language LTO with rust from 2019.
I mentioned LTO uses an “intermediate representation”, and this is important because this is different across compilers. I’m not sure but I’d expect it can also change between compiler versions (citation needed). And I usually use the “x86_64-pc-windows-msvc” rust target, which means that when a crate attempts to compile C code using the cc crate at build time, it’s going to try to use MSVC (cl.exe) and MSVC LTO is incompatible with LLVM LTO. The rust compiler uses LLVM, and that’s why you can get cross language LTO when using clang and rustc. I knew about the existence of clang-cl, and I knew that firefox had shipped cross language LTO in 2019, so I expected cross language LTO for rust/C++ on windows for the MSVC target to be a well-trodden path. It wasn’t. I wasn’t able to find much, if any, information on how to do it for my specific target. After spending a couple days banging my head against the keyboard, I made a separate hello world rust binary crate that compiled a single C file using the cc crate:
My objective was to get the C symbol
function inlined as a 3 in the rust main
.
The 2 sources of information I used to get this working were
https://blog.llvm.org/2019/09/closing-gap-cross-language-lto-between.html
https://doc.rust-lang.org/rustc/linker-plugin-lto.html
problems: –target, host macros? Setting /clang:-Wno-everything in CFLAGS resulted in linking errors. I believe I looked into it and it was because of cmake files that parsed compiler output for warnings to detect compiler features in opencv. Linking with rust LTO and without linker-plugin-lto worked fine but linking with it was giving linking errors. I believe this was because the rust-only LTO was removing references to symbols before they reached the linker in the object files that rust generates, but when enabling cross language LTO with linker-plugin-lto rust emits all code as LLVM bitcode in the object files and references everything, so when it reaches the linker it results in an undefined symbol. I did not test it but I think removing the lto=”thin” configuration from my Cargo.toml profile would have reproduced the same linking error without linker-plugin-lto Setting RUSTFLAGS seems to override /.cargo/config , which makes sense but was a bit surprising Check out the Jekyll docs for more info on how to get the most out of Jekyll. File all bugs/feature requests at Jekyll’s GitHub repo. If you have questions, you can ask them on Jekyll Talk.
[unit-compilation]: I use this strange term because things are a bit complicated. In C/C++, you usually have one “Translation Unit” for each .c/.cpp file (Although you don’t have to! Some people prefer things like Unity Builds), but in rust you specify to rustc how many “codegen units” you want it to split a crate into.