Gaming Life

一日24時間、ゲームは10時間

WindowsのC++でウィンドウアプリケーションを書く際にWinMain()を隠匿する方法の一例

序文

OpenSiv3Dなど、ユーザーが使いやすいように考えて作られたC++Windowsアプリケーション開発用フレームワークは、複雑なシグネチャを持つ WinMain() を隠してくれる。

# include <Siv3D.hpp>

// WinMain()を書く必要がない
// int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
//                    LPSTR lpCmdLine, int nCmdShow)
void Main()
{
    Print << U"Hello, Siv3D!";

    while (System::Update())
    {
    }
}

私もこうしたWinMainの隠匿をしてみたいなーと思い、実際に実装してみた。

最も簡単な実装

FrameworkMain.cpp

#include <Windows.h>

void Main();

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                   LPSTR lpCmdLine, int nCmdShow)
{
    OutputDebugStringA("winmain() start!\n");
    Main();
    return 0;
}

UserMain.cpp

#include <Windows.h>

void Main() {
    MessageBoxA(nullptr, "Demo", "Demo Message", MB_OK);
}

上記のコードのように、フレームワーク側(.lib)で、宣言だけ用意して未実装な void Main() 関数を用意し、それをフレームワーク側に記述した WinMain() で呼び出す。これにより、ユーザーは WinMain() の存在に気を払うことなく、Main() 関数さえ実装することができる。

以上で目的は達成できたのだが、これではあまりに薄いので、もう少し凝った実装をしてみた。

クラスのメンバ変数をエントリポイントのように扱う

ここでは以下の要件を満たすものを実装する。

  1. フレームワークはAppBaseという名のクラスを用意する
  2. AppBaseは virtual int Init() という純粋仮想関数をもつ。
  3. ユーザーはAppBaseをpublic継承したアプリケーション用クラスを作成できる
  4. ユーザーは継承先で virtual int Init() 関数をエントリポイントのように扱って実装できる
  5. ユーザーは、これらの機能を最小限の労力で使用できる。

AppBase.h(フレームワーク実装)

#pragma once
#include <windows.h>

class AppBase* GetWinPtr();

class AppBase {
private:
    friend AppBase* GetWinPtr();
    static AppBase* m_App;

public:
    AppBase();
    virtual int Init() = 0;
};

AppBase.cpp(フレームワーク実装)

#include "AppBase.h"

AppBase::AppBase() { m_App = this; }

AppBase* AppBase::m_App = nullptr;

AppBase* GetWinPtr() { return AppBase::m_App; }

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                   LPSTR lpCmdLine, int nCmdShow)
{
    OutputDebugStringA("winmain() start!\n");
    AppBase* g_App = nullptr;
    g_App = GetWinPtr();
    if (g_App == nullptr) {
        return -1;
    }
    return g_App->Init();
}

MyApp.cpp(ユーザー実装)

#include "AppBase.h"

class MyApp : public AppBase {
public:
    virtual int Init() {
        MessageBoxA(nullptr, "Demo", "Demo Message", MB_OK);
        return 0;
    }
};

MyApp app;

解説

ユーザーは、グローバルに自身がAppBaseクラスを継承して作成したクラスのインスタンスを作成するだけで、 Init() 関数をエントリポイントのように扱って実装できる。仕組みとしては以下の通り。

AppBaseはstatic変数として、自身の型のポインタ変数を持つ。このポインタは、AppBaseのfriend関数である GetWinPtr() から取得できる。

フレームワーク上では、 WinMain() において GetWinPtr() 経由でAppBase型のポインタから Init() メンバ関数を呼び出す。しかし、この時点ではそのポインタの実体がどの型を表しているか分からない。

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                   LPSTR lpCmdLine, int nCmdShow)
{
    OutputDebugStringA("winmain() start!\n");
    AppBase* g_App = nullptr;
    g_App = GetWinPtr();
    if (g_App == nullptr) {
        return -1;
    }
    return g_App->Init();
}

そこでユーザーは、AppBase型を継承したクラスであるMyApp型のインスタンスを、グローバルで作成する。グローバルで定義した変数は、エントリポイント、つまり WinMain() より早く用意される。

これにより、ユーザーは、AppBase継承クラスの Init() 関数を、エントリポイントのように扱って実装することができる。

補足

この実装は、WindowsGUIフレームワークとしてMicrosoftが提供する、MFCMicrosoft Foundation Class)の実装とほとんど同じらしい。

参考

In a typical MFC application we never write WinMain() function so how is it possible to compile and link a windows program with out WinMain()?(http://www.equestionanswers.com/vcpp/mfc-application-winmain().php)

winmainの隠蔽工作