<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><link rel="self" href="https://dvratil.cz/category/rust//feed.xml" type="application/atom+xml"/><link rel="alternate" href="https://dvratil.cz/category/rust/" tyle="text/html"/><id>https://dvratil.cz/category/rust/</id><updated>2025-12-16T23:00:00+0100</updated><title>Daniel Vrátil's blog Rust category feed</title><subtitle>Because opensource matters.</subtitle><author><name>Daniel Vrátil</name><email>me@dvratil.cz</email><uri>https://dvratil.cz/</uri></author><entry><title>Rust: linking static C/C++ libraries with LTO</title><link rel="alternate" href="https://dvratil.cz/2025/12/rust-linking-static-c-c-libraries-with-lto/"/><id>https://dvratil.cz/2025/12/rust-linking-static-c-c-libraries-with-lto/</id><published>2025-12-16T23:00:00+0100</published><updated>2025-12-16T23:00:00+0100</updated><category term="Rust"/><summary>&lt;h2 id="the-backstory"&gt;The Backstory&lt;/h2&gt;
&lt;p&gt;I wasted entire afternoon today trying to figure this out, so here&amp;rsquo;s a quick note for future reference - hopefully to save someone else&amp;rsquo;s afternoon.&lt;/p&gt;
&lt;p&gt;Today, I was working on adding Rust bindings for an internal C++ library. The C++ library itself is built using CMake and produces a statically-linked library, let&amp;rsquo;s call it &lt;em&gt;libfoo.a&lt;/em&gt;.
I used the &lt;a href="https://cxx.rs/"&gt;&lt;code&gt;cxx&lt;/code&gt;&lt;/a&gt; crate to generate the glue code between Rust and the C++ library, wrote the higher-level Rust API and ran &lt;code&gt;cargo build&lt;/code&gt;. The crate built just fine, so I moved on to integrating it into our larger Rust project. However, when I tried to build the project, I got a linker error:&lt;/p&gt;</summary><content type="html" xml:base="https://dvratil.cz/2025/12/rust-linking-static-c-c-libraries-with-lto/">&lt;h2 id="the-backstory"&gt;The Backstory&lt;/h2&gt;
&lt;p&gt;I wasted entire afternoon today trying to figure this out, so here&amp;rsquo;s a quick note for future reference - hopefully to save someone else&amp;rsquo;s afternoon.&lt;/p&gt;
&lt;p&gt;Today, I was working on adding Rust bindings for an internal C++ library. The C++ library itself is built using CMake and produces a statically-linked library, let&amp;rsquo;s call it &lt;em&gt;libfoo.a&lt;/em&gt;.
I used the &lt;a href="https://cxx.rs/"&gt;&lt;code&gt;cxx&lt;/code&gt;&lt;/a&gt; crate to generate the glue code between Rust and the C++ library, wrote the higher-level Rust API and ran &lt;code&gt;cargo build&lt;/code&gt;. The crate built just fine, so I moved on to integrating it into our larger Rust project. However, when I tried to build the project, I got a linker error:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;note: rust-lld: error: undefined symbol: url_to_json[abi:cxx11](std::basic_string_view&amp;lt;char, std::char_traits&amp;lt;char&amp;gt;&amp;gt; const&amp;amp;)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I double-checked the &lt;code&gt;build.rs&lt;/code&gt; script of the bindings crate to ensure that &lt;em&gt;libfoo.a&lt;/em&gt; was being linked. It was. I then used &lt;code&gt;nm&lt;/code&gt; to confirm that the symbol actually exists in &lt;em&gt;libfoo.a&lt;/em&gt;. It did. So what now? I spent a lot of time googling around, trying different &amp;ldquo;hacks&amp;rdquo; in the build.rs script and other sorcery. I had a temporary success by adding complex &lt;code&gt;build.rs&lt;/code&gt; into the consumer project, but that wasn&amp;rsquo;t really a scalable solution.&lt;/p&gt;
&lt;p&gt;The most confusing part was that I used the exact same approach and code to create bindings for another our C++ library a while ago, and there it all just worked. No special linker flags, no magical cargo incantations, no &lt;code&gt;build.rs&lt;/code&gt; in projects that used the bindings crate. What was different this time?&lt;/p&gt;
&lt;h2 id="a-suspect-is-identified"&gt;A Suspect is Identified&lt;/h2&gt;
&lt;p&gt;After literally hours of trial and error and out of desperation, I decided to &lt;code&gt;objdump&lt;/code&gt; the &lt;em&gt;libfoo.a&lt;/em&gt; to look at the disassembly of the problematic &lt;code&gt;url_to_json&lt;/code&gt; function. I am far from an assembly expert, but the disassembled output looked very suspicious, even to my untrained eye.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;0000000000000000 &amp;lt;.gnu.lto__Z11url_to_jsonB5cxx11RKSt17basic_string_viewIcSt11char_traitsIcEE.1350.d0d92fceb60052fc&amp;gt;:
0: 28 b5 2f fd 60 72 sub %dh,0x7260fd2f(%rbp)
6: 02 25 12 00 b6 a0 add -0x5f49ffee(%rip),%ah
c: 71 46 jno 54 &amp;lt;.gnu.lto__Z11url_to_jsonB5cxx11RKSt17basic_string_viewIcSt11char_traitsIcEE.1350.d0d92fceb60052fc+0x54&amp;gt;
e: e0 d0 loopne ffffffffffffffe0
10: 36 1d f8 c7 f0 69 ss sbb $0x69f0c7f8,%eax
16: 6c insb (%dx),%es:(%rdi)
17: 81 05 c4 c0 e0 96 61 addl $0x149c4e61,-0x691f3f3c(%rip)
1e: 4e 9c 14
21: a0 ad c0 3f f4 3e de movabs 0x102dde3ef43fc0ad,%al
28: 2d 10
2a: a4 movsb %ds:(%rsi),%es:(%rdi)
2b: 61 (bad)
(truncated)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Those instructions don&amp;rsquo;t look sensible at all, especially when compared to what the actual C++ code looks like. And what about the &amp;ldquo;bad&amp;rdquo; instruction? It&amp;rsquo;s not like I haven&amp;rsquo;t run into miscompilations before, but this was much more than that.&lt;/p&gt;
&lt;p&gt;At this point, I consulted the situation with an AI, and the answer was clear:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;When working with static libraries and Clang, issues with assembly output from tools like objdump can arise, particularly when Link-Time Optimization (LTO) is enabled.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Wait! Did it say LTO? The static library is definitely built with LTO (it&amp;rsquo;s even in the name of the symbol). Quick check into the library&amp;rsquo;s CMakeLists.txt and bingo: LTO is enabled by default. I disabled it and&amp;hellip;the consumer project built and linked without a single error. Problem solved.&lt;/p&gt;
&lt;h2 id="butwhy"&gt;But&amp;hellip;why?&lt;/h2&gt;
&lt;p&gt;My (shallow) understanding of LTO has always been that it&amp;rsquo;s just a special optimizer pass at link time, when the linker can see the final executable (or shared library) as a whole, and can perform optimizations across translation units and more efficiently eliminate unused code. A static library is just a collection of object files, so LTO should not really play any role here, right?&lt;/p&gt;
&lt;p&gt;The reality is that with LTO enabled, compilers &amp;ldquo;cheat&amp;rdquo; (yes, compiler&lt;strong&gt;s&lt;/strong&gt; - GCC does this as well), and instead of producing object files with the final machine code, they produce object-like files that contain the intermediate representation (IR) of the code. IR (GCC calls it GIR) is a compiler-specific representation of the code during the compilation process. It&amp;rsquo;s no longer the original source code, but it&amp;rsquo;s not the final machine code either. Having access to the IR allows the linker to perform advanced optimizations that wouldn&amp;rsquo;t be possible if it only had access to the final machine code. The final codegen that emits the executable machine code happens after the optimization pass.&lt;/p&gt;
&lt;p&gt;This entire process is actually described quite nicely in the &lt;a href="https://llvm.org/docs/LinkTimeOptimization.html"&gt;LLVM documentation about LTO&lt;/a&gt; - which is an information that is useful only when you know that you need it :-)&lt;/p&gt;
&lt;p&gt;However, if the &lt;em&gt;libfoo.a&lt;/em&gt; contained LLVM IR, and rustc also uses LLVM (and supports LTO on its own), why didn&amp;rsquo;t it Just Work™? The problem is that Rust will only perform LTO between Rust crates by default.&lt;/p&gt;
&lt;h2 id="cross-language-lto-with-cxx"&gt;Cross-language LTO with &lt;code&gt;cxx&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Luckily, it&amp;rsquo;s possible to coerce the Rust compiler to perform cross-language LTO, but if you are using the &lt;code&gt;cxx&lt;/code&gt; crate, there&amp;rsquo;s an extra step involved.&lt;/p&gt;
&lt;p&gt;This is what ultimately worked for me, and allowed me to build both without LTO in debug mode and with LTO in release mode.&lt;/p&gt;
&lt;p&gt;In your &lt;code&gt;build.rs&lt;/code&gt;, you must make sure that the generated bridge code is also compiled with LTO enabled (when needed):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; enable_lto &lt;span style="color:#f92672"&gt;=&lt;/span&gt; std::env::var(&lt;span style="color:#e6db74"&gt;&amp;#34;PROFILE&amp;#34;&lt;/span&gt;).unwrap_or_default() &lt;span style="color:#f92672"&gt;==&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;release&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// Compile the C++ library
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cmake::Config::new(&lt;span style="color:#e6db74"&gt;&amp;#34;.&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .define(&lt;span style="color:#e6db74"&gt;&amp;#34;ENABLE_LTO&amp;#34;&lt;/span&gt;, &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; enable_lto { &lt;span style="color:#e6db74"&gt;&amp;#34;ON&amp;#34;&lt;/span&gt; } &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; { &lt;span style="color:#e6db74"&gt;&amp;#34;OFF&amp;#34;&lt;/span&gt; })
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .build_target(&lt;span style="color:#e6db74"&gt;&amp;#34;all&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// Build the cxx bridge
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;mut&lt;/span&gt; bridge &lt;span style="color:#f92672"&gt;=&lt;/span&gt; cxx_build::bridge(&lt;span style="color:#e6db74"&gt;&amp;#34;src/bridge.rs&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .file(&lt;span style="color:#e6db74"&gt;&amp;#34;src/bridge.cpp&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// ... additional includes paths, source files, flags, etc.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; enable_lto {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// Enable LTO for the bridge compilation as well
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; bridge.flag_if_supported(&lt;span style="color:#e6db74"&gt;&amp;#34;-flto&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;bridge.compile(&lt;span style="color:#e6db74"&gt;&amp;#34;foo_bridge&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And to compile it. Just make sure you use Clang for building the C++ code as well (the &lt;code&gt;cc&lt;/code&gt; crate inside &lt;code&gt;cxx&lt;/code&gt; should pick this up automatically from the environment variables):&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;# Make sure to use clang!
export CXX=clang++
export CC=clang
# Enable LTO linker plugin
export RUSTFLAGS=&amp;#34;-Clinker-plugin-lto&amp;#34;
# Let&amp;#39;s gooooo
cargo build --release
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;An important requirement is that the C/C++ code must be compiled with a version of Clang that is compatible with the LLVM version used by rustc. The compatibility matrix is &lt;a href="https://doc.rust-lang.org/rustc/linker-plugin-lto.html#toolchain-compatibility"&gt;documented in the rustc book&lt;/a&gt;. In my case, I was using rustc 1.90 (LLVM 20.1.8) and Clang 20.1.8.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;If you ever run into &lt;code&gt;undefined reference&lt;/code&gt; linker errors when trying to link static C or C++ libraries with your Rust code, double check whether the static library was built with LTO enabled, and make sure to enable cross-language LTO in your Rust build as well.&lt;/p&gt;</content></entry></feed>