今天會介紹 link libraries 時需要注意的事項和一些建議的實作方式, 比如盡量不要讓 static library 被重複 link, 和只 export 必要的 symbol
這樣能讓我們的程式更安全, 更輕量, 執行速度較快
下面我們就來看看具體是怎麼做的
先來看看今天的專案架構
cmake-example/
├─ src/
│ ├─ lib/
│ │ ├─ CMakeLists.txt
│ │ ├─ shared_lib.cpp
│ │ ├─ static_lib.cpp
│ ├─ CMakeLists.txt
│ ├─ main.cpp
├─ CMakeLists.txt
src/main.cpp
#include <iostream>
extern int x;
void Foo();
int main() {
Foo();
std::cout << x << std::endl;
return 0;
}
src/lib/static_lib.cpp
#include "sharedlib_export.h"
SHAREDLIB_EXPORT int x = 10;
src/lib/shared_lib.cpp
#include "sharedlib_export.h"
SHAREDLIB_EXPORT void Foo() {
// pass
};
void Bar() {
// pass
}
我們的 dependency tree 會長這樣
可以看到, executable Main 會 link StaticLib 和 SharedLib
而 SharedLib 也會 link StaticLib
這樣會有什麼問題呢?
假如 SharedLib 是 private link StaticLib, SharedLib 就不會有 StaticLib 的 symbol
這會造成 Main 在 link StaticLib 時將 StaticLib 的 symbols 塞到 relocatable text section
但是 SharedLib 這時也有一份, 導致在最後產出 executable 時就會同時存在兩個 StaticLib 中的 global variable instance, 分別存在於 Main 和 SharedLib 中
這會造成之後 Main 和 SharedLib 在使用 StaticLib 的變數時得到不一致的結果
那麼該怎麼辦呢?
比較好的做法是, 讓 SharedLib 提供 StaticLib 的 symbols, Main 就只需要 link SharedLib 就好, 所以我們可以把 Link Dependency 改成這樣
這樣就可以避免上述問題發生, 但由於 StaticLib 的 symbols 現在只有 SharedLib 能看見, 我們還需要一些處理才能讓 Main 順利使用
通常我們不想要把 SharedLib
所有的 symbols 都 export 出來, 這會讓最終產生的 executable 變太大, 降低執行速度
遺憾的是, 這是 gcc
和 clang
預設的行為
所以我們最好將該行為改掉, 變成預設是不 export 任何 symbols, 有需要用到再 export!
CMake 提供了 CMAKE_<LANG>_VISIBILITY_PRESET
和 CMAKE_VISIBILITY_INLINES_HIDDEN
兩個變數來控制該行為
我們可以將 CMAKE_<LANG>_VISIBILITY_PRESET
設為 hidden, CMAKE_VISIBILITY_INLINES_HIDDEN
設為 TRUE
, 這樣就會預設將所有的 symbols 都隱藏不 export 了, 讚讚
src/lib/CMakeLists.txt
set(CMAKE_CXX_VISIBILITY_PRESET hidden) 👈
set(CMAKE_VISIBILITY_INLINES_HIDDEN TRUE) 👈
add_library(StaticLib STATIC
static_lib.cpp
)
add_library(SharedLib SHARED
shared_lib.cpp
)
target_link_libraries(SharedLib PUBLIC
StaticLib
)
挖~改完之後 Main
就看不到 SharedLib
的 symbols 了, 所以會 build fail
-- Configuring done (0.0s)
-- Generating done (0.0s)
-- Build files have been written to: /content/cmake-example/build
[ 16%] Building CXX object src/lib/CMakeFiles/StaticLib.dir/static_lib.cpp.o
[ 33%] Linking CXX static library libStaticLib.a
[ 33%] Built target StaticLib
[ 50%] Building CXX object src/lib/CMakeFiles/SharedLib.dir/shared_lib.cpp.o
[ 66%] Linking CXX shared library libSharedLib.so
[ 66%] Built target SharedLib
[ 83%] Building CXX object src/CMakeFiles/Main.dir/main.cpp.o
[100%] Linking CXX executable Main
/usr/bin/ld: CMakeFiles/Main.dir/main.cpp.o: in function `main':
main.cpp:(.text.startup+0xc): undefined reference to `Foo()'
collect2: error: ld returned 1 exit status
gmake[2]: *** [src/CMakeFiles/Main.dir/build.make:99: src/Main] Error 1
gmake[1]: *** [CMakeFiles/Makefile2:118: src/CMakeFiles/Main.dir/all] Error 2
gmake: *** [Makefile:91: all] Error 2
所以我們需要把 Foo
重新 export 出來, 可以用 CMake 提供的 generate_export_header()
generate_export_header(LIBRARY_TARGET
[BASE_NAME <base_name>]
[EXPORT_MACRO_NAME <export_macro_name>]
[EXPORT_FILE_NAME <export_file_name>]
[DEPRECATED_MACRO_NAME <deprecated_macro_name>]
[NO_EXPORT_MACRO_NAME <no_export_macro_name>]
[INCLUDE_GUARD_NAME <include_guard_name>]
[STATIC_DEFINE <static_define>]
[NO_DEPRECATED_MACRO_NAME <no_deprecated_macro_name>]
[DEFINE_NO_DEPRECATED]
[PREFIX_NAME <prefix_name>]
[CUSTOM_CONTENT_FROM_VARIABLE <variable>]
)
GenerateExportHeader
module<target-name>_export.h
header file<target-name>_EXPORT
, 根據 compiler 不同被 preprocess 成不同的 keyword, 比如 Visual Studio 是 __declspec()
, gcc
則是 __attribute__()
src/CMakeLists.txt
set(CMAKE_CXX_VISIBILITY_PRESET hidden)
set(CMAKE_VISIBILITY_INLINES_HIDDEN TRUE)
add_library(StaticLib STATIC
static_lib.cpp
)
add_library(SharedLib SHARED
shared_lib.cpp
)
target_include_directories(SharedLib PUBLIC 👈
${CMAKE_CURRENT_BINARY_DIR}
)
target_link_libraries(SharedLib PUBLIC
StaticLib
)
include(GenerateExportHeader) 👈
generate_export_header(SharedLib) 👈
src/lib/shared_lib.cpp
#include "sharedlib_export.h" 👈
SHAREDLIB_EXPORT void Foo() { 👈
// pass
};
void Bar() {
// pass
}
我們可以用 nm -gDC
看一下, 得到以下結果
w __cxa_finalize
w __gmon_start__
w _ITM_deregisterTMCloneTable
w _ITM_registerTMCloneTable
0000000000001100 T Foo() 👈
-g
-D
-C
可以看到, Bar()
因為沒有加上 SHAREDLIB_EXPORT
, 所以不會被加到 symbol table 中
一種情況是, 如果 SharedLib 沒有使用到 StaticLib 的某些 symbols, linker 就會直接忽略他們, 當 Main 需要時就找不到了, 比如
src/lib/static_lib.cpp
int x = 10;
src/lib/shared_lib.cpp
extern int x; 👈
void Foo() {
// pass
};
void Bar() {
// pass
}
這邊我們在 SharedLib
並沒有使用 StaticLib 的 x
, 所以不會被加到 dynsym 中, 我們可以用 nm -gDC build/src/lib/libSharedLib.so
確認
w __cxa_finalize
w __gmon_start__
w _ITM_deregisterTMCloneTable
w _ITM_registerTMCloneTable
0000000000001110 T Bar()
0000000000001100 T Foo()
可以看到 SharedLib
只有 export 自己的 Bar()
和 Foo()
而已
但如果我們在 Foo()
中使用 x
, x
就會出現在 synsym 中了!
src/lib/shared_lib.cpp
extern int x;
void Foo() {
// pass
++x; 👈
};
void Bar() {
// pass
}
w __cxa_finalize
w __gmon_start__
w _ITM_deregisterTMCloneTable
w _ITM_registerTMCloneTable
0000000000004020 D x 👈
0000000000001110 T Bar()
0000000000001100 T Foo()
但我們想在 SharedLib
真的沒有使用到 x
的時候將他 export 出來, 要怎麼做呢?
這時可以設定 SharedLib 的 compile options, 加上 --whole-archive
, 告訴 linker 即使 SharedLib 沒有用到, 也要保留 StaticLib 的 symbols
就像 Day 10 提過的, gcc
和 clang
有不同的 options 語法, 所以我們要用 compiler-independent 的寫法, 將 "$<LINK_LIBRARY:WHOLE_ARCHIVE,StaticLib>" 加到 SharedLib link StaticLib 的指令中
並且, 需要讓 static_lib.cpp
也使用 SharedLib 產生的 sharedlib_export.h
src/lib/static_lib.cpp
#include "sharedlib_export.h"
SHAREDLIB_EXPORT int x = 10;
src/lib/CMakeLists.txt
set(CMAKE_CXX_VISIBILITY_PRESET hidden)
set(CMAKE_VISIBILITY_INLINES_HIDDEN TRUE)
add_library(StaticLib STATIC
static_lib.cpp
)
add_library(SharedLib SHARED
shared_lib.cpp
)
target_include_directories(SharedLib PUBLIC
${CMAKE_CURRENT_BINARY_DIR}
)
target_include_directories(StaticLib PUBLIC
${CMAKE_CURRENT_BINARY_DIR}
)
target_link_libraries(SharedLib PRIVATE
$<LINK_LIBRARY:WHOLE_ARCHIVE,StaticLib> 👈
)
include(GenerateExportHeader)
generate_export_header(SharedLib)
然後我們用 Day 17 提過的 objdump
來確認, 可以看到即使 SharedLib
沒有用到 StaticLib
的 symbols, 但 dynsym 仍包含了 StaticLib 的 symbols 了🎉🎉🎉
w __cxa_finalize
w __gmon_start__
w _ITM_deregisterTMCloneTable
w _ITM_registerTMCloneTable
0000000000004020 D x 👈
0000000000001100 T Foo()
這時候 Main 再 link SharedLib 就能找到想要的 symbols
由於下一篇原本要講的內容在 Day 15 就介紹過了, 所以會改成介紹一些原訂 Day 20 - 安裝 Project 才要講的內容