😊

dllでホットリロードのホット/リロードじゃない部分を作ってみた。

2024/12/22に公開

この記事は、神戸電子専門学校 ゲーム技研部 Advent Calendar 2024の22日目の記事です。
https://qiita.com/advent-calendar/2024/kdgamegiken

はじめに

現在ホットリロード作ってるんで、今できてる部分の備忘録です。
皆さんはホットリロードをご存じですか?コードの変更をアプリ起動中に適応するものです。今回はホットリロードのコードの変更を適応させる部分を作っていきます。やりかたとしては、ユーザーがコードを触る領域を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)\

クラスを作る

IModule.h
#pragma once
class IModule
{
public:
	IModule() = default;
	virtual ~IModule() = default;

	virtual void Initialize() = 0;
	virtual void Update() = 0;
	virtual void Shutdown() = 0;
};
ModuleManager.h
#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;
};


ModuleManager.cpp
#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を追加

クラスを作る

Renderer.h
#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);
Renderer.cpp
#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を追加

Application.cpp
#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をいじりビルド

Renderer.cpp
void Renderer::Update()
{
	// std::cout << "Renderer : 更新\n";
	std::cout << "Renderer : 更新(ホットリロード?)\n";
}

そしてR押すと....

更新される!
以上!!!お疲れぃ!!!!

まとめ

今回はdllの再読み込みによって、実行中でコードの更新を適応できるようしました。ただ、これではホットリロードでありません。
足りないもとして

  1. リロードではなくリセットである点
    dllを読み込み直す度にインスタンスも再生成しているため、dll読み込み前の情報が飛んでしまいます。そのため、シリアライズ/デシリアライズを導入してリセットからリロードにしましょう。
  2. ホットじゃない
    exeファイルでの実行でしか成り立ちません。ホットリロード?の実装の代償がブレイクポイント等が使えなくなる辛いってぇ。visual stadio環境でできる実行中のビルド方法があれば知りたい、ってかCMake勉強しろって感じだよね。南無

EXE上でしか成り立たなくて、シリアライズ/デシリアライズのない初期化これは本当にホットリード?

今回作った奴の完全版を置いときますね
https://github.com/JitoPin/HotReset

引用

https://learn.microsoft.com/ja-jp/cpp/build/walkthrough-creating-and-using-a-static-library-cpp?view=msvc-170
https://learn.microsoft.com/ja-jp/cpp/build/walkthrough-creating-and-using-a-dynamic-link-library-cpp?view=msvc-170
https://qiita.com/yuusukepgworks/items/0a9cefcd0f2e41102857
https://qiita.com/Osakazyuuzi/items/a0214822a84168115ee7
https://lambda00.hatenablog.com/entry/2023/06/21/224902

脚注
  1. ライブラリの機能(プロジェクトの内容)を動的読み込んで扱えるもの ↩︎

神戸電子専門学校ゲーム技術研究部

Discussion