C++:如何打造一個動態連結函式庫

好像一直都沒有認真研究過 Shared Library 的製作和使用
所以特地來寫了一篇作紀錄
Backpack
為了要做成 library,寫了一個 Backpack 的 class
與一般的 Array 不一樣,它支援同時儲存各式種類的型態,只是要透過 pointer 的方式去儲存
也支援 random access 的方式做存取
#ifndef _BACKPACK_HPP
#define _BACKPACK_HPP
#include <cstddef>
#include <utility>
#include <vector>
namespace mylib
{
class Backpack
{
public:
Backpack(std::size_t max_size);
~Backpack();
void push(const std::byte *ptr, std::size_t size);
void pop();
std::pair<std::byte *, std::size_t> access(std::size_t idx) const;
std::size_t size() const;
private:
std::size_t max_size;
std::byte *data;
std::vector<std::size_t> prefix_sum;
};
}
#endif
#include <stdexcept>
#include <cstring>
#include "backpack.hpp"
namespace mylib
{
Backpack::Backpack(std::size_t max_size) : prefix_sum(1, 0)
{
this->max_size = max_size;
data = new std::byte[max_size];
}
Backpack::~Backpack()
{
if (data != nullptr)
{
delete[] data;
data = nullptr;
}
}
void Backpack::push(const std::byte *ptr, std::size_t size)
{
const size_t offset = prefix_sum.back();
if (offset + size >= max_size)
{
throw std::length_error("Exceed max size");
}
memcpy(data + offset, ptr, size);
prefix_sum.push_back(offset + size);
}
void Backpack::pop()
{
if (prefix_sum.size() == 1)
{
throw std::out_of_range("Backpack is empty");
}
prefix_sum.pop_back();
}
std::pair<std::byte *, std::size_t> Backpack::access(std::size_t idx) const
{
if (idx + 1 >= prefix_sum.size())
{
throw std::out_of_range("Index out of bound");
}
const std::size_t offset = prefix_sum[idx];
const std::size_t size = prefix_sum[idx + 1] - prefix_sum[idx];
return {data + offset, size};
}
std::size_t Backpack::size() const
{
return prefix_sum.size() - 1;
}
}
Library
接著我們準備 Library 的 header 和 cpp file
#ifndef _MYLIB_HPP
#define _MYLIB_HPP
#ifdef MYLIB_SHARED
#ifdef MYLIB_EXPORTS
#ifdef _WIN32
#define MYLIB_API __declspec(dllexport)
#else
#define MYLIB_API __attribute__((visibility("default")))
#endif
#else
#ifdef _WIN32
#define MYLIB_API __declspec(dllimport)
#else
#define MYLIB_API
#endif
#endif
#else
#define MYLIB_API
#endif
#ifdef __cplusplus
extern "C"
{
#endif
typedef struct BackpackHandle BackpackHandle;
typedef enum
{
MYLIB_SUCCESS = 0,
MYLIB_OUT_OF_BOUND,
MYLIB_EMPTY,
MYLIB_EXCEED_MAX_SIZE,
MYLIB_UNKNOWN_ERROR,
MYLIB_NULL_POINTER,
} MyLibErrorCode;
MYLIB_API BackpackHandle *create_backpack(const size_t max_size);
MYLIB_API void destroy_backpack(BackpackHandle *handle);
MYLIB_API MyLibErrorCode add_item(const BackpackHandle *handle, const void *buf, size_t size);
MYLIB_API MyLibErrorCode remove_last_item(const BackpackHandle *handle);
MYLIB_API MyLibErrorCode get_item_size(const BackpackHandle *handle, const size_t idx, size_t *size);
MYLIB_API MyLibErrorCode get_item(const BackpackHandle *handle, const size_t idx, void *buf);
MYLIB_API MyLibErrorCode get_size(const BackpackHandle *handle, size_t *size);
MYLIB_API const char *get_error_string(MyLibErrorCode code);
#ifdef __cplusplus
}
#endif
#endif
#include <stdexcept>
#include <vector>
#include <cstring>
#include "backpack.hpp"
#include "mylib.h"
struct BackpackHandle
{
mylib::Backpack *backpack;
};
BackpackHandle *create_backpack(const size_t max_size)
{
mylib::Backpack *backpack;
try
{
backpack = new mylib::Backpack(max_size);
}
catch (...)
{
return nullptr;
}
auto handle = new BackpackHandle();
handle->backpack = backpack;
return handle;
}
void destroy_backpack(BackpackHandle *handle)
{
if (handle != nullptr)
{
delete handle->backpack;
delete handle;
}
}
MyLibErrorCode add_item(const BackpackHandle *handle, const void *buf, size_t size)
{
if (handle == nullptr)
{
return MyLibErrorCode::MYLIB_NULL_POINTER;
}
try
{
handle->backpack->push(reinterpret_cast<const std::byte *>(buf), size);
}
catch (std::length_error &e)
{
return MyLibErrorCode::MYLIB_EXCEED_MAX_SIZE;
}
catch (...)
{
return MyLibErrorCode::MYLIB_UNKNOWN_ERROR;
}
return MyLibErrorCode::MYLIB_SUCCESS;
}
MyLibErrorCode remove_last_item(const BackpackHandle *handle)
{
if (handle == nullptr)
{
return MyLibErrorCode::MYLIB_NULL_POINTER;
}
try
{
handle->backpack->pop();
}
catch (std::out_of_range &e)
{
return MyLibErrorCode::MYLIB_EMPTY;
}
catch (...)
{
return MyLibErrorCode::MYLIB_UNKNOWN_ERROR;
}
return MyLibErrorCode::MYLIB_SUCCESS;
}
MyLibErrorCode get_item_size(const BackpackHandle *handle, const size_t idx, size_t *size)
{
if (handle == nullptr)
{
return MyLibErrorCode::MYLIB_NULL_POINTER;
}
try
{
auto r = handle->backpack->access(idx);
*size = r.second;
}
catch (std::out_of_range &e)
{
return MyLibErrorCode::MYLIB_OUT_OF_BOUND;
}
catch (...)
{
return MyLibErrorCode::MYLIB_UNKNOWN_ERROR;
}
return MyLibErrorCode::MYLIB_SUCCESS;
}
MyLibErrorCode get_item(const BackpackHandle *handle, const size_t idx, void *buf)
{
if (handle == nullptr)
{
return MyLibErrorCode::MYLIB_NULL_POINTER;
}
try
{
auto r = handle->backpack->access(idx);
memcpy(buf, r.first, r.second);
}
catch (std::out_of_range &e)
{
return MyLibErrorCode::MYLIB_OUT_OF_BOUND;
}
catch (...)
{
return MyLibErrorCode::MYLIB_UNKNOWN_ERROR;
}
return MyLibErrorCode::MYLIB_SUCCESS;
}
MyLibErrorCode get_size(const BackpackHandle *handle, size_t *s)
{
if (handle == nullptr)
{
return MyLibErrorCode::MYLIB_NULL_POINTER;
}
try
{
*s = handle->backpack->size();
}
catch (...)
{
return MyLibErrorCode::MYLIB_UNKNOWN_ERROR;
}
return MyLibErrorCode::MYLIB_SUCCESS;
}
const char *get_error_string(MyLibErrorCode code)
{
static std::vector<const char *> error_string_arr = {
"",
"Out of bound",
"Empty",
"Exceed max size",
"Unknown error",
"Null pointer"};
if (code == MyLibErrorCode::MYLIB_SUCCESS)
{
return "";
}
else if (static_cast<std::size_t>(code) >= error_string_arr.size())
{
return "";
}
return error_string_arr[code];
}
Name Mangling
Compiler 會依照固定的規則給予每個 identifier 一個名稱,之後 linker 就可以依照這個名稱找到對應的 identifier,這稱作 Name Mangling,這很好地解決了 overloading 的問題,即使在 Code 中名稱相同的 overloading function 在處理過後,會得到不一樣的名稱,而 C 和 C++ 各自擁有不同的命名規則。
由於 Backpack 是由 C++ 開發的,如果要兼容 C,會因為有命名規則不同導致 Compiler 找不到 function,所以為了同時兼容 C,當 Compiler 為 C++ Compiler 時,加上 extern C
來告知 C++ Compiler 使用 C 語言的命名規則。
#ifdef __cplusplus
extern "C"
{
#endif
...
#ifdef __cplusplus
}
#endif
dllexport / dllimport
Library 分成 static 和 shared 兩種,兩種不同的類別在匯出和匯入上有一些差異,我們透過不同的定義,之後的 function 就可以同時都用 MYLIB_API
來修飾每個函式或變數,以下就來慢慢說明。
Shared Library
Shared Library 在程式啟動時由作業系統載入到記憶體中,並由多個程式共享,當 EXE 使用 Shared Library 時,EXE 中只包含對這些函式庫的引用,而不是實際的程式碼。
Export
如果要在 Windows 上製作 Shared Library,就一定要認識 dllexport
和 dllimport
,因為在 Windows 上,符號預設是隱藏的,符號可能是函式或變數,也就是其他的程式無法使用這些函式或變數,因此我們需要加上 __declspec(dllexport)
來告知 Compiler 和 Linker,讓它們將這些函式或變數匯出,紀錄這些函式或變數的位置,來讓其他程式可以使用。
而在 Linux 上,由於符號預設是可見的,因此不需要額外做處理,但為了避免符號衝突和減少 Shared Library 的大小,我們會修改預設成隱藏 (-fvisibility=hidden
),並透過 __attribute__((visibility("default")))
讓 Compiler 只匯出該符號。
#ifdef MYLIB_SHARED
#ifdef MYLIB_EXPORTS
#ifdef _WIN32
#define MYLIB_API __declspec(dllexport)
#else
#define MYLIB_API __attribute__((visibility("default")))
#endif
#else
...
#endif
#else
...
#endif
MYLIB_SHARED
:指定目前編譯目標是 Shared Library。MYLIB_EXPORTS
:是不是在編譯 Shared Library,還是使用 Shared Library。_WIN32
:當前平台是不是 Windows。
Import
Windows 上匯入需要透過 __declspec(dllimport)
來告至 Compiler,透過 Shared Library 來匯入這些符號,而在 Linux 上就不需要額外的指示了。
#ifdef MYLIB_SHARED
#ifdef MYLIB_EXPORTS
...
#else
#ifdef _WIN32
#define MYLIB_API __declspec(dllimport)
#else
#define MYLIB_API
#endif
#endif
#else
...
#endif
MYLIB_SHARED
:指定目前編譯目標是 Shared Library。MYLIB_EXPORTS
:是不是在編譯 Shared Library,還是使用 Shared Library。_WIN32
:當前平台是不是 Windows。
Static Library
Static Library 在程式編譯和連結階段,程式碼就會直接複製到最終的 EXE 之中,一旦生成了 EXE,它就包含了所有必要的函式庫程式碼,不需要外部的函式庫檔案就能獨立運行,因此我們也不必特別處理匯出和匯入的問題,因此只要沒有定義 MYLIB_SHARED
,就不額外添加任何東西。
#ifdef MYLIB_SHARED
...
#else
#define MYLIB_API
#endif
MYLIB_SHARED
:指定目前編譯目標是 Shared Library。
Opaque Pointer
因為有用到 class,而 class 在 C 中是不支持的,我們可以透過建立一個不完整的型態 (Handle),並在該型態中放置一個指向 class 的指標,如此一來使用者只知道這是一個 pointer 指向該型態,而不知道裡面的實作是什麼,自然也不會遇到 class 的問題,這樣可以兼容 C,也可以將實作隱藏起來。
使用者看的到 mylib.h
,但他只知道這是一個型態,可以透過這個型態可以和 library 的 function 互動,但不知道裡面有什麼,自然就不能存取。
typedef struct BackpackHandle BackpackHandle;
MYLIB_API BackpackHandle *xxxxx(...);
定義我們就放在 mylib.cpp
中,但使用者看不到 mylib.cpp
,因為被打包成 Library 了。
struct BackpackHandle
{
mylib::Backpack *backpack;
};
Exception
由於 C++ 內,我們習慣使用 throw
+ try
+ catch
來處理錯誤,但 C 並沒有這些東西,為了相容於 C,我們在 mylib.cpp
透過 try
+ catch
來處理 C++ 的 exception,並轉 Error Code 回傳給使用者。
typedef enum
{
MYLIB_SUCCESS = 0,
MYLIB_OUT_OF_BOUND,
MYLIB_EMPTY,
MYLIB_EXCEED_MAX_SIZE,
MYLIB_UNKNOWN_ERROR,
MYLIB_NULL_POINTER,
} MyLibErrorCode;
MyLibErrorCode xxxxx(...)
{
try
{
...
}
catch (...)
{
return MyLibErrorCode::MYLIB_UNKNOWN_ERROR;
}
return MyLibErrorCode::MYLIB_SUCCESS;
}
Build
為了在不同平台上都能夠編譯和建置,採用了 cmake
來負責建置編譯系統,編譯採用 C++ 17,並且同時編譯兩種類型 Static Library, 和 Shared Library。對於 Shared Library,我們要額外加上 MYLIB_EXPORTS
和 MYLIB_SHARED
,而 Static Library 只需要 MYLIB_EXPORTS
。在 Unix 和 Apple 平台上,我們要修改預設的可見設定,透過 -fvisibility=hidden
將預設設定改為隱藏。
cmake_minimum_required(VERSION 3.14)
project(MyLib CXX)
set(CMAKE_CXX_STANDARD 17)
add_library(mylibStatic STATIC mylib.cpp mylib.h backpack.cpp backpack.hpp)
add_library(mylibShared SHARED mylib.cpp backpack.cpp)
target_compile_definitions(mylibStatic PRIVATE MYLIB_EXPORTS)
target_compile_definitions(mylibShared PRIVATE MYLIB_EXPORTS)
target_compile_definitions(mylibShared PRIVATE MYLIB_SHARED)
if(UNIX OR APPLE)
target_compile_options(mylibShared PRIVATE -fvisibility=hidden)
endif()
set_target_properties(mylibStatic PROPERTIES
OUTPUT_NAME mylibStatic
)
set_target_properties(mylibShared PROPERTIES
OUTPUT_NAME mylib
)
目前專案檔案架構為如下:
.
├── mylib.h
├── mylib.cpp
├── backpack.hpp
├── backpack.cpp
└── CMakeLists.txt
接下來就可以開始編譯了
cmake -B build -S .
cmake --build build --config Release
最後檔案會在以下位置:
- Shared:
build\lib\shared\Release\mylib.dll
- Static:
build\lib\static\Release\mylibStatic.lib
cmake -B build -S . -DCMAKE_BUILD_TYPE=Release
cmake --build build
最後檔案會在以下位置:
- Shared:
build/lib/shared/libmylib.so
- Static:
build/lib/static/libmylibStatic.a
不同作業系統下,不同類型的 Library 的副檔名也不同:
OS | Shared Library | Static Library |
---|---|---|
Windows | .dll | .lib |
Linux | .so | .a |
macOS | .dylib | .a |
Symbol
產生出 Shared Library 後,我們來看看我們到底有沒有成功輸出到 Shared Library 上吧!
開啟 Developer Command Prompt for VS 2022,並輸入以下的指令 (mylib.dll) 可以自行替換路徑到 .dll 檔案。
dumpbin /EXPORTS mylib.dll
File Type: DLL
Section contains the following exports for mylib.dll
00000000 characteristics
FFFFFFFF time date stamp
0.00 version
1 ordinal base
8 number of functions
8 number of names
ordinal hint RVA name
1 0 00001210 add_item
2 1 00001250 create_backpack
3 2 000012A0 destroy_backpack
4 3 000012F0 get_error_string
5 4 00001430 get_item
6 5 00001490 get_item_size
7 6 000014E0 get_size
8 7 00001520 remove_last_item
Summary
1000 .data
1000 .pdata
2000 .rdata
1000 .reloc
1000 .rsrc
2000 .text
我們可以看到 Library 提供的 function 都有被列在上面。
執行以下指令:
nm -D libmylib.so
w _ITM_deregisterTMCloneTable
w _ITM_registerTMCloneTable
U _Unwind_Resume@GCC_3.0
U _ZNSt12length_errorC1EPKc@GLIBCXX_3.4.21
U _ZNSt12length_errorD1Ev@GLIBCXX_3.4
U _ZNSt12out_of_rangeC1EPKc@GLIBCXX_3.4.21
U _ZNSt12out_of_rangeD1Ev@GLIBCXX_3.4
0000000000001930 W _ZNSt6vectorIPKcSaIS1_EED1Ev
0000000000001930 W _ZNSt6vectorIPKcSaIS1_EED2Ev
0000000000001b30 W _ZNSt6vectorImSaImEE17_M_realloc_insertIJmEEEvN9__gnu_cxx17__normal_iteratorIPmS1_EEDpOT_
U _ZSt20__throw_length_errorPKc@GLIBCXX_3.4
U _ZTISt12length_error@GLIBCXX_3.4
U _ZTISt12out_of_range@GLIBCXX_3.4
U _ZdaPv@GLIBCXX_3.4
U _ZdlPvm@CXXABI_1.3.9
U _Znam@GLIBCXX_3.4
U _Znwm@GLIBCXX_3.4
U __cxa_allocate_exception@CXXABI_1.3
U __cxa_atexit@GLIBC_2.2.5
U __cxa_begin_catch@CXXABI_1.3
U __cxa_end_catch@CXXABI_1.3
w __cxa_finalize@GLIBC_2.2.5
U __cxa_free_exception@CXXABI_1.3
U __cxa_guard_abort@CXXABI_1.3
U __cxa_guard_acquire@CXXABI_1.3
U __cxa_guard_release@CXXABI_1.3
U __cxa_throw@CXXABI_1.3
w __gmon_start__
U __gxx_personality_v0@CXXABI_1.3
U __stack_chk_fail@GLIBC_2.4
0000000000001670 T add_item
00000000000015c0 T create_backpack
0000000000001620 T destroy_backpack
00000000000017b0 T get_error_string
0000000000001730 T get_item
00000000000016f0 T get_item_size
0000000000001770 T get_size
U memcpy@GLIBC_2.14
U memmove@GLIBC_2.2.5
00000000000016b0 T remove_last_item
我們可以看到 Library 提供的 function 都有被列在上面。
Usage
使用時,我們只需要 mylib.h
和 Shared Library 或是 Static Library 就好了
也寫了一個簡單的 main.c
#include <stdio.h>
#include <stdlib.h>
#include "mylib.h"
#define CHECK(call) \
do \
{ \
MyLibErrorCode err = (call); \
if (err != MYLIB_SUCCESS) \
{ \
fprintf(stderr, "Error at %s:%d: %s\n", __FILE__, __LINE__, get_error_string(err)); \
exit(EXIT_FAILURE); \
} \
} while (0)
int main()
{
BackpackHandle *handle = create_backpack(1000); // 1000 bytes
if (handle == NULL)
{
fprintf(stderr, "Create Failed\n");
exit(EXIT_FAILURE);
}
int a = 10;
long long b = 100000000000LL;
char c = 'c';
printf("Item 0: %d\n", a);
printf("Item 1: %lld\n", b);
printf("Item 2: %c\n", c);
CHECK(add_item(handle, &a, sizeof(a)));
CHECK(add_item(handle, &b, sizeof(b)));
CHECK(add_item(handle, &c, sizeof(c)));
for (int i = 0; i < 3; ++i)
{
size_t s;
CHECK(get_item_size(handle, i, &s));
printf("Item %d size: %zu\n", i, s);
}
int a_get;
long long b_get;
char c_get;
CHECK(get_item(handle, 0, &a_get));
CHECK(get_item(handle, 1, &b_get));
CHECK(get_item(handle, 2, &c_get));
printf("[Get] Item 0: %d\n", a_get);
printf("[Get] Item 1: %lld\n", b_get);
printf("[Get] Item 2: %c\n", c_get);
for (int i = 0; i < 3; ++i)
{
size_t s;
CHECK(get_size(handle, &s));
printf("Total size: %zu\n", s);
remove_last_item(handle);
}
destroy_backpack(handle);
}
Shared
cmake_minimum_required(VERSION 3.14)
project(MyLib C)
add_executable(test_mylib main.c)
target_link_directories(test_mylib PRIVATE "/path/to/mylib/folder")
target_link_libraries(test_mylib PRIVATE mylib)
target_include_directories(test_mylib PRIVATE "/path/to/header/folder")
Static
cmake_minimum_required(VERSION 3.14)
project(MyLib C)
add_executable(test_mylib main.c)
target_link_directories(test_mylib PRIVATE "/path/to/mylibStatic/folder")
target_link_libraries(test_mylib PRIVATE mylibStatic)
target_include_directories(test_mylib PRIVATE "/path/to/header/folder")
Shared
cmake_minimum_required(VERSION 3.14)
project(MyLib C)
add_executable(test_mylib main.c)
target_link_directories(test_mylib PRIVATE "/path/to/mylib/folder")
target_link_libraries(test_mylib PRIVATE mylib)
target_include_directories(test_mylib PRIVATE "/path/to/header/folder")
Static
cmake_minimum_required(VERSION 3.14)
project(MyLib C)
add_executable(test_mylib main.c)
target_link_directories(test_mylib PRIVATE "/path/to/mylibStatic/folder")
target_link_libraries(test_mylib PRIVATE mylibStatic stdc++)
target_include_directories(test_mylib PRIVATE "/path/to/header/folder")
/path/to/mylib/folder
: 更換成 mylib 的 Library 所在的資料夾/path/to/header/folder
: 更換成mylib.h
的 Header 所在的資料夾
接著就可以編譯啦 ~
cmake -B build -S .
cmake --build build --config Release
最後檔案會在 build\Release\test_mylib.exe
,記得如果是用 Shared Library 的話,要把 .dll 跟 test_mylib.exe
放在一起喔 ~
cmake -B build -S . -DCMAKE_BUILD_TYPE=Release
cmake --build build
最後檔案會在 build/test_mylib
。
輸出長這樣:
Item 0: 10
Item 1: 100000000000
Item 2: c
Item 0 size: 4
Item 1 size: 8
Item 2 size: 1
[Get] Item 0: 10
[Get] Item 1: 100000000000
[Get] Item 2: c
Total size: 3
Total size: 2
Total size: 1
Reference
如果你覺得這篇文章有用 可以考慮贊助飲料給大貓咪