iT邦幫忙

2023 iThome 鐵人賽

DAY 18
0
Software Development

30 天 CMake 跨平台之旅系列 第 18

[Day 18] 注意! Link dependencies

  • 分享至 

  • xImage
  •  

本日內容

  • 從這裡開始
  • 修改 Link Dependency
  • Symbol Visibility
  • Export Symbols
  • 我全都要!
  • 預告

Day 18 - Colab

今天會介紹 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 會長這樣
https://ithelp.ithome.com.tw/upload/images/20230918/20161950MsZne6S7nW.png

可以看到, 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 的變數時得到不一致的結果

那麼該怎麼辦呢?

修改 Link Dependency

比較好的做法是, 讓 SharedLib 提供 StaticLib 的 symbols, Main 就只需要 link SharedLib 就好, 所以我們可以把 Link Dependency 改成這樣

https://ithelp.ithome.com.tw/upload/images/20230918/201619501k3O0mG94n.png

這樣就可以避免上述問題發生, 但由於 StaticLib 的 symbols 現在只有 SharedLib 能看見, 我們還需要一些處理才能讓 Main 順利使用

Symbol Visibility

通常我們不想要把 SharedLib 所有的 symbols 都 export 出來, 這會讓最終產生的 executable 變太大, 降低執行速度
遺憾的是, 這是 gccclang 預設的行為
所以我們最好將該行為改掉, 變成預設是不 export 任何 symbols, 有需要用到再 export!

CMake 提供了 CMAKE_<LANG>_VISIBILITY_PRESETCMAKE_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()

Export Symbols

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>]
)
  • 要先 include GenerateExportHeader module
  • 此指令會幫我們產生 <target-name>_export.h header file
  • 該 header file 會定義 macro <target-name>_EXPORT, 根據 compiler 不同被 preprocess 成不同的 keyword, 比如 Visual Studio 是 __declspec(), gcc 則是 __attribute__()
  • 將該 macro 加在想 export 的 function, variable 或是 class 前就能 export 他們的 symbol 了

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
    • Show external symbols
  • -D
    • Show dynamic symbols
  • -C
    • Demangle symbol names

可以看到, 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 提過的, gccclang 有不同的 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 才要講的內容


上一篇
[Day 17] 什麼是 Symbols?
下一篇
[Day 19] Install Basics
系列文
30 天 CMake 跨平台之旅30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言