Skip to content

LTO drops TLS initialization routines for thread-local variables, causing undefined symbols at link time #541

@mertcandav

Description

@mertcandav

Hello, I am the developer of the Jule programming language.
First of all, thank you for this project.

Clang is the recommended C++ backend compiler for Jule.
On Windows, llvm-mingw is the recommended toolchain.

I recently released Jule version 0.2.0. Until now, I had not encountered any issues with this toolchain. However, starting with this release, I began using thread-local variables. Their primary purpose is to manage coroutine state across worker threads and to store a pointer to the runtime object associated with the current thread.

I did not notice any problems until the release process, because during development I was using debug builds and therefore did not observe issues caused by optimizations. For builds, I use the compilation commands generated by julec (the official Jule compiler). When producing a production build, julec enables several optimization flags, and one of these clearly triggers the problem.

For Windows builds, I use a Windows 11 virtual machine running under VMware Fusion. The primary architecture is ARM64 (AArch64). For ARM64 builds, I use the default clang++. On the same virtual machine, I also build for Windows AMD64 (x86_64), using x86_64-w64-mingw32-clang++. The compilation command is otherwise identical.

These are the exact compilation commands I am using:

windows-arm64

clang++ -static -Wno-everything --std=c++20 -fwrapv -ffloat-store -fno-fast-math -fno-rounding-math -ffp-contract=fast -O3 -flto=thin -DNDEBUG -fomit-frame-pointer -fno-strict-aliasing -o ./bin/julec.exe ir.cpp -lws2_32 -lshell32 -liphlpapi -lsynchronization

windows-amd64

x86_64-w64-mingw32-clang++ -static -Wno-everything --std=c++20 -fwrapv -ffloat-store -fno-fast-math -fno-rounding-math -ffp-contract=fast -O3 -flto=thin -DNDEBUG -fomit-frame-pointer -fno-strict-aliasing -o ./bin/julec.exe ir.cpp -lws2_32 -lshell32 -liphlpapi -lsynchronization

Compiling with these commands consistently results in the following linker error:

ld.lld: error: undefined symbol: thread-local initialization routine for __jule_ct
>>> referenced by ir.cpp
>>>               C:/Users/mertc/AppData/Local/Temp/ir-e624c2.o
clang-22: error: linker command failed with exit code 1 (use -v to see invocation)

The symbol __jule_ct referenced in the error is a thread_local variable that holds a pointer to the thread's runtime object. Its type is __jule_Ptr, which is a smart pointer implementing reference counting. This type is not trivially copyable, trivially constructible, or trivially destructible.

I investigated the issue further, and it appears to be related to the interaction between the __jule_Ptr type and LTO. If I avoid using __jule_Ptr and instead use a fully trivial type (for example, a raw pointer), LTO does not cause any problems. However, when LTO is enabled, LTO appears to eliminate or fail to preserve the TLS initialization routine generated for the __jule_Ptr thread-local variable, which then results in the linker error shown above.

If -flto=thin is removed from the compilation commands above, the problem is resolved. Any use of LTO, whether thin or full, triggers this issue.

ir.cpp is a C++ IR file generated by julec. However, the issue is actually unrelated to this, the real problem is the __jule_ct variable defined on the Jule's C++ API side.

__jule_ct is defined here: api/async.hpp

It looks like this:

inline thread_local __jule_Ptr<__jule_thread> __jule_ct;

To determine when this issue was introduced, I tested older llvm-mingw releases. Based on my tests, llvm-mingw-20240502-ucrt-aarch64 (LLVM 18.1.5) behaves as expected and links successfully with LTO enabled. However, llvm-mingw-20240518-ucrt-aarch64 (LLVM 18.1.6) consistently exhibits the linker failure described above. This suggests that the issue may have been introduced between LLVM 18.1.5 and 18.1.6.

clang++ --version for 18.1.6:

clang version 18.1.6 (https://github.com/llvm/llvm-project.git 1118c2e05e67a36ed8ca250524525cdb66a55256)
Target: aarch64-w64-windows-gnu
Thread model: posix
InstalledDir: C:/llvm/bin

clang++ --version for 18.1.5:

clang version 18.1.5 (https://github.com/llvm/llvm-project.git 617a15a9eac96088ae5e9134248d8236e34b91b1)
Target: aarch64-w64-windows-gnu
Thread model: posix
InstalledDir: C:/llvm/bin

The problem is still present in the latest release and pre-release.

Since I am working on Windows ARM64, I am using this toolchain:
llvm-mingw-20260127-ucrt-aarch64.

Reproduction

I attempted to create a small standalone reproducible example, but I was not able to isolate the issue in a minimal form. I've prepared a PowerShell script. This script downloads the source code of the jule0.2.0 release along with the IR file used for the production build, and then attempts to compile it using clang++. I hope this will make reproducing the issue easier.

By default, it downloads the IR file built for ARM64. If you're testing on an AMD64 system, you can change the windows-arm64.cpp part of the IR file name to windows-amd64.cpp.

After performing the required downloads, it runs a build attempt for you. If the reproduction is successful, you should then be able to debug the downloaded source code locally as you wish.

This is the script:

echo "Downloading jule0.2.0 source code"
curl.exe -L -o jule0.2.0.zip "https://github.com/julelang/jule/archive/refs/tags/jule0.2.0.zip"
echo "Extracting..."
Expand-Archive -Path "jule0.2.0.zip" -DestinationPath ".\"
echo "cd jule-jule0.2.0"
cd jule-jule0.2.0
echo "mkdir bin"
mkdir bin
echo "Downloading C++ IR of jule0.2.0"
curl -o ir.cpp https://raw.githubusercontent.com/julelang/julec-ir/e4134d89f34588e9fb5cc5698f27b0471918e057/src/windows-arm64.cpp
echo "Compiling..."
clang++ -static -Wno-everything --std=c++20 -fwrapv -ffloat-store -fno-fast-math -fno-rounding-math -ffp-contract=fast -O3 -flto=thin -DNDEBUG -fomit-frame-pointer -fno-strict-aliasing -o ./bin/julec.exe ir.cpp -lws2_32 -lshell32 -liphlpapi -lsynchronization

Additional

Potential issues that may point to the same problem:

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions