quimnuss
quimnuss

Reputation: 1561

How to create a shared library wrapper for a static library (without using source files) with CMake?

I'd like to create a liboo.so from a libfoo.a using CMake.

So far I have

include_directories(third-party/includes)
find_library(${thirdparty_LIBRARIES} foo PATHS third-party/lib)
add_library(boo SHARED empty.cpp)
target_link_libraries(boo ${thirdparty_LIBRARIES})
add_executable(runBoo main.cpp)
target_link_libraries(runBoo boo)

where main calls functions from libfoo.so. But provokes the error:

main.cpp:(.text+0x26): undefined reference to `Foo::Foo()'
main.cpp:(.text+0x50): undefined reference to `Foo::sayHello(std::basic_string<char, std::char_traits<char>, std::allocator<char> >)'

I'm guessing the symbols aren't added since empty.cpp doesn't use them, but I'm not sure that's the issue and how to overcome it.

I've seen CMake: how create a single shared library from all static libraries of subprojects? , but I'd prefer to stick to lower versions of cmake for now.

I've also seen how to link static library into dynamic library in gcc but I can't get it to work and I'm using CMake anyway.

Upvotes: 2

Views: 2487

Answers (3)

Mike Kinghan
Mike Kinghan

Reputation: 61575

@quimnuss's accepted CMake solution is to create a shared library whose solitary source file is an empty file and that is simply linked aginst the static library it is supposed to wrap1.

At this writing the only other answer is @cyrusbehr's. As @cyrusbehr says, the accepted answer is wrong, and it is wrong for the reason @cyrusbehr states.

@cyrusbehr's remedy's is to enclose static libraries beteween linker options --whole-archive/--no-whole-archive. That is right, provided the toolchain is GCC. The effect of these options to coerce the linker to link all object files in the enclosed static libraries into the target, whether it needs them or not. It may be inferred that the OP's toolchain is GCC from incidental clues, but that is not specified and a toolchain-neutral CMake solution exists. @cyrusbehr also carries over from @quimnuss the kludge of adding an empty source file to the project just to provide something that satisfies the grammar of:

add_library(<libame> SHARED <sourcefile>...)

It would be odd if CMake made this necessary and it isn't. (On Linux, it also leaves a trace in the output shared library in the form of a local symbol named empty.cpp, or whatever the empty file is called).

Under the hood, CMake on my Linux system delegates by default to GNU Make and the GCC toolchain and deals in ELF object files, programs and shared libraries. On other systems these internals will vary but the story is the same.

Demo that the wrong answer fails, and why.

We'll make a static library libfoobar.a that defines functions foo and bar and pretend it already existed, let's say in directory ./lib.

$ cat foo.cpp 
int bar()
{
    return 17;
}

$ cat bar.cpp
int foo()
{
    return 13;
}

$ mkdir lib

$ g++ -c foo.cpp bar.cpp
$ ar rcs ./lib/libfoobar.a foo.o bar.o

Here are the symbol tables of the two object files in the archive:

$ readelf --demangle --syms  ./lib/libfoobar.a 

File: ./lib/libfoobar.a(foo.o)

Symbol table '.symtab' contains 4 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS foo.cpp
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 .text
     3: 0000000000000000    15 FUNC    GLOBAL DEFAULT    1 bar()

File: ./lib/libfoobar.a(bar.o)

Symbol table '.symtab' contains 4 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS bar.cpp
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 .text
     3: 0000000000000000    15 FUNC    GLOBAL DEFAULT    1 foo()
     

Next we'll make a CMake project that:-

  • Uses the wrong answer to create a shared library libfoobar.so supposedly wrapping libfoobar.so.
  • Creates a program that calls foo and bar and is linked against libfoobar.so.

This is just the same sort of project that the wrong answer offers as its evidence.

$ cat main.cpp 
#include <iostream>

extern int foo();
extern int bar();

int main()
{
    std::cout << std::boolalpha << (foo() == bar()) << std::endl;
    return 0;
}
    
$ cat CMakeLists.txt
cmake_minimum_required (VERSION 3.15)
project(prog)

find_library(static_foobar foobar PATHS ${CMAKE_SOURCE_DIR}/lib)
add_library(foobar SHARED empty.cpp)
target_link_libraries(foobar ${static_foobar})
add_executable(prog main.cpp)
target_link_libraries(prog foobar)

$ mkdir build

$ cd build/

$ cmake ..
-- The C compiler identification is GNU 13.2.0
-- The CXX compiler identification is GNU 13.2.0
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done (0.5s)
-- Generating done (0.0s)
-- Build files have been written to: /home/imk/develop/so/cmake_prob/build

