おもちゃバコ

中身スカスカ♡

C++: 簡単なホットリロードを実装してみた

ちは

簡単なホットリロードを実装した時の備忘録です。
誇大表現かも


ホットリロードとは

Microsoftの解説を引用します。

アプリケーションのコード ファイルを編集し、そのコード変更を実行中のアプリケーションに対して直ちに適用できるようにすることで、これを実現します ("ホット リロード" とも呼ばれます)。

learn.microsoft.com

自分は「実行中に再ビルド無しに新たな変更を適用すること」ぐらいの認識でいます。
大きく間違ってはないはず...


環境


ざっくり方針

1. DLLのプログラムを作る
 ついでにクラスを利用する方法も考えてみた。

2. DLLを呼び出すプログラムを作る
 プロジェクト設定で.libを利用しないのがポイント。
 あたしゃ動的リンクしたいからね。

当たり前な気がしますが,エンジン部分と実装部分を分離できるのも良いですね。


プロジェクト設定

DLLとそれを動的リンクするプログラムを用意しましょう。
DLLと動的リンクのプログラムは別のソリューションで用意しました。

  • DLL用プログラム
    Bodyと命名しました。
    「構成の種類」を「ダイナミック ライブラリ (.dll)」にするのをお忘れなく。
  • 動的リンク用プログラム
    Engineと命名しました。
    最終的にエンジンを開発したいからね。

こだわりが無ければビルドはx64がおススメ。


DLL: プログラム

IBody.h/Body.h/Body.cppを実装します。
エクスポート関数以外はお好きに。

IBody.h

#ifndef MYLIB_IBODY_H
#define MYLIB_IBODY_H

// インタフェース

namespace mylib
{
    class IBody
    {
    public:
        IBody() {}
        virtual ~IBody() {}
        virtual int Expr(const int num) const = 0;
    };
}

#endif // MYLIB_IBODY_H

Body.h

#ifndef MYLIB_BODY_H
#define MYLIB_BODY_H

// 実装

#include "IBody.h"

namespace mylib
{
    class Body final : public IBody
    {
    public:
        Body();
        ~Body();
        int Expr(const int num) const override;
    };
}

extern "C" __declspec(dllexport) mylib::IBody * GetBodyInstance();

#endif // MYLIB_BODY_H

Body.cpp

#include "Body.h"

#include <memory>

namespace mylib
{
    Body::Body()
    {
    }
    Body::~Body()
    {
    }
    int Body::Expr(const int num) const
    {
        return num * 2;
    }
}

mylib::IBody* GetBodyInstance()
{
    return new mylib::Body{};
}

クラスをエクスポートして使用したかったけど,よく分からないから一般的な方式にしました。
名前マングリングはdumpbin /exports *.dllなどで確認できます。

また,リロードするときにクラスサイズをチェックする必要がある点に注意。
その管理が面倒なのでインタフェースで誤魔化している。

追記: よい記事を見つけました。
ダウンキャストと似た印象を受けました。
qiita.com
www2s.biglobe.ne.jp

再追記: エクスポート関数のファイルも分けると良いですね。

エンジン: プログラム

動的リンクするプログラムを書きます。
そんなに難しくないはず。

#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#undef WIN32_LEAN_AND_MEAN

#include <iostream>
#include <thread>

// ↓ここ人によるから気を付けてね
#include "../Solution1/Body/IBody.h"

int LoadLib()
{
    // DLLロード
    HMODULE hModule = LoadLibrary(L"Body.dll");
    if (hModule == nullptr) { return -1; } // GetLastError()でエラー確認できる

    // アドレス取得
    using type = mylib::IBody*(*)();
    type func = reinterpret_cast<type>(GetProcAddress(hModule, "GetBodyInstance"));
    if (func == nullptr) { return -1; } // GetLastError()でエラー確認できる

    // 呼び出し: インタフェースを呼び出すことに注意
    mylib::IBody* const pBody = func();
    if (pBody == nullptr) { return -1; }
    const int num = pBody->Expr(100);

    delete pBody;

    // DLLアンロード
    if (FreeLibrary(hModule) == FALSE) { return -1; }
    return num;
}

int main()
{
    // 1回目
    std::cout << LoadLib() << std::endl;

    // Body.dllを差し替える
    // - ビルドするプログラムを書くのが良いけど,面倒なので30秒以内に手動で書き換える。
    std::this_thread::sleep_for(std::chrono::milliseconds(30000));

    // 2回目
    std::cout << LoadLib() << std::endl;

    return 0;
}

エラー時の処理は適当です。
実行するときはDLL用プログラムで作成したBody.dllをバイナリのあるフォルダにコピーしましょう。

本当はBody.dllをビルドして差し替えるプログラムを書けると良かったのですが,気力と能力が無かったので30秒以内に手動で差し替える方針でお茶を濁しました。
Body::Expr()の計算を変えて確かめてみるとリロード出来ていることが分かると思います。

あと,シングルトンなどを実装するときは色々な管理に注意が必要な気がしました。


まとめ

ホットリロードの基礎的なプログラムを実装してみました。
ゲームエンジンなどに実装されているホットリロードはもっと複雑なことをやっていると思いますが,雰囲気は掴めたのでとりあえずはヨシッ!!