dllでホットリロードのホット/リロードじゃない部分を作ってみた。
この記事は、神戸電子専門学校 ゲーム技研部 Advent Calendar 2024の22日目の記事です。
はじめに
現在ホットリロード作ってるんで、今できてる部分の備忘録です。
皆さんはホットリロードをご存じですか?コードの変更をアプリ起動中に適応するものです。今回はホットリロードのコードの変更を適応させる部分を作っていきます。やりかたとしては、ユーザーがコードを触る領域をdll[1]として出力させて更新して、随時読み込むことで変更が適応されます。前提知識としてdllやプロジェクトの構成プロパティ設定方法などありますが、まずは作りましょう!
環境
- Window11
- C++ 14(Visual Studio 2022)
作っていく
1.素材
- Coreプロジェクト(.lib)
- モジュール用のインターフェイスクラス
- モジュールの生成/削除/更新を管理するマネージャークラス
- Rendererプロジェクト(.dll)
- インターフェイスを継承したモジュールクラス
- モジュールクラスの生成/削除関数
- Applicationプロジェクト(.exe)
- main関数
2.プロジェクトの作成
アプリケーション
クライアント用のプロジェクトを作ります
コア
インターフェイスを提供する静的ライブラリを作ります。
レンダラー
モジュールを提供する動的ライブラリを作ります。
今回はRenderer(名前だけ)です。
その他
ビルドをDebugのWin32で行います。Win32がないので構成マネージャーから追加します
各プロジェクトのプロパティページは
構成: すべての構成
プラットフォーム: Win32
で編集してください
こうなっていればOK
3.プロジェクトの構築
Coreプロジェクト
中間/出力ディレクトリを整える
[Coreのプロパティページ]→[全般]
→[出力ディレクトリ] Lib\$(Configuration)\$(Platform)\
→[中間ディレクトリ] Bin\$(Configuration)\$(Platform)\
クラスを作る
#pragma once
class IModule
{
public:
IModule() = default;
virtual ~IModule() = default;
virtual void Initialize() = 0;
virtual void Update() = 0;
virtual void Shutdown() = 0;
};
#pragma once
#include <string>
#include <list>
#include <iostream>
#include <Windows.h>
#include <libloaderapi.h>
class IModule;
typedef IModule* (*CreateFnc)();
typedef void (*DeleteFnc)(IModule*);
class ModuleManager
{
public:
struct IModuleSet
{
HMODULE hModule = nullptr;
IModule* pModule = nullptr;
};
bool Load(const std::string& dllPath);
void Release();
void Update();
private:
std::list<IModuleSet*> m_moduleList;
};
#include "pch.h"
#include "ModuleManager.h"
#include "IModule.h"
#include <fstream>
bool CopyFileUsingStreams(const std::string& sourcePath, const std::string& destinationPath) {
// 入力ファイルを開く(バイナリモード)
std::ifstream inputFile(sourcePath, std::ios::binary);
if (!inputFile.is_open()) {
std::cerr << "Failed to open source file: " << sourcePath << std::endl;
return false;
}
// 出力ファイルを開く(バイナリモード)
std::ofstream outputFile(destinationPath, std::ios::binary);
if (!outputFile.is_open()) {
std::cerr << "Failed to open destination file: " << destinationPath << std::endl;
return false;
}
// ストリームを使ってデータをコピー
outputFile << inputFile.rdbuf();
// ストリームを閉じる
inputFile.close();
outputFile.close();
// コピー成功
return true;
}
bool ModuleManager::Load(const std::string& dllPath)
{
IModuleSet* moduleSet = new IModuleSet();
std::string tempDllPath = dllPath + "_copy.dll";
if (CopyFileUsingStreams(dllPath, tempDllPath)) {
std::cerr << "Failed to load DLL: " << dllPath << std::endl;
return false;
}
moduleSet->hModule = LoadLibraryA(tempDllPath.c_str());
if (!moduleSet->hModule) {
std::cerr << "Failed to copy DLL: " << dllPath << std::endl;
return false;
}
CreateFnc createFunc = (CreateFnc)GetProcAddress(moduleSet->hModule, "CreateModule");
if (!createFunc) {
std::cerr << "Failed to get factory function: CreateModule" << std::endl;
return false;
}
if ((moduleSet->pModule = createFunc())) {
moduleSet->pModule->Initialize();
m_moduleList.push_back(moduleSet);
return true;
}
return false;
}
void ModuleManager::Release()
{
for (auto modulePack : m_moduleList) {
DeleteFnc deleteFunc = (DeleteFnc)GetProcAddress(modulePack->hModule, "DeleteModule");
if (!deleteFunc) {
std::cerr << "Failed to get factory function: CreateModule" << std::endl;
return;
}
if (modulePack->pModule)
{
modulePack->pModule->Shutdown();
deleteFunc(modulePack->pModule);
modulePack->pModule = nullptr;
}
if (modulePack->hModule)
{
FreeLibrary(modulePack->hModule);
modulePack->hModule = nullptr;
}
delete modulePack;
modulePack = nullptr;
}
m_moduleList.clear();
}
void ModuleManager::Update() {
for (auto& modulePack : m_moduleList) {
modulePack->pModule->Update();
}
}
Rendererプロジェクト
中間/出力ディレクトリを整える
[Rendererのプロパティページ]→[全般]
→[出力ディレクトリ] Dll\$(Configuration)\$(Platform)\
→[中間ディレクトリ] Bin\$(Configuration)\$(Platform)\
Coreプロジェクトをリンクします
[Rendererのプロパティページ]→[リンカー]→[全般]→[追加のライブラリディレクトリ]
..\Core\Lib\$(Configuration)\$(Platform)
を追加
[Rendererのプロパティページ]→[リンカー]→[入力]→[追加の依存ファイル]
Core.lib
を追加
DllをExportする用のマクロを設定します
[プロジェクトのプロパティ]→[C/C++]→[プリプロセッサ]→[プリプロセッサの定義]
RENDERER_DLL_EXPORTS
を追加
クラスを作る
#pragma once
#include "../Core/IModule.h"
#ifdef RENDERER_DLL_EXPORTS
#define RENDERER_API __declspec(dllexport)
#else
#define RENDERER_API __declspec(dllimport)
#endif // RENDERER_DLL_EXPORTS
class RENDERER_API Renderer :public IModule
{
public:
Renderer() = default;
~Renderer() override = default;
virtual void Initialize()override;
virtual void Update()override;
virtual void Shutdown()override;
};
extern "C" RENDERER_API IModule* CreateModule();
extern "C" RENDERER_API void DeleteModule(IModule* _module);
#include "pch.h"
#include <iostream>
#include "Renderer.h"
void Renderer::Initialize()
{
std::cout << "Renderer : 初期化\n";
}
void Renderer::Update()
{
std::cout << "Renderer : 更新\n";
}
void Renderer::Shutdown()
{
std::cout << "Renderer : 終了\n";
}
RENDERER_API IModule* CreateModule()
{
return new Renderer();
}
RENDERER_API void DeleteModule(IModule* _module)
{
delete _module;
}
Applicationプロジェクト
[Applicationのプロパティページ]→[リンカー]→[全般]→[追加のライブラリディレクトリ]
..\Core\Lib\$(Configuration)\$(Platform)
を追加
[Applicationのプロパティページ]→[リンカー]→[入力]→[追加の依存ファイル]
Core.lib
を追加
#include "../Core/IModule.h"
#include "../Core/ModuleManager.h"
#include <iostream>
int main()
{
ModuleManager manager;
manager.Load("../Renderer/Dll/Debug/Win32/Renderer.dll");
manager.Update();
while (!(GetAsyncKeyState('R') & 0x8000))
{
}
manager.Release();
manager.Load("../Renderer/Dll/Debug/Win32/Renderer.dll");
manager.Update();
manager.Release();
while (!(GetAsyncKeyState(VK_ESCAPE) & 0x8000))
{
}
}
ホットリロード(?)をしてみる
今回はDebugのWin32でビルドしてください
ビルドが成功したら、Application/Debugにある.exeをApplication直下に置きます
そして起動!
ApplicationプロジェクトからRenderer.cppをいじりビルド
void Renderer::Update()
{
// std::cout << "Renderer : 更新\n";
std::cout << "Renderer : 更新(ホットリロード?)\n";
}
そしてR押すと....
更新される!
以上!!!お疲れぃ!!!!
まとめ
今回はdllの再読み込みによって、実行中でコードの更新を適応できるようしました。ただ、これではホットリロードでありません。
足りないもとして
- リロードではなくリセットである点
dllを読み込み直す度にインスタンスも再生成しているため、dll読み込み前の情報が飛んでしまいます。そのため、シリアライズ/デシリアライズを導入してリセットからリロードにしましょう。 - ホットじゃない
exeファイルでの実行でしか成り立ちません。ホットリロード?の実装の代償がブレイクポイント等が使えなくなる辛いってぇ。visual stadio環境でできる実行中のビルド方法があれば知りたい、ってかCMake勉強しろって感じだよね。南無
EXE上でしか成り立たなくて、シリアライズ/デシリアライズのない初期化これは本当にホットリード?
今回作った奴の完全版を置いときますね
引用
-
ライブラリの機能(プロジェクトの内容)を動的読み込んで扱えるもの ↩︎
Discussion