11.6. Win32のDLLをビルド・利用する

HaskellライブラリをDLLにする機能はWindowsでは現在動作しない。我々は将来この機能を復活できるように望んでいる(4.11. 共有ライブラリを使うを見よ)。Haskellアプリケーション全体を単一のDLLとしてビルドするのには対応していることに注意。動作しないのは複数のDLLからなるHaskellプログラムである。GHCのWindows用配布物には静的ライブラリしか入っていない。

11.6.1. DLLを作成する

HaskellライブラリをDLLに封じ込めるのは簡単な作業である。ライブラリを構成するオブジェクトファイルをコンパイルして作り、次のようなコマンドを発行することでDLLをビルドする。

ghc –shared -o foo.dll bar.o baz.o wibble.a -lfooble

GHCのコンパイラ駆動器に–sharedを渡すと、実行ファイルを作る代わりにDLLをビルドする。そのDLLは、コマンド行で渡された全てのオブジェクトファイルとアーカイブから成る。

注意事項をいくつか。

  • –sharedを使う際、デフォルトでは、全てのオブジェクトファイルのエントリポイントがDLLからエクスポートされる。これを制限したいなら、次のようにして、コマンド行中でモジュール定義ファイルを指定することができる。

    ghc –shared -o .... MyDef.def
    

    詳細はMicrosoftの文書を参照してほしいが、モジュール定義ファイルは、エクスポートしたいエントリポイントの単なる羅列である。HaskellのCOMサーバDLLをビルドするのに使うものを挙げる。

    EXPORTS
     DllCanUnloadNow     = DllCanUnloadNow@0
     DllGetClassObject   = DllGetClassObject@12
     DllRegisterServer   = DllRegisterServer@0
     DllUnregisterServer = DllUnregisterServer@0
    
  • –sharedオプションは、DLLを作成するのに加え、インポートライブラリも作る。インポートライブラリの名前は、次のようにしてDLLの名前から作られる。

    DLL: HScool.dll  ==> import lib: libHScool.dll.a
    

    このような名前の付け方は少々奇妙に見えるかもしれないが、これはインポートライブラリと通常の静的ライブラリが共存できるようにするためである。(例えばlibHSfoo.alibHSfoo.dll.a)。さらに、コンパイラ駆動器が非静的モードの時、コマンド行中の-lHSfoo-lHSfoo_impに書き換えるので、非静的リンクから静的リンクに切り替えるのは、単に-staticをコマンド行に追加するだけで済む。

11.6.2. 他の言語から呼ぶためのDLLを作る

Haskellコードをまとめて、他の言語、例えばVisual BasicやC++から呼べるようにしたいなら、いくつか知っておくと良いことがある。これは8.2.1.2. 他言語のコードから呼べるようなHaskellライブラリを作るの特別な場合である。以下ではDLL特有の問題について扱う。例を挙げる。

  • foreign export宣言を使って、外部から呼びたいHaskellの関数をエクスポートする。例を挙げる。

    module Adder where
    
    adder :: Int -> Int -> IO Int  –– 余計なIO
    adder x y = return (x+y)
    
    foreign export stdcall adder :: Int -> Int -> IO Int
    
  • これをコンパイルする。

    ghc -c adder.hs -fglasgow-exts
    

    これで、adder.oとadder_stub.oの二つのファイルができる。

  • HaskellのRTSを起動するDllMain()をコンパイルする。実装の一例を示す。

    #include <windows.h>
    #include <Rts.h>
    
    extern void __stginit_Adder(void);
    
    static char* args[] = { "ghcDll", NULL };
                           /* 注意: argvはNULLで終わっていなければならない */
    BOOL
    STDCALL
    DllMain
       ( HANDLE hModule
       , DWORD reason
       , void* reserved
       )
    {
      if (reason == DLL_PROCESS_ATTACH) {
          /* この時点でRTSのDLLはロードされているはずだが、起動する必要がある。*/
          startupHaskell(1, args, __stginit_Adder);
          return TRUE;
      }
      return TRUE;
    }
    

    ここで、Adderはモジュールの木の根をなすモジュールの名前である。(上述のように、根となるモジュールはただ一つなければならない。したがって、DLLには一つのモジュール木がなければならない)。これをコンパイルする。

    ghc -c dllMain.c
    
  • DLLを構築する。

    ghc –shared -o adder.dll adder.o adder_stub.o dllMain.o
    
  • これでVBAからadderを使える。これは私なら次のようにDeclareする。

    Private Declare Function adder Lib "adder.dll" Alias "adder@8"
          (ByVal x As Long, ByVal y As Long) As Long
    

    このHaskell DLLはGHCに付属するいくつかのDLLに依存しているので、それらが可視であるように注意。

    静的にリンクされたDLLをビルドするのは前節の通りである。HaskellソースをコンパイルしてDLLをビルドするときに-staticを加えるだけで良い。

