🧐

エントリーポイントの隠蔽

2022/08/30に公開

はじめに

C/C++(Windows)でプログラムが実行されると、エントリーポイントと呼ばれる関数から実行されます。コンソールであればmain()ウィンドウアプリならWinMain()が呼ばれます。サンプルプログラムではおなじみの関数ですが、世に出ているライブラリやゲームエンジンではユーザー側からは見ることはなく、ライブラリ内に隠蔽されています。なんで隠蔽するんでしょうね?

int main(int argc, char** argv, char** envp)
{
    //...
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
{
    //...
}

UnrealEngine

UnrealではLaunchWindows.cppWinMain()が確認できます。しかし、ユーザーがゲームロジックとしての開始位置を意識すればいいのはBeginPlayイベントです。

LaunchWindows.cpp
int32 WINAPI WinMain(_In_ HINSTANCE hInInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ char* pCmdLine, _In_ int32 nCmdShow)
{
    int32 Result = LaunchWindowsStartup(hInInstance, hPrevInstance, pCmdLine, nCmdShow, nullptr);
    LaunchWindowsShutdown();
    return Result;
}

Unity

Unityはよくわからないですが、ユーザーがプログラムの開始位置を意識する必要はないです。
C#スクリプトのStart()がUnrealのBeginPlayイベントにあたります

public class Player : MonoBehaviour
{
   void Start()
   {
   }
}

明確な理由がわからない

エントリーポイントを隠蔽する明確な理由はわかりませんでした。エンジンやライブラリの役割(開発の効率化・カプセル化)を考えれば必然という感じはします。ただし、それらの理由抜きにした「隠蔽しなければいけない」となると理由はわかりません...

https://qiita.com/lriki/items/3385457986934d3ec694

エントリポイント(main 関数)をゲームエンジンの内部に隠蔽するか、しないか。
と言っても Web をサポートするなら実質隠蔽するしかない。
この場合ランタイムに、初期化時、フレーム更新時、などのコールバックを登録することににる。

実装

参考

https://github.com/cinder/Cinder
参考にした実装はCinderのAppクラス。今回はApplication(ライブラリ)と Editor(アプリケーション)に分離させ、ライブラリ側にエントリーポイントを定義します。つまり、ApplicationのプロジェクトはスタティックライブラリEditorのプロジェクトはアプリケーションに設定することを想定します。

Application(スタティックライブラリ)

class Application
{
public:
    Application(const std::string& appName);
    void Run();
    
    template<class AppClass>
    static void Entry(const std::string& appName);
    
private:
    std::string m_AppName;
}

template<class AppClass>
void Application::Entry(const std::string& appName)
{
    Application* app = static_cast<Application*>(new AppClass(appName));
    app->Run();

    delete app;
}

#define APPLICATION_ENTRY(AppClass, AppName)     \
int32 main(int32 argc, char** argv, char** evnp) \
{                                                \
    Application::Entry<AppClass>(AppName);       \
    return 0;                                    \
}

Editor(アプリケーション)

#include <Application.h>

// エントリーポイントの定義
APPLICATION_ENTRY(Editor, "Editor");

// エントリークラス
class Editor : public Application
{
public:
    Editor(const std::string& appName) : Application(appName) {}
    ~Editor() {}
};

ひとことで言ってしまえば、main(WinMain)をマクロ化したものをライブラリに定義し、アプリケーション側でマクロを呼び出しているだけです。ライブラリにエントリーポイントは必要ありませんが、アプリケーションはエントリーポイントの定義が必要です。そのため、アプリケーションであるEditorではこのような対応をとっています。

#define APPLICATION_ENTRY(AppClass, AppName)

APPLICATION_ENTRY(Editor, "Editor")
APPLICATION_ENTRY(Game, "Game")

AppclassにはエントリーポイントとなるクラスEditorを、AppNameにはアプリケーションの名前となる文字列(キャプションバーに表示されるもの)を指定します。エントリーポイント関数が呼ばれてAppclassに指定したクラス(Applicationクラスを継承)がインスタンス化されます。ビルド時にエントリークラスを切り替えることで、Editorビルド・Gameビルドといったような変更が可能になります。

まとめ

いろいろ話しましたが、結局「なんでエントリーポイントを隠蔽するのか?」という疑問はわかりませんでした。正直なところ、エントリーポイントの隠蔽がカッコいいと思って実装していた節があります。理由がまだわからないので開発の効率化・カプセル化なんだろうなと思うようにしています。

Discussion