ヒープとshared_ptr

先日ファイルシステムが壊れてます、みたいなエラーが記録されているのを発見。
ググったらデフラグかけたらエラーがなくなった、みたいな記事を見つけました。

いくらなんでもそれはないだろう・・・と思ったけど・・・、
まぁ、エントロピーが増大しまくっているHDDを久しぶりにデフラグをかけたら
ちょっとは速くなるだろうと思い3時間かけてデフラグ終了。
エラーが記録されているHDDにデフラグをかけるなんて、傷に塩を塗り込む行為だとは思いつつ(^^;

で、今日イベントビューア見たらエラーが消えてました・・・どーなってんの、コレ?(笑)
まぁいいや。

で、本題です。

Windowsで動的にメモリブロックを確保するには、Heap APIを使うか、もっと原始的に?VirtualAlloc()などの仮想メモリを直接確保するAPIを駆使するか・・・と思います。でも仮想メモリを直接いじると何でもできちゃう反面、ちょっと大げさですし・・・。ということで、よほど大きなメモリブロックを確保するほどでもないサイズだとヒープAPIを使用するほうがお手軽です。

そんなの malloc()で十分・・・という感じもしますが・・・ちょっとした備忘録のメモも兼ねています。

で、確保したメモリブロックの寿命管理をスマートポインタ、shared_ptr に任せてしまおう、というわけです。

プライベートヒープを使用する上で、HeapCreate()とHeapDestroy()を呼ばないといけないわけで、ヒープハンドルをいつ廃棄するか(HeapDestroy()を呼ぶか)を参照カウンタで管理したいと思い次のようなコードを思いつきました。

//-----shared_heap.h-------

#include <windows.h>

//shared_ptrはboost版でもいいですが、
//Visual C++ 2008SP1にはTR1の実装があるのでそれを使います。
#include <memory>

using namespace std::tr1;

class shared_heap
{
public:
  typedef std::tr1::shared_ptr<VOID> return_type;

private:
	//CCriticalSectionは CRITICAL_SECTION構造体を単純にラップしたクラスです。
  static CCriticalSection _criticalsection;
  static HANDLE _sHeap;
  static UINT _sCount;
  
  class HeapDeleter
    {
    public:
      void operator()(PVOID ptr)
        {
          _criticalsection.Enter();
          if(::HeapFree(_sHeap,HEAP_NO_SERIALIZE,ptr))
            {
              if((--_sCount == 0) && _sHeap)
                {
                  ::HeapDestroy(_sHeap);
                  _sHeap = NULL;
                }
            }
          _criticalsection.Leave();
        }
    };
  
  shared_heap(){};
  ~shared_heap(){};

public:
  static return_type alloc(size_t size)
    {
      return_type retVal;
      
      _criticalsection.Enter();
      if(!_sHeap)
        _sHeap = ::HeapCreate(HEAP_NO_SERIALIZE,0,0);
      
      retVal = return_type(::HeapAlloc(_sHeap,HEAP_NO_SERIALIZE | HEAP_ZERO_MEMORY,size),HeapDeleter());
      _sCount++;
      
      _criticalsection.Leave();
      
      return retVal;
    }
};

CCriticalSection shared_heap::_criticalsection;
HANDLE shared_heap::_sHeap  = NULL;
UINT   shared_heap::_sCount = 0;

この shared_heapクラスは単純に静的関数を集めたもので、インスタンス化しても意味はないのでコンストラクタとデストラクタはプライベートにしています。実際には、各静的メンバ関数およびカスタムデリータの定義(.h)と実装(.cpp)を分ける必要がありますし、静的メンバ変数の初期化は実装側(.cpp)に書く必要もありますねぇ・・・。

で、まぁ、要点はshared_heap::alloc()で、HeapAlloc()APIをコールして得たメモリブロックのポインタをカスタムデリータとともにshared_ptrのコンストラクタに渡して、shared_ptrを返しています。スレッドセーフにするためクリティカルセクションを使用しています。

非常に単純なコードですが、これで廃棄処理コードを書く手間が省けます。shared_ptr 万歳!(^_^;;;

//使用例
#include "shared_heap.h"
int _tmain(int argc,_TCHAR **argv)
{
  shared_heap::return_type sptr = shared_heap::alloc(512);

  //なんかの処理

  return 0;
}

※僕の備忘録ですので、多々間違いがあるかもしれません。誰もいないと思いますが、そのままをコピペして使用するのは危険です。ご了承を。

エアロのガラス効果

Windows Vistaに全く興味がなかったわけでもないですが、メインのPCをWindows7にしたことだし・・・ということで、ほとんど何の知識もないWindows Aeroですが、ちょっと勉強がてらいじってみました。

Aeroの最大の特徴は、やはり、ウィンドウフレームにも適用されているガラス効果でしょう。各ウィンドウとガラス効果などのAero効果をデスクトップ上のウィンドウとして表示できるように合成・管理しているのが、DWM(Desktop Window Manager)ということだそうです。

クライアント向けのWindowsで、NT4からXPまでのウィンドウマネージャなどの大部分はカーネルモードで動作していたと思います。これは純粋にパフォーマンス向上のためと思いますが、Vista以降ではドライバモデルが変更され、iその一部がユーザーモードで動作するようになりました。本来のNTの思想に戻ったといえます。ビデオカードをはじめとするハードウェア自体の性能向上のおかげでしょうね・・・。

このDWM、タスクマネージャで確認するとVistaと7では、明らかにメモリ使用量が違っています。詳しくは知りませんが、なんらかの改良が施されているんだと思います。

で、DWMに関連するAPIでネットを検索しても、SDKプログラマの興味がないのか、関心がないのか、C/C++ WindowsSDKで利用できるまとまった情報(コード)が少ないです。使ってみました、という記事はいっぱい出るんですが、詳細な記事が見つからず。MSDNのAPI説明見てもいまいち分からないし・・・。

まぁ、だけど利用できるAPIは限られていて、DWMに関連するAPIで最も簡単に利用できるのが、DwmExtendFrameIntoClientAreaというAPIです。
これは、ウィンドウフレームに適用されているガラス効果をクライアントエリアに拡張するAPIなんですが、使い方は至って簡単です。ガラス効果をどれだけクライアント側に食い込ませるかをMARGINS構造体にセットして、それをウィンドウハンドルとともに渡せばOKです。

//{左,右,上,下}という風になります。
//下記例だと、上部20ピクセルが拡張されます。
MARGINS margins = {0,0,20,0};
DwmExtendFrameIntoClientArea(hWnd,&margins);

//ちなみに、以下のように-1をセットすると、クライアント領域すべてがガラス効果の影響受けます。
MARGINS margins = {-1};

※dwmapi.hのインクルードと、dwmapi.libのリンクが必要です。

MARGINS margins={-1};としたときのガラス効果

このようにAPI自体はカンタンに利用できる反面、C/C++とWindowsSDKのみで組んで行くには相当めんどくさい処理が必要になります。

  1. 単純にボタンやエディットなどのコントロールを配置すると、悲惨なことになる。
  2. ガラス効果上に黒い文字を描画すると、黒い文字が透明になっちゃう(^^;;;
  3. ガラス効果にしたクライアント領域部分は、別途常に黒く塗りつぶさないといけない。
    (要するに、拡張した部分の背景色の再描画処理を別途記述しなければならないかと・・・)
  4. DWMのデスクトップコンポジションが有効か無効かを判別してそれぞれに対する処理を行う必要がある。

などなど・・・

ガラス効果上に配置した黒く塗りつぶしたGDIアイテムは、全部透明になってしまいます(テキストとかメニューの文字とか)。要するに、色値が0x00000000(要するに黒色)の部分がガラス効果として透明なるっちゅーわけだから、当たり前と言えば当たり前ですが・・・。COLORREF型の先頭1バイトを255にすればいいのか?よくわかんないです。
これの解決方法としては、コントロールをオーナードローで描画するか、GDI+ライブラリを使用する、という選択肢がMSDNで紹介されていました。が、子コントロールの色を全部修正するなんて、メンドーだし、他の解決方法があるのかもしれません。
そのうちガラス効果関連処理をラップしたC/C++のクラスライブラリがそのうち出るかもしれないし、でないかもしれないし、すでに出ているのかも・・・。C#の情報は結構あるんですけどね・・・。

ダイアログをリソースファイルで定義してDialogBox() APIで表示させているようなプログラムの場合、最低限以下のようなウィンドウメッセージを捕まえて背景を別途処理しないといけない。

・WM_INITDIALOG
デスクトップコンポジションが有効なら、DwmExtendFrameIntoClientAreaを実行。

・WM_ERASEBKGND
ガラス効果を適用しても、ダイアログの背景用描画ブラシで上書きされてしまうので、ガラス効果を拡張した部分は黒色で、それ以外はダイアログの背景色で背景を塗りつぶす。

・WM_DWMCOMPOSITIONCHANGED
デスクトップコンポジションが有効になったり、無効になったりすると、飛んでくるので、これを捕まえて適切な処理を行う。
注意しなければならないのは、デスクトップコンポジションが有効になったらその都度、DwmExtendFrameIntoClientAreaをコールする必要があること。

INT_PTR CALLBACK DlgProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
  //上部20ピクセルを拡張
  MARGINS margins = {0,0,20,0};
  RECT rect = {0};
  BOOL fDwmEnabled = FALSE;
  HBRUSH hBrush = NULL;
  LONG oldBottom = 0;

  switch(message)
  {
  case WM_INITDIALOG:
    DwmIsCompositionEnabled(&fDwmEnabled);
    if(fDwmEnabled)
      DwmExtendFrameIntoClientArea(hDlg,&margins);
    return (INT_PTR)TRUE;

  case WM_ERASEBKGND:
    DwmIsCompositionEnabled(&fDwmEnabled);
    GetClipBox((HDC)wParam,&rect);

    //デスクトップコンポジションの有効・無効によって背景に使用するブラシを決定
    hBrush = fDwmEnabled ? (HBRUSH)GetStockObject(BLACK_BRUSH) : GetSysColorBrush(COLOR_BTNFACE);

    //クライアント領域の上部20ピクセル(ガラス効果)の背景を描画
    oldBottom = rect.bottom;
    rect.bottom = rect.top + 20;
    FillRect((HDC)wParam,&rect,hBrush);
    
    //クライアント領域の残りの下部の背景を描画
    hBrush = GetSysColorBrush(COLOR_BTNFACE);
    rect.top += 20;
    rect.bottom = oldBottom;
    FillRect((HDC)wParam,&rect,hBrush);

    return (INT_PTR)TRUE;

  case WM_DWMCOMPOSITIONCHANGED:
    DwmIsCompositionEnabled(&fDwmEnabled);
    if(fDwmEnabled)
      DwmExtendFrameIntoClientArea(hDlg,&margins);
    return (INT_PTR)0;

  case WM_COMMAND:
      //ダイアログ上のコントロールへの応答処理など
      if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL)
        {
          EndDialog(hDlg, LOWORD(wParam));
          return (INT_PTR)TRUE;
        }
      break;
  }
  return (INT_PTR)FALSE;
}

ということで、なんとかかんとか、思ったように表示できました・・・。

ガラス効果2

なんか、久しぶりにGUIなAPI触ると、何がどうだったか・・・忘却の彼方へ・・・(ーー;

モニターの電源を切れ

今日、iPod Touchの車載用FMトランスミッター兼充電器買ってきました。エンジンの始動とともに音楽が始まるところは便利です(^^

バッファローコクヨ・FMトランスミッター

雑音(ノイズ)も入るのかな~、と思ってましたが、そんなこともなく、なかなかです。明日のドライブに重宝しそうです(^^

で、本題です。

続きを読む

VC++ 2008EE で COMサーバーをカンタンに作成

【追記】
ご承知のように、Visual Studio Commuity Edition がリリースされました。ライセンス的にやや難がありますが、商用/非商用アプリケーションの個人開発者にはVisual Studio の Professional 版相当のフルセットが無料で提供されるようになりましたね、バンザーイ!
ってことで、このページの内容は、意味をなさないので、とっととCommunity Editionをインストールしてラクをしてください。


備忘録のエントリです。

フリーの開発環境でラクにCOMサーバーを作ろう、という主旨です(^^;;;

キーワードは「C++属性」です。この属性を利用することで手作業であっても退屈なメンドクサイ、コードの大部分(ほとんど)が自動的に作成されます。コーディングするのは、実装するインターフェイスのメソッドの中身だけ。簡単です。ただ・・・ATLだけはどうしても必要です。

しかし・・・Visual Studio 2008 Express EditionにはATLは付属していません。

ATLのヘッダーファイルとライブラリファイルをどこぞから調達しなくてはいけませんが、幸いなことにマイクロソフトがこれまた無料で配布しているWDK(Windows Driver Kit)をインストールすることにより利用することが可能です。WDKはかつては DDK(Driver Developper Kit)と呼ばれていたモノです。WDKにはVC++2008と同じコンパイラ・リンカなどの開発ツール、ATL/MFCやWindowsでのカーネルモードドライバを作成するために必要なすべてのファイル・ドキュメントが同梱されています。

無料で手に入れることができるのですが・・・一点、不安なことがあります。

それは・・・Visual Studio 2008 Express Editionと、WDK内の構成ファイル(ATLヘッダーファイルとライブラリファイル)を混ぜてコードをビルドするのがライセンス的に良いのか駄目なのか・・・等は、調査不足で分かりません。Visual Studio 2008 Express Editionで作成したアプリケーションは商用・非営利を問わず配布できたかと思いますが、WDKはそのあたり分かりません。まぁ非営利・個人的な利用なら問題はないかと思いますが・・・。

(WDKのダウンロードにはWindows Liveやマイクロソフト・パスポートのアカウントが必要です)

MSのダウンロードセンターからダウンロードできるようになってました。( 2012/1/11)

で、話を本題に戻します。 このWDKをインストール後、VC++ 2008 Express Editionのディレクトリ設定にこれらのパスを追加登録してあげればATLを利用したアプリケーション開発が可能となります。ただし、ATL関連ファイルがあるというだけで、便利なプロジェクト・ウィザードはないので全部手作業です(^^;;; やっぱりラクはできません(笑)。

さて、COMサーバーの実際の手順なんですが・・・これは日本語ドキュメントが既にあります(^^ MSDNライブラリの「COM属性によるCOM DLLの開発 -> チュートリアル : テキスト エディタを使った COM サーバーの作成」です。

サンプルがありますのでこれを自分用に修正していけばいいだけ。僕は以下のようにMyServerというCOMオブジェクトをテストとして作成することにしました。見れば分かりますが・・・[ ] でくくっているところが属性と呼ばれるマイクロソフトの独自の拡張です。かなり違和感のあるコーディングです。が、これをコンパイルすることで必要なファイル、足りないコードを自動生成してくれます。理屈は分かりません(笑)  ある程度理解するにはやはりCOM/ATLの知識が必要です。魔法(ブラックボックス)として扱うのがよさそうです。

IMyServerインターフェイスを定義して、CMyServerクラスで実装。インターフェイスメソッド、CurrentDirectory()を定義。これは現在のカレントディレクトリを文字列で返すメソッドをサンプルとして定義しました。必要に応じてインターフェイスやメソッドを増やしたい場合もチョチョッと手を加えるだけで済みます。

んで、これだけのコードで完璧なオートメーションサーバーができてしまいます。属性の詳細は・・・あまり知る必要はないかと。詳細はかなり難解です。何度も書きますが、理解するにはCOMとかATLに関する中級~上級レベルの知識が必須かと。ただ公開するものの実装さえしてやればいいので、あとのCOMに関連するおきまりのコードはコンパイラとリンカが勝手にやってくれますのでかなりラクです。従来ならIDLファイルを書いて、IDispatch,IUnknowを実装して、レジストリへの登録処理を書いて・・・などが必要でしたが・・・。

このファイルをたとえば  cl /LD MyServer.cpp /link /TLBOUT:MyServer.tlb  みたいにコンパイルしてやればインプロセスサーバー(DLL)、タイプライブラリが生成されます。

あとはRegsvr32.exe でDLLファイルを登録し、以下のようなテストコードで動作確認。

パフォーマンスにシビアなコードはC++で実装して、あとはC#やJScriptでお手軽に・・・というような用途にいいかもです。

簡易メモリアロケータ

最近面白そうな機能満載の携帯がいっぱい出てきて、意味無く携帯を新しくしたいです。どっちかっていうと、時間潰しができて遊べる携帯(^^;;; 早くDOCOMOからiPhone出てくれないかな~。坂本龍一がCMやってたSamsungも良いなぁ。面白そうなやつは全部ソフトバンクなんだなぁ~。DOCOMOももっと変わったもん出して欲しいなぁ。

そんなことはさておき、

ローカルスコープ内で、使い捨てみたいな使い方をするような小さなメモリを確保するとき、たとえば、Win32APIでなんらかの文字列を受け取るときのバッファとか、いちいちnewでメモリ確保して、使い終わったらdeleteして・・・が面倒くさい。特にC#を覚えてnewしっぱなしでdeleteする必要がない言語に慣れきってしまうとC++は非常にやりづらい。

と、かなり前に思って、以前ずっとかなり前にWin32APIの勉強のため書いていた簡易メモリアロケータを引っ張り出してきて、ちょこっと手直し。

正直、メモリアロケータなんて大層なもんじゃなくて、コンストラクタでnewしてメモリを確保して、スコープから抜けるとデストラクタでdeleteする、という超簡単なもん。

手直ししたのは、コンストラクタとデストラクタでメモリの確保と解放する際に、newとdelete演算子を使わず、Windowsの Heap系のAPIで置き換えたことと、& 演算子をオーバーロードして、内部バッファのポインタを返すようにしたことの2点。Advanced Windows のメモリ管理を参考に参照カウンタも付けてみました(^^;

ほとんどサンプルと同じになっちゃった(^^;

具体的には・・・

CAllocator alloc(MAX_PATH); //MAX_PATHバイトのメモリ確保
CAllocator allocs[5] = {100,100,100,100,100}; //配列とか、
CAllocator alloc; alloc->Alloc(100); //などでメモリ確保
CAllocator alloc; alloc.Alloc(100); //などでメモリ確保

メモリブロックのポインタを得るには、*演算子。

LPSTR lpStr = reinterpret_cast<LPSTR>(*alloc);

スコープを抜けるとデストラクタがコールされて、メモリが解放。参照数がゼロになると、プライベートヒープハンドルも解放。

修正すべき欠点は、スレッドセーフじゃない。複数スレッドで使用すると参照カウンタとかで競合が起きるし。大きなメモリ(たとえば1MBとか)を確保したいときとかは、VirtualAlloc系を使うべき、らしい。
まぁ・・・スレッドをおこすようなプログラムは複雑になればなるほどC#の方が安全で簡単なんで。

追記) &演算子で確保したメモリのポインタをリターンしてますが、スコープを抜けると問答無用でメモリ解放してしまうので、気をつけないといけません。CAllocatorを動的に確保すればいいんでしょうけど、それだと意味がなくなる(^^;

これをベースに実用的に使えるまで改良していきたいと思います。


8月30日、ところどころ修正しました。