目錄

廣告 AD

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

好像一直都沒有認真研究過 Shared Library 的製作和使用

所以特地來寫了一篇作紀錄

廣告 AD

為了要做成 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 的 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];
}

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

Library 分成 static 和 shared 兩種,兩種不同的類別在匯出和匯入上有一些差異,我們透過不同的定義,之後的 function 就可以同時都用 MYLIB_API 來修飾每個函式或變數,以下就來慢慢說明。


Shared Library 在程式啟動時由作業系統載入到記憶體中,並由多個程式共享,當 EXE 使用 Shared Library 時,EXE 中只包含對這些函式庫的引用,而不是實際的程式碼。


如果要在 Windows 上製作 Shared Library,就一定要認識 dllexportdllimport,因為在 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。

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 在程式編譯和連結階段,程式碼就會直接複製到最終的 EXE 之中,一旦生成了 EXE,它就包含了所有必要的函式庫程式碼,不需要外部的函式庫檔案就能獨立運行,因此我們也不必特別處理匯出和匯入的問題,因此只要沒有定義 MYLIB_SHARED,就不額外添加任何東西。

#ifdef MYLIB_SHARED
    ...
#else
    #define MYLIB_API
#endif
  • MYLIB_SHARED:指定目前編譯目標是 Shared Library。

因為有用到 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;
};

由於 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;
}

為了在不同平台上都能夠編譯和建置,採用了 cmake 來負責建置編譯系統,編譯採用 C++ 17,並且同時編譯兩種類型 Static Library, 和 Shared Library。對於 Shared Library,我們要額外加上 MYLIB_EXPORTSMYLIB_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

產生出 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 都有被列在上面。


使用時,我們只需要 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


廣告 AD