C++でDLLを使用してホットリロードを実現する
ホットリロードは開発の生産性を向上させる強力な機能です。この機能により、プログラムを停止させることなくコードの変更を即座に反映させることができます。この記事では、C++とDLL(Dynamic Link Library)を使用して、独自のホットリロードシステムを実装する方法を解説します。
ホットリロードの基本概念
ホットリロードとは、アプリケーションが実行中にプログラムのコードを変更し、それらの変更をリアルタイムで反映させる技術です。これは、特に長時間のビルドやアプリケーションの再起動が必要な大規模なプロジェクトで時間の節約になります。
実装ステップ
ステップ1:DLLの作成
まず、ホットリロード可能な機能をDLLとして実装します。DLLを使用することで、実行中のアプリケーションから動的にコードをロードおよびアンロードすることが可能になります。
次にプロパティを設定していきます。
構成プロパティ>全般>出力ディレクトリに
$(SolutionDir)bin\$(Platform)\$(Configuration)\
を設定し、binファイル内に.dllを出力させる設定にします。
それでは、コードを書いていきます。
PlayerController.hを作成しプロトタイプ宣言します。
#pragma once
extern "C" __declspec(dllexport) void Update();
extern "C" __declspec(dllexport) void Draw();
PlayerController.cppを作成し、関数を定義します。
#include "PlayerController.h"
#include <iostream>
extern "C" __declspec(dllexport) void Update()
{
std::cout << "更新処理" << std::endl;
}
extern "C" __declspec(dllexport) void Draw()
{
std::cout << "描画処理" << std::endl;
}
ここでビルドして、.dllがbinファイルの中に作成されたか確認しましょう。
ステップ2:アプリケーション(DLLを読み込む側)の作成
アプリケーション側では、DLLを定期的にチェックし、変更があった場合にDLLを再ロードして実行します。
Player.hを作成しクラスを作成します。
#pragma once
#include <string>
class Player final {
public:
~Player();
void Update();
void Draw();
public:
void LoadScript();
private:
typedef void(*_UpdateFunc)(void);
_UpdateFunc UpdateFunc;
typedef void(*_DrawFunc)(void);
_DrawFunc DrawFunc;
};
関数の定義をPlayer.cppを作成し、行います。
#include "Player.h"
#include <Windows.h>
#include <string>
#include <iostream>
HMODULE module = nullptr;
Player::~Player()
{
if (module) {
// ライブラリの解放
FreeLibrary(module);
}
}
void Player::Update()
{
if (UpdateFunc)
UpdateFunc();
}
void Player::Draw()
{
if (DrawFunc)
DrawFunc();
}
void Player::LoadScript()
{
std::wstring dllPath = L"EngineDLL.dll";
if (module) {
// ライブラリの解放
FreeLibrary(module);
}
// ライブラリの読み込み
module = LoadLibrary(dllPath.c_str());
if (module == nullptr) {
std::cout << "DLLをロードできませんでした。" << std::endl;
return;
}
// 関数の読み込み
UpdateFunc = (_UpdateFunc)GetProcAddress(module, "Update");
if (UpdateFunc == nullptr) {
std::cout << "Update関数をロードできませんでした。" << std::endl;
}
DrawFunc = (_DrawFunc)GetProcAddress(module, "Draw");
if (DrawFunc == nullptr) {
std::cout << "Draw関数をロードできませんでした。" << std::endl;
}
}
#include <cstdio>
#include <iostream>
#include <Windows.h>
#include "Player.h"
int main()
{
Player playerInstance;
while (1)
{
playerInstance.LoadScript();
playerInstance.Update();
playerInstance.Draw();
Sleep(3000); // 3秒停止
}
return 0;
}
ここで実行してみると
更新処理
描画処理
更新処理
描画処理
更新処理
描画処理
・・・
のように文字列が列挙されます。
この段階でDLLを上書きすればスクリプトが書き換わるように思えますが、使用中のDLLは上書きすることが出来ません。
ホットリロードするには?
ホットリロードするには読み込む専用DLLと書き込む専用DLLに2つが必要となります。新たに書き込まれたDLLをコピーし、コピーした側のDLLを読み込み専用DLLとして読み込みます。それと同時に書き込み専用のDLLの最終更新時間を見て変更を検知したら繰り返すということです。
#pragma once
#include <string>
class Player final {
public:
~Player();
void Update();
void Draw();
void DLLCheck();
private:
void LoadScript();
private:
typedef void(*_UpdateFunc)(void);
_UpdateFunc UpdateFunc = nullptr;
typedef void(*_DrawFunc)(void);
_DrawFunc DrawFunc = nullptr;
};
#include "Player.h"
#include <Windows.h>
#include <string>
#include <iostream>
#include <filesystem>
HMODULE module = nullptr;
std::filesystem::file_time_type lastWriteTime;
Player::~Player()
{
if (module) {
// ライブラリの解放
FreeLibrary(module);
}
}
void Player::Update()
{
if (UpdateFunc)
UpdateFunc();
}
void Player::Draw()
{
if (DrawFunc)
DrawFunc();
}
void Player::DLLCheck()
{
std::string dllReadPath = "tempEngineDLL.dll"; // コピー先
std::string dllWritePath = "EngineDLL.dll"; // コピー元
// コピー元DLLが存在するかチェック
if (!std::filesystem::exists(dllWritePath)) {
std::cout << "コピー元DLLが存在しません。" << std::endl;
return;
}
// 初めての場合(コピー元が存在し、コピー先が存在しない場合)
if (std::filesystem::exists(dllWritePath)
&& !std::filesystem::exists(dllReadPath))
{
// 最終更新日付を取得
lastWriteTime = std::filesystem::last_write_time(dllWritePath);
}
// 2回目以降の場合(コピー元コピー先どちらも存在する場合)
if (std::filesystem::exists(dllWritePath)
&& std::filesystem::exists(dllReadPath))
{
// 最終更新日付が同じ場合(変更なしの場合)
if (lastWriteTime ==
std::filesystem::last_write_time(dllWritePath)) return;
}
// DLLを上書きコピーする
std::filesystem::copy_file(dllWritePath, dllReadPath,
std::filesystem::copy_options::overwrite_existing);
// 読み込みなおす
LoadScript();
}
void Player::LoadScript()
{
std::string dllPath = "tempEngineDLL.dll";
if (module) {
// ライブラリの解放
FreeLibrary(module);
}
// ライブラリの読み込み
module = LoadLibrary(dllPath.c_str());
if (module == nullptr) {
std::cout << "DLLをロードできませんでした。" << std::endl;
return;
}
// 関数の読み込み
UpdateFunc = (_UpdateFunc)GetProcAddress(module, "Update");
if (UpdateFunc == nullptr) {
std::cout << "Update関数をロードできませんでした。" << std::endl;
}
DrawFunc = (_DrawFunc)GetProcAddress(module, "Draw");
if (DrawFunc == nullptr) {
std::cout << "Draw関数をロードできませんでした。" << std::endl;
}
}
#include <cstdio>
#include <iostream>
#include <Windows.h>
#include "Player.h"
int main()
{
Player playerInstance;
while (1)
{
playerInstance.DLLCheck();
playerInstance.Update();
playerInstance.Draw();
Sleep(3000); // 3秒停止
}
return 0;
}
ここで実行してみて、途中でDLLを変更してみると
更新処理
描画処理
更新処理
描画処理
更新処理(変更後)
描画処理(変更後)
更新処理(変更後)
描画処理(変更後)
・・・
のように文字列が変更されます。
これにてC++でのホットリロード(実行しながらのコード変更)が行えました。
ホットリロードの応用
ホットリロード機能を持つDLLを実装することで、ゲーム開発やデスクトップアプリケーション開発において、ユーザーインターフェイスやビジネスロジックの迅速な試行錯誤が可能になります。特に、開発中の機能のテストや、バグ修正後の即時反映などに有効です。
まとめ
C++でDLLを利用したホットリロード機能の実装は、開発プロセスを効率化し、開発者がより迅速にフィードバックを得られるようにする強力なツールとなります。リソース管理やエラーハンドリング、セキュリティ対策など、安全かつ効果的に利用するためには考慮すべき点が多くあると思います。