それってどんな仕組みですか?


 前回、DLLを作ってみました。
 テストアプリケーションを使って、動作を確認していただけたと思います。
 これは、アプリケーションにあらかじめ「TESTDLL.DLL」というライブラリを使用する
と言うことを指定していたので、DLLが無いとアプリケーションを起動するときに
Windowsから怒られることになるのです。

怒られちゃう、怒られちゃう

 さてさて、EmuZ-2000が作られるときに、あらかじめどんなボードが開発されるのか分っていたのでしょうか?
 そんなはずは有りませんよね。
 未来を確実に予測することなど不可能なのですから。

 でも、EmuZ-2000を起動するときに、この様なDLLが見つからないエラーが出ないのは何故でしょうか?

 これが、ソケットと言われる仕組みを利用している利点なのです。

 ソケットのお話は、以前致しましたが、ここではその仕組みをもっと掘り下げて見たいと思います。

 拡張ボードDLLの実際の開発は、その後になってしまいます。もう暫くお待ちください。


「DLLは自分でロードします」 by EmuZ-2000

 アプリケーションを作るときに、LIBファイルをあらかじめリンクしておく方式は一般的に良く使われる方式ですが
これでは、上の様なエラーが発生することがあります。
 くどいようですが、それはDLLが見つからなかったときです。
 EmuZ-2000では、この様なことを起こさずに、DLLを動的にリンクする「ソケット」方式を採用しています。

 自動的にロードしてくれるシステムに頼っていては、絶対に回避できませんので、自分でDLLをロードして来て、
そのDLLが持っているエントリーポイント(WINAPIで宣言されている関数のことです)を自分で見付けて、リンクを
行うといったことを行っています。

 さて、どうすればそんなことが出来るのでしょうか?
 前回の説明の「DllMain」のヘルプの文章をもう一度見てみましょう。

The DllMain function is an optional method of entry into a dynamic-link library (DLL).
If the function is used, it is called by the system when processes and threads are initialized
and terminated, or upon calls to the LoadLibrary and FreeLibrary functions.
(DllMain関数はダイナミックリンクライブラリ(DLL)へのエントリーポイントです。
この関数が呼び出されるタイミングは、プロセスまたはスレッドが作成、終了する時です。
この関数はWindowsシステムが呼び出すか、あるいはLoadLibraryおよびFreeLibraryによって呼び出されます。)

 察しの良い方はお分りになったかもしれません。
 この「Windowsシステムが呼び出すか、あるいはLoadLibraryおよびFreeLibraryによって呼び出されます」と言う
部分に注目してください。
 自動的にロードしてくれるシステムは、「Windowsシステムが呼び出すか」という部分を示しています。
 自分でロードする方法は「LoadLibraryおよびFreeLibraryによって呼び出されます」ということになります。
 ヘルプでこの2つの関数を調べて見てくださいね。

 自分でロードすると言うことはどういうことを行うのでしょうか?
 また、システムが自動でやってくれている事は、どのようなことを指しているのでしょうか?

 一つ一つ紐解いて見ましょう。

 DLLの様なライブラリを動的リンクすると言うことは、以下のようなことを全て行う必要があります。
 1.まずプロセス(アプリケーション)が使用しているメモリ空間に、DLLをロードする
 2.使用している関数全てに対してリンクをする
 3.プログラムが終了する際には、ロードしたものを開放する必要がある
 自動でやってくれるシステムを利用する際には、これらのことはシステムの仕事ですので、何もする必要はありません。
 逆に、自分でロードする場合には、システムは何もしてくれません。ここに注意が必要です。

 特に2番は重要です。
 ロードしてくることは、それほど大変なことではありません。
 しかし、リンクすると言うことは、少し高度なC言語の知識が必要になります。
 ただ、一度覚えてしまえば、それほどたいした問題にはなりませんので、ここで覚えて行ってくださいね。

 では、DLLのロードから行って見ましょう!
 LoadLibraryのヘルプを見て見ると分るように、この関数は HINSTANCE という型を返すようです。
 帰ってきた値が NULL であれば、ロードは失敗したことを意味します。
 では、これに則って、前回作ったDLLをロードする部分を作ってみましょう。