11.6.3. DllMain()に注意!

関数DllMain()の本体は極めて危険な場所である。これは、プロセスの終了時、DLLがアンロードされる順番が規定されていないことによる。つまり、あなたのDLLのDllMain()が呼ばれた時点で既に、あなたのDLLが脱初期化に際して呼ぶ関数の含まれているDLLがアンロードされた後かもしれないのである。言い換えると、終了処理のコードをDllMain()に置くことはできない。但し、終了処理のコードが、利用できることが保証されているある種の関数(詳しくはPlatform SDKを見よ)しか呼ばないなら別である。

解決策は、常にDLLから関数Begin()End()をエクスポートし、このDLLを使うアプリケーションからそれらを呼んで、End()関数にある終了処理コードが必要とする全てのDLLが、呼ばれた時点で利用可能状態であることを保証することである。

以下の例は、テストしていないが、この考え方を例証するものである。(この例に何か問題を見付けたり、もっと良い例があるなら、知らせてほしい)。BarZapという二つのHaskellモジュールを使うLewisというDLLがあるとしよう。ただし、BarZapをインポートしており、従って8.2.1.1. 自分で用意したmain()を使うの意味でルートモジュールであるとする。すると、このDLLの主要なC++単位はこのような感じになる。

 // Lewis.cpp -- GCCでコンパイルする
 #include <Windows.h>
 #include "HsFFI.h"

 #define __LEWIS_DLL_EXPORT
 #include "Lewis.h"

 #include "Bar_stub.h"  // GHCが生成したもの
 #include "Zap_stub.h"

 BOOL APIENTRY DllMain( HANDLE hModule, 
                        DWORD  ul_reason_for_call, 
                        LPVOID lpReserved
                       ){
   return TRUE;
 }

 extern "C"{

 LEWIS_API HsBool lewis_Begin(){
   int argc = ...
   char *argv[] = ...

   // Haskellランタイムを初期化する
   hs_init(&argc, &argv);

   // Haskellに、全てのルートモジュールについて通知する
   hs_add_root(__stginit_Bar);

   // ここで残り全部の初期化を行い
   // 問題があれば偽を返す
   return HS_BOOL_TRUE;
 }

 LEWIS_API void lewis_End(){
   hs_exit();
 }

 LEWIS_API HsInt lewis_Test(HsInt x){
   // BarやZapからエクスポートされた
   // Haskell関数を使う

   return ...
 }

 } // extern "C"

そして、このDLLの関数を使ったアプリケーションのmain()は以下のようになるだろう。

 // MyApp.cpp
 #include "stdafx.h"
 #include "Lewis.h"

 int main(int argc, char *argv[]){
   if (lewis_Begin()){
      // 以降、Lewis DLLからエクスポートされた
      // 他の関数を安全に呼べる

   }
   lewis_End();
   return 0;
 }

Lewis.hは、DLLの外部ユーザ(GHCをインストールしているとは限らないし、従ってHsFFI.hなどを持っているとは限らない)に対してもHaskell FFIの型が定義されているように、必要な#ifndefを持つことになるだろう。