Skip to content

windows-gnullvm: always link libunwind statically - #160712

Open
mati865 wants to merge 4 commits into
rust-lang:mainfrom
mati865:gnullvm-static-libunwind
Open

windows-gnullvm: always link libunwind statically#160712
mati865 wants to merge 4 commits into
rust-lang:mainfrom
mati865:gnullvm-static-libunwind

Conversation

@mati865

@mati865 mati865 commented Aug 7, 2026

Copy link
Copy Markdown
Member

View all comments

  • I did not use an LLM to create a change in this PR.
  • I used an LLM to create a change in this PR, and I have explained below how it was used.

Previously shared library was used by default, meaning that programs and libraries couldn't be loaded if libunwind.dll was missing from the PATH. Using Wine (on Linux) because it better shows the problem (and is more convenient):

❯ cargo new hello &> /dev/null

❯ cargo rustc --target x86_64-pc-windows-gnullvm &> /dev/null

❯ wine target/x86_64-pc-windows-gnullvm/debug/hello.exe
0024:err:module:import_dll Library libunwind.dll (which is needed by L"Z:\\tmp\\hello\\target\\x86_64-pc-windows-gnullvm\\debug\\hello.exe") not found
0024:err:module:loader_init Importing dlls for L"Z:\\tmp\\hello\\target\\x86_64-pc-windows-gnullvm\\debug\\hello.exe" failed, status c0000135

❯ llvm-readobj --coff-imports target/x86_64-pc-windows-gnullvm/debug/hello.exe
...
Import {
  Name: libunwind.dll
  ImportLookupTableRVA: 0x3D308
  ImportAddressTableRVA: 0x3D678
  Symbol: _GCC_specific_handler (0)
  Symbol: _Unwind_DeleteException (0)
  Symbol: _Unwind_GetDataRelBase (0)
  Symbol: _Unwind_GetIPInfo (0)
  Symbol: _Unwind_GetLanguageSpecificData (0)
  Symbol: _Unwind_GetRegionStart (0)
  Symbol: _Unwind_GetTextRelBase (0)
  Symbol: _Unwind_RaiseException (0)
  Symbol: _Unwind_Resume (0)
  Symbol: _Unwind_SetGR (0)
  Symbol: _Unwind_SetIP (0)
}
...

Optionally libunwind could be linked statically via +crt-static:

❯ cargo rustc --target x86_64-pc-windows-gnullvm -- -C target-feature=+crt-static &> /dev/null

❯ wine target/x86_64-pc-windows-gnullvm/debug/hello.exe
Hello, world!

❯ llvm-readobj --coff-imports target/x86_64-pc-windows-gnullvm/debug/hello.exe | rg 'libunwind.dll' || echo "doesn't depend on shared libunwind"
doesn't depend on shared libunwind

After a discussion of approach in #159782 with @bjorn3 (thanks BTW!), I changed the proposed approach to always link static libunwind.

I don't have a good solution for #121794 that will resurface. I guess the user has three options:

  • symlink libunwind.dll.a as libunwind.a
  • add --unwindlib=none -lunwind to the linker args
  • create linker wrapper
  • use self-contained mode which is likely is undesirable

I think the ease of use (not having to deal with additional DLL dependency) outweights the benefit of working with incomplete C toolchain.

The size bloat is also not a problem, sizes (in bytes) of the binary for the literal hello world project:

  • debug build:
    • shared libunwind 4194816
    • static libunwind 4323328
  • release build:
    • shared libunwind 382464
    • static libunwind 423424

Debug diff +125.5 KiB, release diff: +40 KiB.
Size of libunwind.dll that has to be provided when linking shared libunwind: 204288 bytes (199.5 KiB).

@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. T-bootstrap Relevant to the bootstrap subteam: Rust's build system (x.py and src/bootstrap) T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Aug 7, 2026
@mati865

This comment was marked as outdated.

@rust-bors

This comment was marked as outdated.

@mati865

This comment was marked as outdated.

@rust-bors

This comment has been minimized.

rust-bors Bot pushed a commit that referenced this pull request Aug 7, 2026
windows-gnullvm: always link libunwind statically


try-job: *gnullvm
@rust-bors

This comment was marked as outdated.

@rust-log-analyzer

This comment was marked as outdated.

@mati865

mati865 commented Aug 7, 2026

Copy link
Copy Markdown
Member Author

@bors try jobs=*llvm-mingw

@rust-bors

This comment has been minimized.

rust-bors Bot pushed a commit that referenced this pull request Aug 7, 2026
windows-gnullvm: always link libunwind statically


try-job: *llvm-mingw
@rust-bors

rust-bors Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

☀️ Try build successful (CI)
Build commit: bd01f2e (bd01f2e7c0afdde70b5a1f7027b6e1c37637bdd1)
Base parent: 65bcac4 (65bcac45b3d8a8b2126e5cc844cf6fff5795d32a)

