Haskell で作成した DLL を秀丸マクロから呼び出す

秀丸Haskell 開発環境をパワーアップさせるために、マクロから呼び出す DLL を Haskell で書ければハッピーになれると思い、調べた結果をまとめておきます。

環境
はじめに

Haskell で DLL を作成するのは、過去においては WindowsGHC の特権だったようですが、今では他のプラットフォームでも共有ライブラリの作成ができるようです。Windows 版の DLL 作成方法は公式ドキュメント(13.6. Building and using Win32 DLLs)にも記載されています。ただし、このドキュメントは内容が古く、この手順通りに進めることができません。具体的には *_stub.o というオブジェクトは生成されません。

Haskell の実装
-- HsDLL.hs
{-# LANGUAGE ForeignFunctionInterface #-}

module HsDLL where

import Foreign.C.String
import Foreign.Marshal.Alloc
import Foreign.Ptr (nullPtr)

foreign import stdcall "windows.h SysAllocString" cSysAllocString :: CWString -> IO CWString
foreign export stdcall hsConcat :: CWString -> CWString -> IO CWString
foreign export stdcall hsAdd :: Int -> Int -> IO Int

hsConcat :: CWString -> CWString -> IO CWString
hsConcat a b = do
    s1 <- toHsString a
    s2 <- toHsString b
    cstr <- newCWString (s1 ++ s2)
    ret <- cSysAllocString cstr
    free cstr
    return ret
  -- peekCWString に NULLを与えるとクラッシュするため、NULLなら、空文字列に置換
  where toHsString s = if s == nullPtr
                       then return ""
                       else peekCWString s

hsAdd :: Int -> Int -> IO Int
hsAdd a b = return (a + b)

文字列連結と整数加算の Haskell 実装です。文字列連結の実装は Haskell で文字列型を使ったDLLの作成例 - happynowの日記 を流用させて頂きました。


コンパイルは以下のように行います。

ghc -c HsDLL.hs
C による補助コードの実装

Haskell の補助コードを C で実装します。

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

static void HsStart() {
    char *argv[] = {"ghcDll", NULL}, **args = argv;
    int argc = sizeof(argv) / sizeof(argv[0]) - 1;

    hs_init(&argc, &args);
}

static void HsEnd() {
    hs_exit();
}

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) {
    switch(fdwReason) {
    case DLL_PROCESS_ATTACH:
        HsStart();
        break;

    case DLL_PROCESS_DETACH:
        HsEnd();
        break;

    case DLL_THREAD_ATTACH:
        break;

    case DLL_THREAD_DETACH:
        break;
    }
    return  TRUE;
}

このコードにはいろいろお約束があり、まず argv は最初の要素が "ghcDll" であること、最後は NULL であること、であるにもかかわらず NULL 以外の要素数を初期化関数に渡さなければならないこと、その際ポインタで渡すことなどです。

上記のような感じで DLLMain 関数を実装しておくと秀丸マクロから使用する際に幸せになれるはずです。


コンパイルは以下のように行います。

ghc -c StartEnd.c
エクスポート関数定義ファイル

エクスポートする関数を以下のように HsDLL.def ファイルで定義します。

EXPORTS
  hsAdd      = hsAdd@8
  hsConcat   = hsConcat@8

これを定義しないと、秀丸から呼び出すことができません。逆に、これを定義することで C などから呼び出すのが面倒になります。

DLL のビルド

以下のようにビルドします。

ghc -shared -o HsDLL.dll HsDLL.o StartEnd.o HsDLL.def -loleaut32

これで HsDLL.dll が生成されます。これを秀丸マクロから呼び出します。

秀丸マクロによる動作確認コード実装

動作確認を秀丸マクロで行います。

// HsDLLTest.mac
loaddll "HsDLL.dll";

if (!result) {
	message "HsDLL.dllのロードに失敗しました。";
	endmacro;
}

#a = dllfunc("hsAdd", 2, 5);
message "2 + 5 = " + str(#a);

$b = dllfuncstrw("hsConcat", "hoge", "fuga");
message "hoge + fuga = " + $b;

freedll;

最近の秀丸で DLL に Unicode が渡せるようになっていて助かりました。

64bit 版秀丸に対応する場合は、いろいろ苦労することになると思います(そもそも Haskell で 64bit 版 DLL が生成できるかどうかわかりません)。