日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > windows >内容正文

windows

C++ 动态库热加载

發(fā)布時間:2024/1/5 windows 35 coder
生活随笔 收集整理的這篇文章主要介紹了 C++ 动态库热加载 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

C++ 動態(tài)庫熱加載

本文參考自 project-based-learning 中的 Build a Live Code-reloader Library for C++,主要內(nèi)容都來自于其中,但是對代碼進行了一點修改,并且改用 CMake 進行構(gòu)建。

文章整體比較基礎(chǔ),適合初學者,通過本文可以學習到以下知識點

  1. 關(guān)于 C++ 程序如何編譯運行,如何運行時加載動態(tài)庫(使用 dl* API)。
  2. 如何設(shè)計簡潔易用的庫 API 供用戶使用。
  3. 如何使用 CMake 組織并構(gòu)建一個包含可執(zhí)行程序、動態(tài)庫和頭文件庫的項目。
  4. 如何使用 GoogleTest 進行測試。

動態(tài)庫熱加載原理

動態(tài)庫熱加載指的是在程序運行時,動態(tài)地加載動態(tài)庫,從而達到不停止程序的情況下,更新程序的功能。

C++ 程序在運行時有兩種方式加載動態(tài)連接庫:隱式鏈接和顯式鏈接 [1]

  1. 隱式鏈接就是在編譯的時候使用 -l 參數(shù)鏈接的動態(tài)庫,進程在開始執(zhí)行時就將動態(tài)庫文件映射到內(nèi)存空間中。
  2. 顯式鏈接使用 libdl.so 庫的 API 接口在運行中加載和卸載動態(tài)庫,主要的 API 有 dlopen、dlclose、dlsym、dlerror。

隱式鏈接的方式要進行熱加載需要不少 Hack,難度較大,本文主要講解第二種方式。

簡單版本

首先我們快速實現(xiàn)一個能夠完成最小功能可運行的版本,熟悉相關(guān) API 的使用。我們簡單編寫三個文件,分別為main.cppreplex.h,hello.cpp,另外還編寫一個快速編譯運行代碼的腳本 run.sh,目錄結(jié)構(gòu)如下

.
├── hello.cpp
├── main.cpp
├── replex.h
└── run.sh

代碼的完整版本見 projects/replex-1。

replex.h 中對 dl* API 進行了簡單的封裝,使用一個 namespace 將 API 進行了包裝,代碼如下

#pragma once

#include <dlfcn.h>

#include <cstdio>

namespace Replex {

inline void* Load(const char* filepath) {
    return dlopen(filepath, RTLD_LAZY);
}

inline void* LoadSymbol(void* library, const char* symbol) {
    return dlsym(library, symbol);
}

inline void Reload(void*& library, const char* filepath) {
    if (library) {
        dlclose(library);
    }
    library = Load(filepath);
}

inline void PrintError() {
    fprintf(stderr, "%s\n", dlerror());
}

}  // namespace Replex

hello.cpp 是我們需要熱加載的動態(tài)庫,代碼如下

#include <cstdio>

extern "C" {
void foo() {
    printf("Hi\n");
}

int bar = 200;
}

其中使用 extern "C"foobar 聲明為 C 語言的函數(shù)和變量,這樣在編譯時就不會對函數(shù)名進行修飾,否則在 main.cpp 中使用 dlsym 時會找不到 foo 對應的符號。

不加 extern "C"時,使用 nm 命令查看 hello.so 中的符號如下

$ nm libhello.so  | grep foo
0000000000001119 T _Z3foov

加上后

$ nm libhello.so  | grep foo
0000000000001119 T foo

main.cpp 是主程序,代碼如下

#include <cstdio>
#include <string>

#include "replex.h"

const char* g_libPath = "libhello.so";

int main() {
    void* handle;
    void (*foo)();
    int bar;

    handle = Replex::Load(g_libPath);
    if (!handle) {
        Replex::PrintError();
        return -1;
    }
    foo = reinterpret_cast<void (*)()>(Replex::LoadSymbol(handle, "foo"));
    foo();
    bar = *reinterpret_cast<int*>(Replex::LoadSymbol(handle, "bar"));
    printf("bar == %d\n", bar);

    // Modify the source code and recompile the library.
    std::string filename = "hello.cpp";
    std::string command = std::string("sed -i ") +
                          (bar == 200 ? "'s/200/300/'" : "'s/300/200/'") + " " +
                          filename;
    system(command.c_str());
    command = std::string("sed -i ") +
              (bar == 200 ? "'s/Hi/Hello/'" : "'s/Hello/Hi/'") + " " + filename;
    system(command.c_str());
    system("g++ -shared -fPIC -o libhello.so hello.cpp");

    Replex::Reload(handle, g_libPath);
    if (!handle) {
        Replex::PrintError();
        return -1;
    }
    foo = reinterpret_cast<void (*)()>(Replex::LoadSymbol(handle, "foo"));
    foo();
    bar = *reinterpret_cast<int*>(Replex::LoadSymbol(handle, "bar"));
    printf("bar == %d\n", bar);

    return 0;
}