$ make
[ 25%] Building CXX object CMakeFiles/foobar.dir/empty.cpp.o
[ 50%] Linking CXX shared library libfoobar.so
[ 50%] Built target foobar
[ 75%] Building CXX object CMakeFiles/prog.dir/main.cpp.o
[100%] Linking CXX executable prog
[100%] Built target prog

$ ./prog
false

The program was built and it runs correctly.

That's good but doesn't answer the question.

We want to know is: Has libfoobar.a has been wrapped in libfoobar.so, so that prog only depends on libfoobar.so. Let's zap prog and look at it's linkage again, this time in verbose mode:

$ rm prog
$ make VERBOSE=1 prog
...[cut]...
[ 75%] Linking CXX executable prog
/usr/bin/cmake -E cmake_link_script CMakeFiles/prog.dir/link.txt --verbose=1
/usr/bin/c++ CMakeFiles/prog.dir/main.cpp.o -o prog  -Wl,-rpath,/home/imk/develop/so/cmake_prob/build libfoobar.so /home/imk/develop/so/cmake_prob/lib/libfoobar.a 
make[3]: Leaving directory '/home/imk/develop/so/cmake_prob/build'
[100%] Built target prog
...[cut]...

See that prog is in fact being linked against both libfoobar.so and the original libfoobar.a.

The new libfoobar.so contributes 0 definitions to the linkage because it does not define either foo or bar:

$ readelf --syms libfoobar.so | egrep \(foo\|bar\); echo Done
Done

That's for the reason that @cyrusbehr stated. libfoobar.so is the same shared library we'd get with empty.cpp as its sole source file and linking nothing to it all.

The original libfoobar.a is still providing the definitions of foo and bar. If we remove libfoobar.so (and its runpath, -rpath...) from the CMake linkage command:

$ /usr/bin/c++ CMakeFiles/prog.dir/main.cpp.o -o prog  /home/imk/develop/so/cmake_prob/lib/libfoobar.a
$ ./prog
false

the linkage is unscathed. If we remove libfoobar.a

$ /usr/bin/c++ CMakeFiles/prog.dir/main.cpp.o -o prog  -Wl,-rpath,/home/imk/develop/so/cmake_prob/build libfoobar.so
/usr/bin/ld: CMakeFiles/prog.dir/main.cpp.o: in function `main':
main.cpp:(.text+0x28): undefined reference to `foo()'
/usr/bin/ld: main.cpp:(.text+0x2f): undefined reference to `bar()'
collect2: error: ld returned 1 exit status

the linkage fails. libfoobar.so is not a wrapper for libfoobar.a: it's a hollow shell..

Why does CMake link prog with both libfoobar.so and libfoobar.a?

Because of:

target_link_libraries(foobar ${static_foobar})

By default in CMake, dependencies of a library are inherited by any target to which the library is in turn linked, so target prog inherits the dependency on libfoobar.a from libfoobar.so.2. But this inheritance defeats the objective of wrapping libfoobar.a in libfoobar.so. For that, libfoobar.a should be a PRIVATE dependency of libfoobar.so.3 And if we do that, then regenerate the build system and rebuild (from now on our shell is always in the build directory):

$ cat ../CMakeLists.txt
cmake_minimum_required (VERSION 3.15)
project(prog)

find_library(static_foobar foobar PATHS ${CMAKE_SOURCE_DIR}/lib)
add_library(foobar SHARED empty.cpp)
target_link_libraries(foobar PRIVATE ${static_foobar})
add_executable(prog main.cpp)
target_link_libraries(prog foobar)

$ rm -fr *
$ cmake ..
...[cut]...
$ make VERBOSE=1
...[cut]...
[100%] Linking CXX executable prog
/usr/bin/cmake -E cmake_link_script CMakeFiles/prog.dir/link.txt --verbose=1
/usr/bin/c++ CMakeFiles/prog.dir/main.cpp.o -o prog  -Wl,-rpath,/home/imk/develop/so/cmake_prob/build libfoobar.so 
/usr/bin/ld: CMakeFiles/prog.dir/main.cpp.o: in function `main':
main.cpp:(.text+0x28): undefined reference to `foo()'
/usr/bin/ld: main.cpp:(.text+0x2f): undefined reference to `bar()'
collect2: error: ld returned 1 exit status
..[cut]...

then the CMake linkage command turns into the one we've already seen to fail.

Demo that @cyrusbehr's fix works for GCC.

For this we'll have:

$ cat ../CMakeLists.txt
cmake_minimum_required (VERSION 3.15)
project(prog)

