目錄

廣告 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 的副檔名也不同:

OSShared LibraryStatic 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