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

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

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をコマンド行に追加するだけで済む。

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

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

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

-- Adder.hs
{-# LANGUAGE ForeignFunctionInterface #-}
module Adder where

adder :: Int -> Int -> IO Int -- 余計なIO
adder x y = return (x+y)

foreign export stdcall adder :: Int -> Int -> IO Int

HaskellのRTSの開始/終了を行う補助コードを加える。

// StartEnd.c
#include <Rts.h>

extern void __stginit_Adder(void);

void HsStart()
{
   int argc = 1;
   char* argv[] = {"ghcDll", NULL}; // argvはNULLで終わっていなければならない

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

   // Haskellに全ての根モジュールを通知する
   hs_add_root(__stginit_Adder);
}

void HsEnd()
{
   hs_exit();
}

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

ghc -c Adder.hs
ghc -c StartEnd.c
ghc -shared -o Adder.dll Adder.o Adder_stub.o StartEnd.o

これで、Adder.dllファイルが他の言語から使えるようになった。Adderの関数を呼ぶ前にHsStartを呼ぶことが必要であり、一番最後にはHsEndを呼ぶ必要がある。

警告: hs_init/hs_exitを呼ぶのにDllMainを使うことが魅力的に思えるかもしれないが、これは動作しない(特に-threaded付きでコンパイルしているなら)。DllMainの間に実行してよい動作には厳しい制約があり、hs_initはこれらの制約を破るので、セットアップ中にdllがフリーズするのにつながることがある。(bug #3605を見よ)

VBAから使う

Adder.dllをVBAから使う例。

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

Private Declare Sub HsStart Lib "Adder.dll" ()
Private Declare Sub HsEnd Lib "Adder.dll" ()

Private Sub Document_Close()
HsEnd
End Sub

Private Sub Document_Open()
HsStart
End Sub

Public Sub Test()
MsgBox "12 + 5 = " & Adder(12, 5)
End Sub

この例はMicrosoft WordのDocument_Open/Close関数を使っているが、HsStartが関数が最初に呼ばれるよりも先に呼ばれ、HsEndが最後のものより後に呼ばれる限り、問題なく動作するはずである。

C++から使う

Adder.dllをC++から使う例。

// Tester.cpp
#include "HsFFI.h"
#include "Adder_stub.h"
#include <stdio.h>

extern "C" {
    void HsStart();
    void HsEnd();
}

int main()
{
    HsStart();
    // これで、DLLの関数を安全に呼べる
    printf("12 + 5 = %i\n", adder(12,5))    ;
    HsEnd();
    return 0;
}

これは次のようにコンパイルし、実行できる。

$ ghc -o tester Tester.cpp Adder.dll.a
$ tester
12 + 5 = 17