@rust-bors

This comment has been minimized.

@mati865
mati865 force-pushed the gnullvm-static-libunwind branch from 501c282 to aa3688d Compare August 14, 2026 16:06
@rust-log-analyzer

This comment has been minimized.

@mati865
mati865 force-pushed the gnullvm-static-libunwind branch from aa3688d to e2eb974 Compare August 14, 2026 16:15
@mati865
mati865 marked this pull request as ready for review August 19, 2026 16:13
@rustbot

rustbot commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Some changes occurred in src/doc/rustc/src/platform-support

cc @Noratrieb

@rustbot rustbot added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. label Aug 19, 2026
@rustbot rustbot removed the S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. label Aug 19, 2026
@rustbot

rustbot commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

r? @petrochenkov

rustbot has assigned @petrochenkov.
They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use r? to explicitly pick a reviewer

Why was this reviewer chosen?

The reviewer was selected based on:

  • Owners of files modified in this PR: compiler
  • compiler expanded to 75 candidates
  • Random selection from 15 candidates

@mati865

mati865 commented Aug 19, 2026

Copy link
Copy Markdown
Member Author

I'm not sure what kind of approval this change will require (r+, FCP, MCP+second?).

@petrochenkov

Copy link
Copy Markdown
Contributor

After a discussion of approach in #159782 with @bjorn3 (thanks BTW!)

Where is the discussion? #159782 doesn't have it.

@petrochenkov

petrochenkov commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Is it correct to link multiple copies of libunwind.a together when you link a dylib (with one copy of libunwind) into an executable (another copy of libunwind)?

For non-gnullvm windows-gnu targets it's apparently not ok (unwinding across DLL boundaries breaks), that's why we have flags like late_link_args_dynamic and late_link_args_static, and -lgcc_s is used if there's any dynamic library in the graph, and -lgcc_eh is used otherwise. cc #71651 (comment)

@petrochenkov petrochenkov added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Sep 4, 2026
@bjorn3

bjorn3 commented Sep 4, 2026

Copy link
Copy Markdown
Member

After a discussion of approach in #159782 with @bjorn3 (thanks BTW!)

Where is the discussion? #159782 doesn't have it.

That was in a zulip DM.

Is it correct to link multiple copies of libunwind.a together when you link a dylib (with one copy of libunwind) into an executable (another copy of libunwind)?

According to @mati865 for as long as 32bit libgcc_s isn't involved as unwinder, multiple unwinder copies will interact just fine with each other. In the case of -gnullvm, we already always use libunwind rather than libgcc_s as unwinder.

@petrochenkov

Copy link
Copy Markdown
Contributor

That was in a zulip DM.

Then my question is what are the downsides of supporting the static vs dynamic choice under an option like in #159782.

@bjorn3

bjorn3 commented Sep 4, 2026

Copy link
Copy Markdown
Member

Unlike on -gnu there is no breakage that would occur from not supporting it. It only increases the size of dylibs by 40KiB by adding a copy of libunwind, but in doing so makes distribution of the dylibs easier by not having to ship libunwind.dll as user.

@mati865

mati865 commented Sep 4, 2026

Copy link
Copy Markdown
Member Author

Is it correct to link multiple copies of libunwind.a together when you link a dylib (with one copy of libunwind) into an executable (another copy of libunwind)?

For non-gnullvm windows-gnu targets it's apparently not ok (unwinding across DLL boundaries breaks), that's why we have flags like late_link_args_dynamic and late_link_args_static, and -lgcc_s is used if there's any dynamic library in the graph, and -lgcc_eh is used otherwise. cc #71651 (comment)

On Windows with 32-bit libgcc built with Dwarf-2 exception model? Not at all.
I'm not entirely sure about 32-bit libgcc built with SjLj exception model, but 64-bit libgcc (SEH) and any version of libunwind will be fine*.

[*] IIRC, on 32-bits libunwind will break if specific libgcc_{eh,s} symbols are present in the binary.

Then my question is what are the downsides of supporting the static vs dynamic choice under an option like in #159782.

Unlike on -gnu there is no breakage that would occur from not supporting it. It only increases the size of dylibs by 40KiB by adding a copy of libunwind, but in doing so makes distribution of the dylibs easier by not having to ship libunwind.dll as user.

Just like Bjorn wrote, but remember that they no longer need to ship libunwind.dll that is ~100 KiB, so this actually reduces distribution size for the most popular cases.


Decided to double-check if memory serves me right to avoid potentially misleading everyone. Heck, libgcc is even worse than I remembered (or it has regressed).

32-bit GCC preparation:
❯ cat dll.cc
#include <stdexcept>

extern "C" __declspec(dllexport) void foo() { throw std::runtime_error("foo"); }

❯ cat main.cc
#include <iostream>
#include <stdexcept>

extern "C" __declspec(dllimport) void foo();