find_library(static_foobar foobar PATHS ${CMAKE_SOURCE_DIR}/lib)
add_library(foobar SHARED empty.cpp)
SET(static_foobar_whole_archive -Wl,--whole-archive ${static_foobar} -Wl,--no-whole-archive)
target_link_libraries(foobar PRIVATE ${static_foobar_whole_archive})
add_executable(prog main.cpp)
target_link_libraries(prog foobar)

with libfoobar.a a PRIVATE dependency of libfoobar.so, but enclosed in --whole-archive/--no-whole-archive linker options.

Rebuilding from scratch again:

$ rm -fr *
$ cmake ..
...[cut]...
$ make VERBOSE=1
...[cut]...
[ 50%] Linking CXX shared library libfoobar.so
/usr/bin/cmake -E cmake_link_script CMakeFiles/foobar.dir/link.txt --verbose=1
/usr/bin/c++ -fPIC -shared -Wl,-soname,libfoobar.so -o libfoobar.so CMakeFiles/foobar.dir/empty.cpp.o   -L/home/imk/develop/so/cmake_prob/lib  -Wl,-rpath,/home/imk/develop/so/cmake_prob/lib -Wl,--whole-archive -lfoobar -Wl,--no-whole-archive
...[cut]... 
[100%] Linking CXX executable prog
/usr/bin/cmake -E cmake_link_script CMakeFiles/prog.dir/link.txt --verbose=1
/usr/bin/c++ CMakeFiles/prog.dir/main.cpp.o -o prog  -Wl,-rpath,/home/imk/develop/so/cmake_prob/build libfoobar.so 
make[2]: Leaving directory '/home/imk/develop/so/cmake_prob/build'
[100%] Built target prog
...[cut]...

--whole-archive/--no-whole-archive in the linkage of libfoobar.so successfully links all the definitions from libfoobar.a into libfoobar.so:

$ readelf --demangle --syms libfoobar.so | egrep \(foo\|bar\|dynsym\|symtab\); echo Done
Symbol table '.dynsym' contains 7 entries:
     5: 00000000000010f9    15 FUNC    GLOBAL DEFAULT   10 bar()
     6: 0000000000001108    15 FUNC    GLOBAL DEFAULT   10 foo()
Symbol table '.symtab' contains 28 entries:
    10: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS foo.cpp
    11: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS bar.cpp
    22: 0000000000001108    15 FUNC    GLOBAL DEFAULT   10 foo()
    24: 00000000000010f9    15 FUNC    GLOBAL DEFAULT   10 bar()
Done

and makes the linkage of prog successful with no other library mentioned:

$ ./prog
false

This solution has the shortcoming that:

SET(static_foobar_whole_archive -Wl,--whole-archive ${static_foobar} -Wl,--no-whole-archive)

is valid only for the GCC toolchain and the equivalent linker option(s) must be found for other toolchains. We'd like a toolchain-neutral CMake way.

And as I said, the empty source file leaves this vestige in the symbol table of libfoobar.so:

$ readelf --demangle --syms libfoobar.so | egrep \(empty.cpp\|dynsym\|symtab\); echo Done
Symbol table '.dynsym' contains 7 entries:
Symbol table '.symtab' contains 28 entries:
     9: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS empty.cpp
Done

Toolchain-neutral solution without the empty source file.

For this we'll have:

$ cat ../CMakeLists.txt 
cmake_minimum_required (VERSION 3.15)
project(prog)

find_library(static_foobar foobar PATHS ${CMAKE_SOURCE_DIR}/lib)
add_library(foobar SHARED ${static_foobar})
target_link_libraries(foobar PRIVATE "$<LINK_LIBRARY:WHOLE_ARCHIVE,${static_foobar}>")
set_target_properties(foobar PROPERTIES LINKER_LANGUAGE CXX )
add_executable(prog main.cpp)
target_link_libraries(prog foobar)

The empty source file is eliminated by replacing:

add_library(foobar SHARED empty.cpp)

with:

add_library(foobar SHARED ${static_foobar})

i.e. we say that the sole source file for building libfoobar.so is libfoobar.a. Of course libfoobar.a is not a programming language source file, but CMake's documentary nomenclature:

[add_library(<name> [<type>] [EXCLUDE_FROM_ALL] <sources>...)](https://cmake.org/cmake/help/latest/command/add_library.html)

is a little misleading here and elsewhere where it uses <sources>... in target-creation syntax. A <source> need not be a programming language source file. If it does not have an absolute name (after relative path expansion) - e.g. empty.cpp - CMake will assume it is source-code file relative to ${CMAKE_SOURCE_DIR} and is to be input to the target's language compiler. Otherwise it will assume the<source> is the absolute name (after expansion) of a file that is to be input to the target's link step. So the snippet:

add_library(foobar SHARED ${static_foobar})
target_link_libraries(foobar PRIVATE "$<LINK_LIBRARY:WHOLE_ARCHIVE,${static_foobar}>")

boils down to: ${static_foobar} ( = the full name of libfoobar.a) is a linker input file for target foobar and it is library on which the target depends which is to be linked with the predefined LINK_LIBRARY feature WHOLE_ARCHIVE). That feature is the one that does the right thing per toolchain to link every object file in a static library into the target.

Since we are inputting no language source files for target foobar, CMake is unable to guess from the extensions of such files what language-specific variant of linkage command to use for that target - what frontend, what boilerplate linkage options. So we have to tell it to link it as per C++ targets:

set_target_properties(foobar PROPERTIES LINKER_LANGUAGE CXX )

Rebuilding from scratch once more:

$ rm -fr *
$ cmake ..
...[cut]...
$ make VERBOSE=1
...[cut]...
[ 33%] Linking CXX shared library libfoobar.so
/usr/bin/cmake -E cmake_link_script CMakeFiles/foobar.dir/link.txt --verbose=1
/usr/bin/c++ -fPIC -shared -Wl,-soname,libfoobar.so -o libfoobar.so  -Wl,--push-state,--whole-archive /home/imk/develop/so/cmake_prob/lib/libfoobar.a -Wl,--pop-state 
...[cut]...
[100%] Linking CXX executable prog
/usr/bin/cmake -E cmake_link_script CMakeFiles/prog.dir/link.txt --verbose=1
/usr/bin/c++ CMakeFiles/prog.dir/main.cpp.o -o prog  -Wl,-rpath,/home/imk/develop/so/cmake_prob/build libfoobar.so 
make[2]: Leaving directory '/home/imk/develop/so/cmake_prob/build'
[100%] Built target prog
...[cut]...

$ ./prog
false

This goes through effectively the same @cyrusbehr's solution except that no empty.cpp.o is redundantly linked, there was no redundant compile step for empty.cpp and empty.cpp gets no name-check in the symbol table of libfoobar.so. The CMake-generated linker options --push-state,--whole-archive ... --pop-state have the same effect --whole-archive ... --no-whole-archive.


  1. Wrapping a static library in a shared library is what the question wants to do, but is a bit of a misnomer. When a shared library or program is linked against a static library, the static library is not embedded or encapsulated in the linkage target. The static library is an archive of object files. By default the linker recursively discovers an object file in the archive that serves to resolve an undefined symbol reference that has accrued earlier in the linkage process, copies that object file out of the archive and links it individually into the target - iterating until the archive yields no more needed object files. No identifying trace of the static library is linked into the target.

  2. CMake has implemented transitivity of library dependencies since at least v2.6, 2008. I can't readily find earlier documentation.

  3. When the question was posted in 2013, recent versions of CMake didn't support the PRIVATE (or PUBLIC or INTERFACE) properties of target_link_libraries to control the inheritance of library dependencies. They supported LINK_PRIVATE and LINK_PUBLIC properties corresponding respectively to PRIVATE and PUBLIC and the target property LINK_INTERFACE_LIBRARIES specified an optional list of inherited dependencies overriding those that would accrue to the target by default.

Upvotes: 1

cyrusbehr
cyrusbehr

Reputation: 1301

The selected answer is not correct. You are compiling an empty source file into a shared library. You then try to link a static library to this shared library. Since the static library is not used in the code which is compiled, it will not be linked and will be ignored. You must therefore add the -whole-archive linker flag to force the static library to be linked, so something like the following.

add_library(my_shared_lib SHARED src/empty.cpp)
SET(MY_LIB_LINK_LIBRARIES -Wl,--whole-archive my_static_library.a -Wl,--no-whole-archive)
target_link_libraries(my_shared_lib ${MY_LIB_LINK_LIBRARIES})

Upvotes: 1

quimnuss
quimnuss

Reputation: 1561

There was just a silly mistake. The corrected code is:

   include_directories(third-party/includes)

   find_library(thirdparty_LIBRARIES foo PATHS third-party/lib) //remove the ${}!

   add_library(boo SHARED empty.cpp)
   target_link_libraries(boo ${thirdparty_LIBRARIES})
   add_executable(runBoo main.cpp)
   target_link_libraries(runBoo boo)

Upvotes: 0

Related Questions