HINSTANCE  dllInstance ;
dllInstance = LoadLibrary( "TESTDLL.DLL" ) ;
if( NULL == dllInstance ){
    MessageBox( NULL, "DLLが見つかりません", "Beans Burn Sec.5", MB_OK ) ;
}
else {
  :
  :

}

 と、この様になります。全く簡単です^^;
 これが、1番目の実装になるのです。

 で、ここからが問題です。上記プログラムで言うと、elseの中に書かれるプログラムなのですが、
ここでは、2番目の「関数に対してリンクする」事を行わなくてはなりません。
 C言語の少し高度な使い方が必要です。先程も言いましたね^^

 これを覚えるためには、ポインタという仕組みを理解しておく必要があります。
 ポインタさえ理解していれば、それの応用ですので、ハードルは決して高くありません。

 通常、ポインタと言うのは、実態に対するアドレスを示しています。
 例えば、valと言う変数のポインタは &val と表現します。
 で、これを別の変数に入れるためには、ポインタ変数を用意してやればいいわけです。

int val = 100 ;
int *pval = &val ;
//
// valとpvalは同じ領域を指している。
// valは実体を、pvalは実体のアドレス(ポインタ)を指している
// *pvalとvalは同等である。

 これが、ポインタの基本です。
 さあ、ここからが応用です。
 値が入るのだったら、アドレスだって入るよね?と言うことを行います。
 つまり、変数に関数のアドレスを入れてやれば、もっと面白いことが出来るようになります。

int vvv( int a, int b )

 という関数のアドレスを格納した変数を作ってみましょう。

int (*pvvv)( int a, int b ) ;

 と、こんな変な形の宣言になります。
 これに、関数vvvのアドレスを入れてみます。

pvvv = vvv ;

 あれあれ?なんか普通の変数に値を入れるような感覚で使えるのですね。

 じゃあ、pvvvという関数のアドレスが入った変数を使って、関数を実行して見ます。

int ret ;
ret = (pvvv)( 1, 3 ) ;

 ほほう、面白い形ですね。
 本当に動くのかどうか、簡単なプログラムを作ってみましょう。

#include <stdio.h>

int vvv( int a, int b )
{
 printf( "a = %d, b = %d\n", a,b ) ;
 return 0 ;
}


void main( void )
{
 int (*pvvv)(int a, int b ) ;
 inr ret ;
 
 pvvv = vvv ;
 
 ret = (pvvv)( 1, 3 ) ;
}

 では、このプログラムをコンパイルして見てください。
 動かして見ると、「int vvv(int a, int b)関数」が呼ばれて動いていることが確認できると思います。

 この方式は、大変便利な使い方ですので、良く覚えておいてくださいね。
 絶対役に立ちますから!

 で、これを何に使うかって?
 そうそう、DLLがロードできれば、そのDLLが持っている関数にリンクしなければならないのですよね?
 それを今から行います。

 まず、関数のアドレスが入る変数から作りましょう。

BOOL (WINAPI *pdivide2)( DWORD val, DWORD *shou, DWORD *amari ) ;

 さあ、変数は作りました。
 次は、DLLの中にある関数のアドレスを引っ張り出して来ましょう!
 それには、「GetProcAddress」関数を使います。
 ヘルプで調べて見ましょう!

 では、この関数を使って、値を入れましょう!

(FARPROC)pdivide2 = (FARPROC)GetProcAddress( dllInstance, "Divide2" ) ;

 とまあ、こんな形になります。

 え?キャストは何かって? あぁ、(FARPROC)の事ですね!
 これはですね、無用なワーニングを出させないためのテクニックです。
 関数の形はまちまちです。それを FARPROC という形に統一させて、ワーニングが出ないようにしているのです。
 あくまでも、コンパイルするときにごまかせれば良いのですから、このようにキャストしています。
 実際に関数が呼ばれるときには、きちんと定義した形で呼ばれますので、ご安心下さい。

 では、先程のプログラムに組み込んで見ましょう!

HINSTANCE  dllInstance ;
BOOL (WINAPI *pdivide2)( DWORD val, DWORD *shou, DWORD *amari ) ;

dllInstance = LoadLibrary( "TESTDLL.DLL" ) ;
if( NULL == dllInstance ){
    MessageBox( NULL, "DLLが見つかりません", "Beans Burn Sec.5", MB_OK ) ;
}
else {
    (FARPROC)pdivide2 = (FARPROC)GetProcAddress( dllInstance, "Divide2" ) ;
}

 さて、良さそうな感じですが、このままでは不完全です。
 何が足りないのでしょうか?
 そうです!エラーの場合の救済措置です。

 以前、Stub(スタブ)という話をした事を覚えているでしょうか?
 Stubを使うと、安全性がググッと上がるのですから、これを使わない手は有りません。
 このことを踏まえて、プログラムを書いてみましょう。

HINSTANCE  dllInstance ;
BOOL (WINAPI *pdivide2)( DWORD val, DWORD *shou, DWORD *amari ) ;

BOOL
WINAPI
Stub_Divide2( DWORD val, DWORD *shou, DWORD *amari )
{
    return FALSE ;
}

dllInstance = LoadLibrary( "TESTDLL.DLL" ) ;
if( NULL == dllInstance ){
    MessageBox( NULL, "DLLが見つかりません", "Beans Burn Sec.5", MB_OK ) ;
}
else {
    (FARPROC)pdivide2 = (FARPROC)GetProcAddress( dllInstance, "Divide2" ) ;
}

 最後に行うべきは、DLLを開放することです。

 これは、先程調べていただいた「FreeLibrary」を使用します。
 使い方は最も簡単です。

FreeLibrary( dllInstance ) ;

 とまあ、これだけです。
 では、以上のことを踏まえて、アプリケーションを作って見ましょう。


アプリケーションの全ソースリストを表示しています。
前回のアプリケーションと違う部分を色分けしてあります。

/*************************/
/* TEST Application Sec.5*/
/*************************/
#include <windows.h>
#include <stdio.h>
#include "resource.h"

/************/
/* Valiable */
/************/
//関数アドレスが入る変数です
BOOL (WINAPI *pDivide2)( DWORD val, DWORD *syou, DWORD *amari ) ;
HINSTANCE  dllInstance ;

/************/
/*   Stub   */
/************/
//これがスタブ関数です。
//DLLが見つからなかったときに呼ばれます
BOOL
WINAPI
Stub_Divide2( DWORD val, DWORD *shou, DWORD *amari )
{
    return FALSE ;
}

//この関数が、ダイアログプロシージャです。ダイアログに起きた様々な事象を受け取ることが出来ます。
BOOL
CALLBACK
DlgProc( HWND hDlg, UINT msg, WPARAM wPrm, LPARAM lPrm )
{
 switch( msg ){
 case WM_INITDIALOG://今ダイアログが作られましたという報告が来た時に実行される部分です。
 {
  HWND hEditVal =   GetDlgItem( hDlg, IDC_EDIT1 ) ;
  HWND hEditShou =  GetDlgItem( hDlg, IDC_EDIT2 ) ;
  HWND hEditAmari = GetDlgItem( hDlg, IDC_EDIT3 ) ;

  SetWindowText( hEditVal,   "100" ) ;
  SetWindowText( hEditShou,  "0" ) ;
  SetWindowText( hEditAmari, "0" ) ;

  //ここで、DLLのロードとリンクを張っちゃいます。
  dllInstance = LoadLibrary( "TESTDLL.DLL" ) ;
  if( NULL == dllInstance ){
      MessageBox( NULL, "DLLが見つかりません", "Beans Burn Sec.5", MB_OK ) ;
      (FARPROC)pDivide2 = (FARPROC)Stub_Divide2 ;
  }
  else {
      (FARPROC)pDivide2 = (FARPROC)GetProcAddress( dllInstance, "Divide2" ) ;
  }
  
  return TRUE ;
 }
 case WM_COMMAND:
  switch( LOWORD(wPrm) ){
  case IDOK:  //OKボタンが押された時
  case IDCANCEL: //終了(x)が押された時
   //終わる前に、ロードしたDLLを開放しておきましょう
   if( NULL != dllInstance )
    FreeLibrary( dllInstance ) ;
   
   EndDialog( hDlg, TRUE ) ; //ダイアログに「終わりなさい」と指示します
   return TRUE ;
  case IDC_BUTTON://「DLLに計算させちゃう」ボタンが押された時に呼ばれます
   {
    HWND hEditVal =   GetDlgItem( hDlg, IDC_EDIT1 ) ;
    HWND hEditShou =  GetDlgItem( hDlg, IDC_EDIT2 ) ;
    HWND hEditAmari = GetDlgItem( hDlg, IDC_EDIT3 ) ;
    DWORD val, shou, amari ;
    TCHAR szVal[16] ;

    GetWindowText( hEditVal, (LPTSTR)szVal, 16 ) ;
    if( 0 == strlen( szVal )){
     MessageBox( hDlg, "元の数字を入力してね!", "Beans Burnよりメッセージ", MB_OK ) ;
    }
    else
    {
     sscanf( szVal, "%d", &val ) ;

     if( TRUE == (pDivide2)( val, &shou, &amari )) //ここで、DLLを呼んでいます!
     {
      //結果表示で〜す
      wsprintf( szVal, "%d", shou ) ;
      SetWindowText( hEditShou, szVal ) ;
      wsprintf( szVal, "%d", amari ) ;
      SetWindowText( hEditAmari, szVal ) ;
     }
     else
     {
      SetWindowText( hEditShou, "ERROR" ) ;
      SetWindowText( hEditAmari, "ERROR" ) ;
     }
    }
    break ;
   }
  }
 }
 return FALSE ;
}

//さあ、ここがアプリケーションの入り口です。
//ただ、ダイアログを表示させているだけですので、短いですね・・・
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPreInst,
                   LPSTR lpszCmdLine, int nCmdShow)
{
 BOOL ret ;
 ret =DialogBox( hInstance,
    MAKEINTRESOURCE(IDD_DIALOG1), NULL, DlgProc ) ;
 return ret ;
}

 修正出来ましたか?
 さて、ここでもう一仕事残っています。
 先程のプロジェクトには、「TESTDLL.LIB」がリンクされているのです。
 これが残っていると、アプリケーションが立ち上がるときに、DLLをシステム経由でロード
しようと試みてしまいます。
 プロジェクトから、これを排除しておきましょう。

 VCの左側に、エクスプローラの様なビューが見えると思います。FILEタブを押してを見てください。
 ここに「TESTDLL.LIB」と言うのが有るのが確認できます。
 それをマウスでクリックしてください。色が変わりましたね?
 ここで「Del」キーを押してください。これでプロジェクトから外すことが出来ました。

 それでは、「F5」キーを押して実行して見てください。
 DLLが無いと、「DLLが見つかりません」と表示されます。
 システムがエラーを出すのではなく、自分のプログラムでエラーと認識したのです。
 DLLが無いまま、計算させて見てください。結果のところに「ERROR」と表示されましたね?
 これは、Stubが呼ばれたことを意味しています。

 では、前回作ったDLLをコピーして来て下さい。
 前回と同じように、結果が正常に出れば正解です。


 これが、EmuZ-2000で使用しているDLLの使い方です。
 いかがだったでしょうか?
 関数のアドレスを、変数に入れるやり方は良く使われますし、とっても便利なので是非覚えて
おきましょうね。

 これで、基礎編は終了です。

 お待たせいたしました。
 次回から、ボードDLLを作っていきましょう!

 今回のテストアプリケーションプロジェクトは、ここからダウンロードできます。