2

Here is a simple CMake project:

cmake_minimum_required(VERSION 3.0)

project(Test)

add_library(Test STATIC test1.cpp test2.cpp)

With test1 as follows:

// test1.h
#pragma once

void test();

// test1.cpp
#include "test1.h"

void test() 
{

}

and test2:

// test2.h
#pragma once

void test();

// test2.cpp
#include "test2.h"

void test() 
{

}

This project compiles just fine. However, if I change the library to be dynamic:

add_library(Test SHARED test1.cpp test2.cpp)

the project fails to compile with a linking error stating (MSVC 17.5.1):

fatal error LNK1169: one or more multiply defined symbols found

What causes the static library to compile and the shared library to fail compilation? I would expect the static library to fail too, but that doesn't happen.

alma456
  • 21
  • 2
  • How to make it fail: use `extern void test()` in the another `cpp`) Do you use MSVC compiler? It has soft link – Shamil Mukhetdinov Apr 21 '23 at 20:41
  • I am not used to Windows/MSVC, but aren't symbols by default non-exported there? Are you sure the error occurs with dynamic linking and not the other way around? – user17732522 Apr 21 '23 at 20:46
  • @ShamilMukhetdinov Function declarations are `extern` by default. Why should `extern` make a difference? – user17732522 Apr 21 '23 at 20:47
  • Generally, from a standard point of view, the program is IFNDR (ill-formed, no diagnostic required) either way for ODR (one definition rule) violation. The NDR part means that a compiler/linker doesn't have to tell you about the issue. Of course that doesn't mean that the program isn't well-defined on a given implementation. – user17732522 Apr 21 '23 at 20:48
  • Yes, I can confirm that the compilation fails with the shared library only (I tried earlier with gcc too and there was the same behaviour). – alma456 Apr 21 '23 at 20:59

1 Answers1

0

Some modified files to demonstrate some points:

CMakeLists.txt

cmake_minimum_required(VERSION 3.0)
project(Test)
#add_library(Test SHARED test1.cpp test2.cpp)
add_library(test STATIC test1.cpp test2.cpp test3.cpp)

test1.h

#pragma once
int test();

test1.cpp

#include "test1.h"
int myvar1 = 12345;
int test() 
{
   return myvar1; 
}

test2.h

#pragma once
int test();

test2.cpp

#include "test2.h"
int myvar2=22;
int test() 
{
    return myvar2;
}

test3.h

#pragma once
int test();
int test3();

test3.cpp

#include "test3.h"
int myvar3=333;
int test() 
{
    return myvar3;
}
int test3()
{
    return 3332;
}

build.sh

tree
cat ./*
cmake .
echo "build the library"
make VERBOSE=1
echo "show symbols"
nm ./libtest.a | c++filt
echo "build the executable manually"
g++ -c ./main.cpp -o main.o
echo "link manually"
g++ --verbose -Wl,--warn-common -Wl,--verbose ./main.o ./libtest.a -o testexec
./testexec
echo "clean"
rm -r ./CMakeCache.txt  ./*.cmake ./Makefile ./CMakeFiles ./testexec ./*.o ./*.a ./*.so

A test build

A redacted [...] output of the build script is as follows:

.
├── build.sh
├── CMakeLists.txt
├── main.cpp
├── test1.cpp
├── test1.h
├── test2.cpp
├── test2.h
├── test3.cpp
└── test3.h

0 directories, 11 files
@cmake
[...]
@build the library
[...]
[ 25%] Building CXX object CMakeFiles/test.dir/test1.cpp.o
/usr/bin/c++    -MD -MT CMakeFiles/test.dir/test1.cpp.o -MF CMakeFiles/test.dir/test1.cpp.o.d -o CMakeFiles/test.dir/test1.cpp.o -c /tmp/cpp/test1.cpp
[ 50%] Building CXX object CMakeFiles/test.dir/test2.cpp.o
/usr/bin/c++    -MD -MT CMakeFiles/test.dir/test2.cpp.o -MF CMakeFiles/test.dir/test2.cpp.o.d -o CMakeFiles/test.dir/test2.cpp.o -c /tmp/cpp/test2.cpp
[ 75%] Building CXX object CMakeFiles/test.dir/test3.cpp.o
/usr/bin/c++    -MD -MT CMakeFiles/test.dir/test3.cpp.o -MF CMakeFiles/test.dir/test3.cpp.o.d -o CMakeFiles/test.dir/test3.cpp.o -c /tmp/cpp/test3.cpp
[100%] Linking CXX static library libtest.a
/usr/bin/cmake -P CMakeFiles/test.dir/cmake_clean_target.cmake
/usr/bin/cmake -E cmake_link_script CMakeFiles/test.dir/link.txt --verbose=1
/usr/bin/ar qc libtest.a CMakeFiles/test.dir/test1.cpp.o CMakeFiles/test.dir/test2.cpp.o CMakeFiles/test.dir/test3.cpp.o
/usr/bin/ranlib libtest.a
[...]
@show symbols

test1.cpp.o:
0000000000000000 D myvar1
0000000000000000 T test()

test2.cpp.o:
0000000000000000 D myvar2
0000000000000000 T test()

test3.cpp.o:
0000000000000000 D myvar3
0000000000000000 T test()
000000000000000c T test3()
@build the executable manually
@link manually and run
Using built-in specs.
[...]
/usr/lib64/gcc/x86_64-suse-linux/7/crtbegin.o
attempt to open ./main.o succeeded
./main.o
attempt to open ./libtest.a succeeded
./libtest.a
(./libtest.a)test1.cpp.o
(./libtest.a)test3.cpp.o
attempt to open /usr/lib64/gcc/x86_64-suse-linux/7/libstdc++.so succeeded
[...]
ld-linux-x86-64.so.2 needed by /usr/lib64/gcc/x86_64-suse-linux/7/libstdc++.so
found ld-linux-x86-64.so.2 at /lib64/ld-linux-x86-64.so.2/usr/lib64/gcc/x86_64-suse-linux/7/../../../../x86_64-suse-linux/bin/ld: ./libtest.a(test3.cpp.o): in function `test()':
test3.cpp:(.text+0x0): multiple definition of `test()'; ./libtest.a(test1.cpp.o):test1.cpp:(.text+0x0): first defined here
/usr/lib64/gcc/x86_64-suse-linux/7/../../../../x86_64-suse-linux/bin/ld: link errors found, deleting executable `testexec'
collect2: error: ld returned 1 exit status
@clean

Explanation

cmake invokes ar (archiver) to create a static library (which is the standard way to do it). However, ar is unaware of program symbols and blindly packages .o (object) files.

ld (the GNU linker) has "smart" symbol detection when loading external libraries. This answer has many interesting details

In the static compile shown above, libtest.a is treated as an external library and linking and loading is done per the following logic:

  • ld processes objects from left to right in its command. Swapping main.o and libtest.a in the command fails differently.
  • ld resolves the test() symbol in main.cpp.o to test() in test1.cpp.o
  • ld detects that it has no more symbols to resolve which are available in test2.cpp.o so test2.cpp.o is never even loaded by ld.
  • ld resolves the test3() symbol in main.cpp.o to test3() in test3.cpp.o so test3.cpp.o is loaded. However, test3.cpp.o also contains the test() symbol so the conflicting symbol error occurs just like when linking dynamically / a shared library. Removing or renaming test() in test3.cpp resolves this.
maxp
  • 26
  • 4