整體代碼邏輯比較好懂,首先加載動態(tài)庫,然后獲取動態(tài)庫中的函數(shù)和變量,調(diào)用函數(shù)和打印變量,然后修改 hello.cpp 中的代碼,重新編譯動態(tài)庫,再次加載動態(tài)庫,調(diào)用函數(shù)和打印變量。

reinterpret_cast 是 C++ 中的強制類型轉(zhuǎn)換,將 void* 指針轉(zhuǎn)換為函數(shù)指針和變量指針。

run.sh 的內(nèi)容如下

#!/bin/bash
set -e # stop the script on errors
g++ -fPIC -shared -o libhello.so hello.cpp
g++ -o main.out main.cpp -ldl
./main.out

腳本中 -shared -fPIC 參數(shù)用于生成位置無關(guān)的動態(tài)庫,-ldl 參數(shù)用于鏈接 libdl.so 庫(dl* API),-o 參數(shù)用于指定輸出文件名。

運行腳本后,輸出如下

Hi
bar == 200
Hello
bar == 300

當前程序能夠完成基本功能,但是對于使用者來說我們的庫不夠好用,使用者(main.cpp)需要自己定義相應的函數(shù)指針和類型,還需要自己進行類型轉(zhuǎn)換,動態(tài)庫的導出符號也需要自己定義,對于使用者來說也相當麻煩。

改進版本

我們考慮提供更簡單的接口供用戶使用,我們將在 replex.h 中創(chuàng)建一個 ReplexModule 類,這個類將用于給動態(tài)庫的繼承使用,然后由動態(tài)庫的作者提供更加簡明的接口供用戶使用。

這一版本代碼的完整實現(xiàn)見 GitHub。

最終的使用效果見如下 main.cpp 文件

#include <iostream>

#include "hello.h"

int main() {
    HelloModule::LoadLibrary();
    HelloModule::Foo();
    int bar = HelloModule::GetBar();
    std::cout << "bar == " << bar << std::endl;

    // Modify the source code and recompile the library.
    // ...

    HelloModule::ReloadLibrary();
    HelloModule::Foo();
    std::cout << "bar == " << HelloModule::GetBar() << std::endl;
    return 0;
}

我們忽略中間的修改源碼和重新編譯的過程,這里只關(guān)注 HelloModule 的使用,相比于前一版本,這里的使用更加簡單,不需要自己定義函數(shù)指針和變量,也不需要自己進行類型轉(zhuǎn)換,只需要調(diào)用 HelloModule 中的接口即可。同時注意到我們包含的頭文件也變成了 hello.h,這個頭文件是動態(tài)庫作者提供的,我們在 main.cpp 中只需要包含這個頭文件即可。

針對于上述需求,ReplexModule 需要公開兩個公共接口,一個用于發(fā)布可熱加載庫,另一個用于加載和重新加載這些可熱加載庫。

ReplexModule 的公開接口僅有兩個,分別為 LoadLibraryReloadLibrary,代碼如下

#pragma once

#include <dlfcn.h>

#include <array>
#include <iostream>
#include <stdexcept>
#include <string>
#include <unordered_map>

template <typename E, size_t NumSymbols>
class ReplexModule {
   public:
    static void LoadLibrary() { GetInstance().Load(); }
    static void ReloadLibrary() { GetInstance().Reload(); }

   protected:
    static E& GetInstance() {
        static E instance;
        return instance;
    }

    // ...
    // ... continued later
}

這兩個函數(shù)都依賴于 GetInstance 函數(shù),這個函數(shù)是一個模板函數(shù),用于返回 ReplexModule 的子類的單例,這樣可以保證每個子類只有一個實例。另外,ReplexModule 是一個模板類,模板參數(shù) E 是一個枚舉類型,用于指定動態(tài)庫中的符號,NumSymbols 是一個常量,用于指定動態(tài)庫中的符號個數(shù)。

接下來關(guān)注 ReplexModule 向動態(tài)庫作者也就是集成該類的子類提供的接口,代碼如下:

    // ... continued above

    // Should return the path to the library on disk
    virtual const char* GetPath() const = 0;

    // Should return a reference to an array of C-strings of size NumSymbols
    // Used when loading or reloading the library to lookup the address of
    // all exported symbols
    virtual std::array<const char*, NumSymbols>& GetSymbolNames() const = 0;

    template <typename Ret, typename... Args>
    Ret Execute(const char* name, Args... args) {
        // Lookup the function address
        auto symbol = m_symbols.find(name);
        if (symbol != m_symbols.end()) {
            // Cast the address to the appropriate function type and call it,
            // forwarding all arguments
            return reinterpret_cast<Ret (*)(Args...)>(symbol->second)(args...);
        }
        throw std::runtime_error(std::string("Function not found: ") + name);
    }

    template <typename T>
    T* GetVar(const char* name) {
        auto symbol = m_symbols.find(name);
        if (symbol != m_symbols.end()) {
            return static_cast<T*>(symbol->second);
        }
        // We didn't find the variable. Return an empty pointer
        return nullptr;
    }

   private:
    void Load() {
        m_libHandle = dlopen(GetPath(), RTLD_NOW);
        LoadSymbols();
    }

    void Reload() {
        auto ret = dlclose(m_libHandle);
        m_symbols.clear();
        Load();
    }

    void LoadSymbols() {
        for (const char* symbol : GetSymbolNames()) {
            auto* sym = dlsym(m_libHandle, symbol);
            m_symbols[symbol] = sym;
        }
    }

    void* m_libHandle;
    std::unordered_map<std::string, void*> m_symbols;
};

首先關(guān)注最底部的數(shù)據(jù)成員,m_libHandle 是動態(tài)庫的句柄,m_symbols 是一個哈希表,用于存儲動態(tài)庫中的符號和符號對應的地址。 Load 函數(shù)用于加載動態(tài)庫,Reload 函數(shù)用于重新加載動態(tài)庫,LoadSymbols 函數(shù)用于加載動態(tài)庫中的符號,這幾個函數(shù)的邏輯相當清晰無需贅述。

值得講解的是 ExecuteGetVar 函數(shù),Execute 函數(shù)用于調(diào)用動態(tài)庫中的函數(shù),GetVar 函數(shù)用于獲取動態(tài)庫中的變量,讓我們先看看 Execute 函數(shù)的實現(xiàn),代碼如下

    template <typename Ret, typename... Args>
    Ret Execute(const char* name, Args... args) {
        // Lookup the function address
        auto symbol = m_symbols.find(name);
        if (symbol != m_symbols.end()) {
            // Cast the address to the appropriate function type and call it,
            // forwarding all arguments
            return reinterpret_cast<Ret (*)(Args...)>(symbol->second)(args...);
        }
        throw std::runtime_error(std::string("Function not found: ") + name);
    }

這是一個模板函數(shù),模板參數(shù) Ret 是返回值類型,Args... 是參數(shù)類型,這里的 Args... 表示可以接受任意多個參數(shù),Args... args 表示將參數(shù)包 args 展開,然后將展開后的參數(shù)作為參數(shù)傳遞給 Execute 函數(shù)。

該函數(shù)首先在 m_symbols 中查找 name 對應的符號,如果找到了,就將符號地址轉(zhuǎn)換為類型為 Ret (*)(Args...) 的函數(shù)指針,然后調(diào)用該函數(shù),傳遞參數(shù) args...,如果沒有找到,就拋出異常。

GetVar 函數(shù)的實現(xiàn)如下

    template <typename T>
    T* GetVar(const char* name) {
        auto symbol = m_symbols.find(name);
        if (symbol != m_symbols.end()) {
            return static_cast<T*>(symbol->second);
        }
        // We didn't find the variable. Return an empty pointer
        return nullptr;
    }

該函數(shù)的實現(xiàn)和 Execute 函數(shù)類似,只是將函數(shù)指針轉(zhuǎn)換為變量指針,然后返回。

hello.cpp 的內(nèi)容保持不變:

#include <cstdio>

extern "C" {
void foo() {
    printf("Hi\n");
}

int bar = 200;
}

hello.h 中定義類 HelloModule 繼承自 ReplexModule,代碼如下

#pragma once
#include <array>

#include "replex.h"

inline std::array<const char*, 2> g_exports = {"foo", "bar"};

class HelloModule : public ReplexModule<HelloModule, g_exports.size()> {
   public:
    static void Foo() { GetInstance().Execute<void>("foo"); }

    static int GetBar() { return *GetInstance().GetVar<int>("bar"); }

   protected:
    virtual const char* GetPath() const override { return "libhello.so"; }

    virtual std::array<const char*, g_exports.size()>& GetSymbolNames()
        const override {
        return g_exports;
    }
};

變量 g_exports 用于存儲動態(tài)庫中需要導出的符號,其采用 inline 修飾,這樣就可以在頭文件中定義,而不會出現(xiàn)重復定義的錯誤。

HelloModule 中定義了兩個靜態(tài)函數(shù),分別為 FooGetBar,這兩個函數(shù)用于調(diào)用動態(tài)庫中的函數(shù)和獲取動態(tài)庫中的變量。

運行腳本的內(nèi)容基本不變,添加了 -std=c++17 的標志保證可以使用 inline 變量的用法。

#!/bin/bash
set -e # stop the script on errors
g++ -fPIC -shared -o libhello.so hello.cpp -std=c++17
g++ -o main.out main.cpp -ldl -std=c++17
./main.out

運行效果與前一版本一致,如下

Hi
bar == 200
Hello
bar == 300

現(xiàn)在我們可以認為我們所編寫的 replex.h 庫足方便使用,動態(tài)庫作者只需要繼承 ReplexModule 類,然后實現(xiàn)兩個虛函數(shù)即可,使用者只需要包含動態(tài)庫作者提供的頭文件,然后調(diào)用相應的接口即可。

CMake 版本

前面兩個版本的代碼都是寫個腳本直接使用 g++ 編譯,這樣的方式不夠靈活,不利于項目的管理,正好這個項目涉及到幾個不同的模塊,可以嘗試使用 CMake 進行管理,學習一下項目的組織構(gòu)建。

完整代碼見 projects/replex-3,采用 現(xiàn)代 CMake 模塊化項目管理指南 中推薦的方式進行項目組織,但是略微進行了一點簡化,目錄結(jié)構(gòu)如下

.
├── CMakeLists.txt
├── hello
│   ├── CMakeLists.txt
│   ├── include
│   │   └── hello.h
│   └── src
│       └── hello.cpp
├── main
│   ├── CMakeLists.txt
│   └── src
│       └── main.cpp
└── replex
    ├── CMakeLists.txt
    └── include
        └── replex.h

首先梳理一下整個項目的依賴關(guān)系,如下所示

main (exe)
├── hello_interface (interface)
│   └── replex (interface)
└── hello (shared lib)

main 模塊依賴于頭文件庫 hello_interface,hello_interface 依賴于頭文件庫 replex,動態(tài)庫 hello 不依賴于任何庫,用于提供給 main 模塊使用。

CMakeLists.txt 為根目錄的 CMakeLists.txt,內(nèi)容如下

cmake_minimum_required(VERSION 3.15)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

project(replex LANGUAGES CXX)

if (NOT CMAKE_BUILD_TYPE)
    set(CMAKE_BUILD_TYPE Release)
endif ()

add_subdirectory(replex)
add_subdirectory(main)
add_subdirectory(hello)

首先設(shè)置 C++ 標準,然后設(shè)置項目名稱,然后判斷是否設(shè)置了構(gòu)建類型,如果沒有設(shè)置,則設(shè)置為 Release 模式,然后添加子目錄,分別為 replex、main 和 hello。

replex/CMakeLists.txt 的內(nèi)容如下

add_library(replex INTERFACE include/replex.h)
target_include_directories(replex INTERFACE include)

replex 為頭文件庫,使用 add_library 添加,類型為 INTERFACE,表示這是一個接口庫,不會生成任何文件,只會導出頭文件,使用 target_include_directories 添加頭文件路徑。

hello/CMakeLists.txt 的內(nèi)容如下

add_library(hello SHARED src/hello.cpp)

add_library(hello_interface INTERFACE include/hello.h)
target_include_directories(hello_interface INTERFACE include)
target_link_libraries(hello_interface INTERFACE replex)

其中定義了兩個庫,一個為動態(tài)庫 hello,一個為頭文件庫 hello_interface 用于導出 動態(tài)庫 hello 中的符號以供使用, hello_interface 依賴于 replex,使用 target_link_libraries 添加依賴。

main/CMakeLists.txt 的內(nèi)容如下

add_executable(main src/main.cpp)
target_link_libraries(main PRIVATE hello_interface)

main 為可執(zhí)行文件,使用 add_executable 添加,使用 target_link_libraries 添加依賴 hello_interface。

最后運行腳本 run.sh,內(nèi)容如下

#!/bin/bash
set -e # stop the script on errors
cmake -B build
cmake --build build
./build/main/main

運行的效果如下

Hi
bar == 200
[  0%] Built target replex
[  0%] Built target hello_interface
[ 50%] Built target main
[ 75%] Building CXX object hello/CMakeFiles/hello.dir/src/hello.cpp.o
[100%] Linking CXX shared library libhello.so
[100%] Built target hello
Hello
bar == 300

添加測試 (GoogleTest)

這部分的完整代碼見 projects/replex-4。

一個好的項目,測試是必不可少的,前面我們實現(xiàn)的 main.cpp 中其實已經(jīng)有了一點自動化測試的影子,但是這種方式不夠好,我們可以使用 GoogleTest 來進行測試。

首先演示一個最基本的 gtest 用法,首先使用 git 的 submodule 命令添加 googletest 到我們的項目中

git submodule add git@github.com:google/googletest.git

然后修改我們根目錄下的 CMakeLists.txt,添加如下內(nèi)容

add_subdirectory(googletest)
enable_testing()
include_directories(${gtest_SOURCE_DIR}/include ${gtest_SOURCE_DIR})

add_subdirectory(test)

創(chuàng)建 test 目錄,結(jié)構(gòu)如下

test
├── CMakeLists.txt
└── src
    └── test.cpp

test/CMakeLists.txt 的內(nèi)容如下

add_executable(tests src/test.cpp)
target_link_libraries(tests PUBLIC gtest gtest_main)

test/src/test.cpp 的內(nèi)容如下

#include <gtest/gtest.h>

TEST(SillyTest, IsFourPositive) {
    EXPECT_GT(4, 0);
}

TEST(SillyTest, IsFourTimesFourSixteen) {
    int x = 4;
    EXPECT_EQ(x * x, 16);
}

int main(int argc, char** argv) {
    // This allows us to call this executable with various command line
    // arguments which get parsed in InitGoogleTest
    ::testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

OK,到現(xiàn)在我們已經(jīng)成功添加了 GoogleTest 到我們的項目中并且可以運行測試了,現(xiàn)在我們要編寫一些測試來測試我們的項目。

我們編寫一個 replex 的測試,測試內(nèi)容如下

#include <gtest/gtest.h>
#include <hello.h>

#include <cstdlib>
#include <fstream>

const char* g_Test_v1 = R"delimiter(
extern "C" {
int foo(int x) {
    return x + 5;
}
int bar = 3;
}
)delimiter";

const char* g_Test_v2 = R"delimiter(
extern "C" {
int foo(int x) {
    return x - 5;
}
int bar = -2;
}
)delimiter";

class ReplexTest : public ::testing::Test {
   public:
    // Called automatically at the start of each test case.
    virtual void SetUp() {
        WriteFile("hello/src/hello.cpp", g_Test_v1);
        Compile(1);
        HelloModule::LoadLibrary();
    }

    // We'll invoke this function manually in the middle of each test case
    void ChangeAndReload() {
        WriteFile("hello/src/hello.cpp", g_Test_v2);
        Compile(2);
        HelloModule::ReloadLibrary();
    }

    // Called automatically at the end of each test case.
    virtual void TearDown() {
        HelloModule::UnloadLibrary();
        WriteFile("hello/src/hello.cpp", g_Test_v1);
        Compile(1);
    }

   private:
    void WriteFile(const char* path, const char* text) {
        // Open an output filetream, deleting existing contents
        std::ofstream out(path, std::ios_base::trunc | std::ios_base::out);
        out << text;
    }

    void Compile(int version) {
        if (version == m_version) {
            return;
        }

        m_version = version;
        EXPECT_EQ(std::system("cmake --build build"), 0);

        // Super unfortunate sleep due to the result of cmake not being fully
        // flushed by the time the command returns (there are more elegant ways
        // to solve this)
        sleep(1);
    }

    int m_version = 1;
};

TEST_F(ReplexTest, VariableReload) {
    EXPECT_EQ(HelloModule::GetBar(), 3);
    ChangeAndReload();
    EXPECT_EQ(HelloModule::GetBar(), -2);
}

TEST_F(ReplexTest, FunctionReload) {
    EXPECT_EQ(HelloModule::Foo(4), 9);
    ChangeAndReload();
    EXPECT_EQ(HelloModule::Foo(4), -1);
}

int main(int argc, char** argv) {
    ::testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

要使得這個測試運行起來,還需要對 CMake 文件進行一些修改,這部分留作練習吧,動手試試會對 CMake 等有更深的理解。

相比較于 projects/replex-3,需要修改的文件有:

  1. 移除 main 文件夾
  2. 根目錄下的 CMakeLists.txt
  3. hello/CMakeLists.txt
  4. hello/include/hello.h
  5. test/src/test.cpp

完整代碼見 projects/replex-4


  1. Linux 下 C++so 熱更新 ??

總結(jié)

以上是生活随笔為你收集整理的C++ 动态库热加载的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。