int main() {
  std::cout << "entered main" << std::endl;
  try {
    foo();
  } catch (const std::runtime_error &e) {
    std::cout << "caught: " << e.what() << std::endl;
    return 0;
  }
  std::cout << "no exception" << std::endl;
  return 1;
}

❯ PATH=/mingw32/bin:$PATH i686-w64-mingw32-g++ -shared -o dll_shared_libgcc.dll dll.cc -Wl,--out-implib=libdll_shared_libgcc.dll.a

❯ PATH=/mingw32/bin:$PATH i686-w64-mingw32-g++ -shared -static-libgcc -static-libstdc++ -o dll_static_libgcc.dll dll.cc -Wl,--out-implib=libdll_static_libgcc.dll.a

❯ PATH=/mingw32/bin:$PATH i686-w64-mingw32-g++ main.cc -o shared_main_static_dll.exe -L. -ldll_static_libgcc

❯ PATH=/mingw32/bin:$PATH i686-w64-mingw32-g++ main.cc -o shared_main_shared_dll.exe -L. -ldll_shared_libgcc

❯ PATH=/mingw32/bin:$PATH i686-w64-mingw32-g++ main.cc -static-libgcc -static-libstdc++ -o static_main_shared_dll.exe -L. -ldll_shared_libgcc

❯ PATH=/mingw32/bin:$PATH i686-w64-mingw32-g++ main.cc -static-libgcc -static-libstdc++ -o static_main_static_dll.exe -L. -ldll_static_libgcc

Runing libgcc case:

❯ PATH=/mingw32/bin:$PATH ./static_main_shared_dll; echo exit code: $status
entered main
exit code: 3

❯ PATH=/mingw32/bin:$PATH ./static_main_shared_dll; echo exit code: $status
entered main
exit code: 3

❯ PATH=/mingw32/bin:$PATH ./static_main_static_dll; echo exit code: $status
entered main
terminate called after throwing an instance of 'std::runtime_error'
  what():  foo
exit code: 3

❯ PATH=/mingw32/bin:$PATH ./shared_main_shared_dll; echo exit code: $status
entered main
caught: foo
exit code: 0

Don’t even get me started on what happened here, instead I'll move to something sane.
I didn't bother changing library names but it's using libunwind, not libgcc.

32-bit Clang preparation
❯ PATH=/opt/llvm-mingw-20251216-ucrt-x86_64/bin:$PATH i686-w64-mingw32-clang++ -shared -o dll_shared_libgcc.dll dll.cc -Wl,--out-implib=libdll_shared_libgcc.dll.a

❯ PATH=/opt/llvm-mingw-20251216-ucrt-x86_64/bin:$PATH i686-w64-mingw32-clang++ -shared -static-libgcc -static-libstdc++ -o dll_static_libgcc.dll dll.cc -Wl,--out-implib=libdll_static_libgcc.dll.a

❯ PATH=/opt/llvm-mingw-20251216-ucrt-x86_64/bin:$PATH i686-w64-mingw32-clang++ main.cc -o shared_main_static_dll.exe -L. -ldll_static_libgcc

❯ PATH=/opt/llvm-mingw-20251216-ucrt-x86_64/bin:$PATH i686-w64-mingw32-clang++ main.cc -o shared_main_shared_dll.exe -L. -ldll_shared_libgcc

❯ PATH=/opt/llvm-mingw-20251216-ucrt-x86_64/bin:$PATH i686-w64-mingw32-clang++ main.cc -static-libgcc -static-libstdc++ -o static_main_shared_dll.exe -L. -ldll_shared_libgcc

❯ PATH=/opt/llvm-mingw-20251216-ucrt-x86_64/bin:$PATH i686-w64-mingw32-clang++ main.cc -static-libgcc -static-libstdc++ -o static_main_static_dll.exe -L. -ldll_static_libgcc

Running libunwind case:

❯ PATH=/opt/llvm-mingw-20251216-ucrt-x86_64/i686-w64-mingw32/bin:$PATH ./static_main_shared_dll; echo exit code: $status
entered main
caught: foo
exit code: 0

❯ PATH=/opt/llvm-mingw-20251216-ucrt-x86_64/i686-w64-mingw32/bin:$PATH ./static_main_static_dll; echo exit code: $status
entered main
caught: foo
exit code: 0

❯ PATH=/opt/llvm-mingw-20251216-ucrt-x86_64/i686-w64-mingw32/bin:$PATH ./shared_main_shared_dll; echo exit code: $status
entered main
caught: foo
exit code: 0

❯ PATH=/opt/llvm-mingw-20251216-ucrt-x86_64/i686-w64-mingw32/bin:$PATH ./shared_main_static_dll; echo exit code: $status
entered main
caught: foo
exit code: 0

@mati865 mati865 added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. and removed S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. labels Sep 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-bootstrap Relevant to the bootstrap subteam: Rust's build system (x.py and src/bootstrap) T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants