Skip to content

[clang] [windows] [modules] undefined symbol when exporting variable from shared library #173997

@Silverlan

Description

@Silverlan

Clang version: 21.1.8 (Downloaded from this repository)
Also tested on trunk (at commit faf140a), which produces the same results.

Full source code for the minimal reproduction code, including GitHub workflows:
https://github.com/Silverlan/tmp_clang_errors/tree/undefined-symbol

Full build log: https://github.com/Silverlan/tmp_clang_errors/actions/runs/20599656929/job/59161977160

Error Message:

 lld-link: error: undefined symbol: struct TestVector const test::cexpr_test_vector
>>> referenced by CMakeFiles/test_program.dir/main.cpp.obj:(main)
clang++: error: linker command failed with exit code 1 (use -v to see invocation)

Description

This bug happens when trying to access a variable exported from a c++20 module from a dynamic library when using clang (I have not tested clang-cl) on Windows. On Linux it compiles and links without any issues.

I've narrowed the problem down as much as I can. The reproduction project consists of a main application ("test_program"), as well as a dynamically linked library ("test_library"), which exports a c++20 module ("test_module"). Here is a short description of the code:

Code

test_library
This is a shared library, which exports a c++20 module with some simple objects.

export module test_module;

#ifdef _WIN32
#define API __declspec(dllexport)
#else
#define API __attribute__((visibility("default")))
#endif

export {
    struct API TestVector {
        float x;
        float y;
        float z;
    };
}

export namespace test {
    // These are causing "undefined symbol" errors in certain circumstances (see test cases in main program)
    constexpr TestVector cexpr_test_vector = TestVector{1.f,2.f,3.f};
    API TestVector dllexp_test_vector = TestVector{1.f,2.f,3.f};
    //

    API TestVector dllexp_get_test_vector() {return TestVector{1.f,2.f,3.f};}
    API void test_function(const TestVector &v)
    {
    }
}

test_program
The program links against test_library and contains several test cases which can be switched between when configuring the project with CMake (using the -DTEST_CASE=X option). Some cases trigger the linking error, others do not (see comments below for each test case).

#include <iostream>

import test_module;

// Test Case 0: Access member of constexpr vector from module test_module
// Result: Build successful
#if TEST_CASE == 0

int main() {
    std::cout<<test::cexpr_test_vector.x<<std::endl;
    return 0;
}

#endif

// Test Case 1: Call test_function from module test_module with constexpr vector
// Result: lld-link: error: undefined symbol: struct TestVector const test::cexpr_test_vector
#if TEST_CASE == 1

int main() {
    test::test_function(test::cexpr_test_vector);
    return 0;
}

#endif

// Test Case 2: Same as Test Case 1, but with locally created vector
// Result: Build successful
#if TEST_CASE == 2

int main() {
    test::test_function(TestVector {1.f, 2.f, 3.4});
    return 0;
}

#endif

// Test Case 3: Calling a locally defined function with constexpr vector as argument
// Result: Build successful
#if TEST_CASE == 3

static void print_vec(const TestVector v)
{
    std::cout<<v.x<<","<<v.y<<","<<v.z<<std::endl;
}

int main() {
    print_vec(test::cexpr_test_vector);
    return 0;
}

#endif

// Test Case 4: Same as Test Case 3, but with optimization turned off
// Result: lld-link: error: undefined symbol: struct TestVector const test::cexpr_test_vector
#if TEST_CASE == 4

#pragma clang optimize off
static void print_vec(const TestVector v)
{
    std::cout<<v.x<<","<<v.y<<","<<v.z<<std::endl;
}

int main() {
    print_vec(test::cexpr_test_vector);
    return 0;
}
#pragma clang optimize on

#endif

// Test Case 5: Same as Test Case 0, but using dllexported vector instead of constexpr
// Result: lld-link: error: undefined symbol: struct TestVector test::dllexp_test_vector
#if TEST_CASE == 5

int main() {
    std::cout<<test::dllexp_test_vector.x<<std::endl;
    return 0;
}

#endif

// Test Case 6: Same as Test Case 0, but using dllexported getter-function for vector instead of constexpr
// Result: Build successful
#if TEST_CASE == 6

int main() {
    std::cout<<test::dllexp_get_test_vector().x<<std::endl;
    return 0;
}

#endif

All test cases build and link successfully with clang on Linux.
Interestingly, when building with MSVC on Windows, Case 0 and 1 also fail with a similar error as the one clang reports:

main.cpp.obj : error LNK2019: unresolved external symbol "struct TestVector const test::cexpr_test_vector" (?cexpr_test_vector@test@@3UTestVector@@B::<!test_module>) referenced in function main

Build instructions:

Run from visual studio 2022 command prompt (replace clang executable paths CMAKE_C_COMPILER and CMAKE_CXX_COMPILER):

git clone --single-branch --branch "undefined-symbol" https://github.com/Silverlan/tmp_clang_errors.git
cd tmp_clang_errors
set root=%cd%
cd test_program
mkdir build
cd build
cmake -S .. -B . -G "Ninja" -DCMAKE_C_COMPILER="%root%/clang/bin/clang.exe"  -DCMAKE_CXX_COMPILER="%root%/clang/bin/clang++.exe" -DCMAKE_INSTALL_PREFIX:PATH="%root%/install" -DCMAKE_CXX_SCAN_FOR_MODULES=1 -DCMAKE_EXPORT_COMPILE_COMMANDS=1 -DCMAKE_BUILD_TYPE=Release -DCMAKE_POLICY_VERSION_MINIMUM=3.5 -DTEST_CASE=0
cmake --build .
cmake --install .
%root%/install/test_program.exe

(Change -DTEST_CASE=0 for each test case.)
(Alternatively you can fork the undefined-symbol branch of the test project repository and run the GitHub workflow.)

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions