Haskell中的foreign import stdcall用于DLL函数

11

这可能是一个很容易回答的问题,但由于某种原因,我感到非常困难。

我有一个用C语言编写的DLL,用于以协议级别访问硬件,并且我想编写一个Haskell程序来调用其中一些C函数。以下是相关C头文件的片段(由于可能存在版权问题,名称略微被混淆):

#ifdef HWDRIVER_EXPORTS
#define HWDRIVER_API __declspec(dllexport)
#else
#define HWDRIVER_API __declspec(dllimport)
#endif
HWDRIVER_API int HW_Init(void);

这个已经在Visual Studio 2003中编译为DLL,我已经成功地从C和C#中加载了DLL,所以我有信心DLL正常工作。DLL的名称为“hw-driver.dll”。

接下来,这是Haskell源代码,只是为了测试我是否可以正确加载DLL并调用其中最简单的函数:

{-# LANGUAGE ForeignFunctionInterface #-}
module Main
    where
import Foreign
import Foreign.C

foreign import stdcall "hw-driver" "HW_Init"  hwInit :: IO (CInt)

main = do
    x <- hwInit
    if x == 0 
        then putStr "Successfully initialized"
        else putStr "Could not initialize"

我遇到麻烦的是foreign import语句。据我所知,其语法为foreign (import/export) (ccall/stdcall) 库名 C函数名 Haskell函数名 :: Haskell类型声明。因此,我的应该是 foreign import stdcall(因为在Win32中加载DLL时要使用stdcall)"hw-driver"(因为文件名为"hw-driver.dll"并且位于与dlltest.hs相同的目录中) "HW_Init"(C中函数的名称) hwInit :: IO(Cint)(无参数,返回一个整数)。但是,当我尝试运行ghci dlltest.hs时,显示以下输出:

[1 of 1] Compiling Main             ( dlltest.hs, interpreted )

dlltest.hs:8:43: parse error on input `"'
Failed, modules loaded: none.

第8行第43列是HW_Init的第一个引号。好的,也许我需要将库名和函数名放在一个字符串中,我在一些地方看到过这样做。如果我尝试运行它,那么我会得到:

[1 of 1] Compiling Main             ( dlltest.hs, interpreted )

dlltest.hs:8:23: Malformed entity string
Failed, modules loaded: none.

8:23是新字符串"hw-driver HW_Init"的第一个引号。

我认为我的ghc设置(6.10.3)没有问题,因为我可以在ghci中运行从Real World Haskell复制粘贴的以下代码:

{-- snippet pragma --}
{-# LANGUAGE ForeignFunctionInterface #-}
{-- /snippet pragma --}

{-- snippet imports --}
import Foreign
import Foreign.C.Types
{-- /snippet imports --}

{-- snippet binding --}
foreign import ccall "math.h sin"
     c_sin :: CDouble -> CDouble
{-- /snippet binding --}

{-- snippet highlevel --}
fastsin :: Double -> Double
fastsin x = realToFrac (c_sin (realToFrac x))
{-- /snippet highlevel --}

{-- snippet use --}
main = mapM_ (print . fastsin) [0/10, 1/10 .. 10/10]
{-- /snippet use --}

简而言之,我该如何正确声明Win32 DLL上的外部导入?我在Google上找不到任何信息。

另外,是否能使用类似c2hs或hsc2hs这样的程序解析头文件hw-driver.h,以便我不必手动编写包含在该DLL中的20-25个函数的外部导入调用?我也找不到任何好的例子。


备注:ephemient指出外部导入行的正确语法是:

foreign import stdcall "hw-driver.h HW_Init" hwInit :: IO CInt

有了这个,我能够调用ghci dlltest.hs -lhw-driver并成功调用主函数并返回正确的返回码。 然而,命令ghc --make dlltest.hs -lhw-driver失败了,并出现了链接错误。 因此,这是该命令的详细输出(请注意,我在工作目录中拥有所有 hw-driver. {dll,h,lib}):

Glasgow Haskell Compiler, Version 6.10.3, for Haskell 98, stage 2 booted by GHC version 6.10.1
Using package config file: C:\ghc\ghc-6.10.3\package.conf
hiding package base-3.0.3.1 to avoid conflict with later version base-4.1.0.0
wired-in package ghc-prim mapped to ghc-prim-0.1.0.0
wired-in package integer mapped to integer-0.1.0.1
wired-in package base mapped to base-4.1.0.0
wired-in package rts mapped to rts-1.0
wired-in package haskell98 mapped to haskell98-1.0.1.0
wired-in package syb mapped to syb-0.1.0.1
wired-in package template-haskell mapped to template-haskell-2.3.0.1
wired-in package dph-seq mapped to dph-seq-0.3
wired-in package dph-par mapped to dph-par-0.3
Hsc static flags: -static
*** Chasing dependencies:
Chasing modules from: *dlltest.hs
Stable obj: [Main]
Stable BCO: []
Ready for upsweep
  [NONREC
      ModSummary {
         ms_hs_date = Mon Jun 22 13:20:05 Eastern Daylight Time 2009
         ms_mod = main:Main,
         ms_imps = [Foreign.C, Foreign]
         ms_srcimps = []
      }]
compile: input file dlltest.hs
Created temporary directory: C:\DOCUME~1\CHRISC~1\LOCALS~1\Temp\/ghc4428_0
*** Checking old interface for main:Main:
[1 of 1] Skipping  Main             ( dlltest.hs, dlltest.o )
*** Deleting temp files:
Deleting: C:\DOCUME~1\CHRISC~1\LOCALS~1\Temp\/ghc4428_0/ghc4428_0.s
Warning: deleting non-existent C:\DOCUME~1\CHRISC~1\LOCALS~1\Temp\/ghc4428_0/ghc4428_0.s
Upsweep completely successful.
*** Deleting temp files:
Deleting: 
link: linkables are ...
LinkableM (Mon Jun 22 13:22:26 Eastern Daylight Time 2009) main:Main
   [DotO dlltest.o]
Linking dlltest.exe ...
*** Windres:
C:\ghc\ghc-6.10.3\bin/windres --preprocessor="C:\ghc\ghc-6.10.3\gcc" "-BC:\ghc\ghc-6.10.3\gcc-lib/" "-IC:\ghc\ghc-6.10.3\include/mingw" "-E" "-xc" "-DRC_INVOKED" --use-temp-file --input=C:\DOCUME~1\CHRISC~1\LOCALS~1\Temp\/ghc4428_0/ghc4428_0.rc --output=C:\DOCUME~1\CHRISC~1\LOCALS~1\Temp\/ghc4428_0/ghc4428_0.o --output-format=coff
*** Linker:
C:\ghc\ghc-6.10.3\gcc -BC:\ghc\ghc-6.10.3\gcc-lib/ -IC:\ghc\ghc-6.10.3\include/mingw -v -o dlltest.exe -DDONT_WANT_WIN32_DLL_SUPPORT dlltest.o -lhw-driver C:\DOCUME~1\CHRISC~1\LOCALS~1\Temp\/ghc4428_0/ghc4428_0.o -LC:\ghc\ghc-6.10.3\base-4.1.0.0 -LC:\ghc\ghc-6.10.3\integer-0.1.0.1 -LC:\ghc\ghc-6.10.3\ghc-prim-0.1.0.0 -LC:\ghc\ghc-6.10.3 -LC:\ghc\ghc-6.10.3/gcc-lib -lHSbase-4.1.0.0 -lwsock32 -lmsvcrt -lkernel32 -luser32 -lshell32 -lHSinteger-0.1.0.1 -lHSghc-prim-0.1.0.0 -lHSrts -lm -lffi -lgmp -lwsock32 -u _ghczmprim_GHCziTypes_Izh_static_info -u _ghczmprim_GHCziTypes_Czh_static_info -u _ghczmprim_GHCziTypes_Fzh_static_info -u _ghczmprim_GHCziTypes_Dzh_static_info -u _base_GHCziPtr_Ptr_static_info -u _base_GHCziWord_Wzh_static_info -u _base_GHCziInt_I8zh_static_info -u _base_GHCziInt_I16zh_static_info -u _base_GHCziInt_I32zh_static_info -u _base_GHCziInt_I64zh_static_info -u _base_GHCziWord_W8zh_static_info -u _base_GHCziWord_W16zh_static_info -u _base_GHCziWord_W32zh_static_info -u _base_GHCziWord_W64zh_static_info -u _base_GHCziStable_StablePtr_static_info -u _ghczmprim_GHCziTypes_Izh_con_info -u _ghczmprim_GHCziTypes_Czh_con_info -u _ghczmprim_GHCziTypes_Fzh_con_info -u _ghczmprim_GHCziTypes_Dzh_con_info -u _base_GHCziPtr_Ptr_con_info -u _base_GHCziPtr_FunPtr_con_info -u _base_GHCziStable_StablePtr_con_info -u _ghczmprim_GHCziBool_False_closure -u _ghczmprim_GHCziBool_True_closure -u _base_GHCziPack_unpackCString_closure -u _base_GHCziIOBase_stackOverflow_closure -u _base_GHCziIOBase_heapOverflow_closure -u _base_ControlziExceptionziBase_nonTermination_closure -u _base_GHCziIOBase_blockedOnDeadMVar_closure -u _base_GHCziIOBase_blockedIndefinitely_closure -u _base_ControlziExceptionziBase_nestedAtomically_closure -u _base_GHCziWeak_runFinalizzerBatch_closure -u _base_GHCziTopHandler_runIO_closure -u _base_GHCziTopHandler_runNonIO_closure -u _base_GHCziConc_runHandlers_closure -u _base_GHCziConc_ensureIOManagerIsRunning_closure
Reading specs from C:/ghc/ghc-6.10.3/gcc-lib/specs
Configured with: ../gcc-3.4.5-20060117-3/configure --with-gcc --with-gnu-ld --with-gnu-as --host=mingw32 --target=mingw32 --prefix=/mingw --enable-threads --disable-nls --enable-languages=c,c++,f77,ada,objc,java --disable-win32-registry --disable-shared --enable-sjlj-exceptions --enable-libgcj --disable-java-awt --without-x --enable-java-gc=boehm --disable-libgcj-debug --enable-interpreter --enable-hash-synchronization --enable-libstdcxx-debug
Thread model: win32
gcc version 3.4.5 (mingw-vista special r3)
 C:/ghc/ghc-6.10.3/gcc-lib/collect2.exe -Bdynamic -o dlltest.exe -u _ghczmprim_GHCziTypes_Izh_static_info -u _ghczmprim_GHCziTypes_Czh_static_info -u _ghczmprim_GHCziTypes_Fzh_static_info -u _ghczmprim_GHCziTypes_Dzh_static_info -u _base_GHCziPtr_Ptr_static_info -u _base_GHCziWord_Wzh_static_info -u _base_GHCziInt_I8zh_static_info -u _base_GHCziInt_I16zh_static_info -u _base_GHCziInt_I32zh_static_info -u _base_GHCziInt_I64zh_static_info -u _base_GHCziWord_W8zh_static_info -u _base_GHCziWord_W16zh_static_info -u _base_GHCziWord_W32zh_static_info -u _base_GHCziWord_W64zh_static_info -u _base_GHCziStable_StablePtr_static_info -u _ghczmprim_GHCziTypes_Izh_con_info -u _ghczmprim_GHCziTypes_Czh_con_info -u _ghczmprim_GHCziTypes_Fzh_con_info -u _ghczmprim_GHCziTypes_Dzh_con_info -u _base_GHCziPtr_Ptr_con_info -u _base_GHCziPtr_FunPtr_con_info -u _base_GHCziStable_StablePtr_con_info -u _ghczmprim_GHCziBool_False_closure -u _ghczmprim_GHCziBool_True_closure -u _base_GHCziPack_unpackCString_closure -u _base_GHCziIOBase_stackOverflow_closure -u _base_GHCziIOBase_heapOverflow_closure -u _base_ControlziExceptionziBase_nonTermination_closure -u _base_GHCziIOBase_blockedOnDeadMVar_closure -u _base_GHCziIOBase_blockedIndefinitely_closure -u _base_ControlziExceptionziBase_nestedAtomically_closure -u _base_GHCziWeak_runFinalizzerBatch_closure -u _base_GHCziTopHandler_runIO_closure -u _base_GHCziTopHandler_runNonIO_closure -u _base_GHCziConc_runHandlers_closure -u _base_GHCziConc_ensureIOManagerIsRunning_closure C:/ghc/ghc-6.10.3/gcc-lib/crt2.o C:/ghc/ghc-6.10.3/gcc-lib/crtbegin.o -LC:\ghc\ghc-6.10.3\base-4.1.0.0 -LC:\ghc\ghc-6.10.3\integer-0.1.0.1 -LC:\ghc\ghc-6.10.3\ghc-prim-0.1.0.0 -LC:\ghc\ghc-6.10.3 -LC:\ghc\ghc-6.10.3/gcc-lib -LC:/ghc/ghc-6.10.3/gcc-lib dlltest.o -lhw-driver C:\DOCUME~1\CHRISC~1\LOCALS~1\Temp\/ghc4428_0/ghc4428_0.o -lHSbase-4.1.0.0 -lwsock32 -lmsvcrt -lkernel32 -luser32 -lshell32 -lHSinteger-0.1.0.1 -lHSghc-prim-0.1.0.0 -lHSrts -lm -lffi -lgmp -lwsock32 -lmingw32 -lgcc -lmoldname -lmingwex -lmsvcrt -luser32 -lkernel32 -ladvapi32 -lshell32 -lmingw32 -lgcc -lmoldname -lmingwex -lmsvcrt C:/ghc/ghc-6.10.3/gcc-lib/crtend.o
C:\ghc\ghc-6.10.3\gcc-lib\ld.exe: cannot find -lhw-driver
collect2: ld returned 1 exit status
*** Deleting temp files:
Deleting: C:\DOCUME~1\CHRISC~1\LOCALS~1\Temp\/ghc4428_0/ghc4428_0.o C:\DOCUME~1\CHRISC~1\LOCALS~1\Temp\/ghc4428_0/ghc4428_0.rc
*** Deleting temp dirs:
Deleting: C:\DOCUME~1\CHRISC~1\LOCALS~1\Temp\/ghc4428_0


事实证明,实际的链接并没有我想象中那么困难。我一直在使用 foreign import stdcall,并认为这是 Visual Studio 2003 中构建的 DLL 的正确方法。我不得不下载 MinGW 的 pexports 工具,它列出了从 DLL 导出的函数。链接器一直在寻找 HWInit@0,但 pexports 却说 DLL 只导出了 HWInit。

我把我的代码行改成了 foreign import ccall,然后我成功地使用以下命令之一链接程序:ghc --make dlltest.hs hw-driver.lib 或者 ghc --make dlltest.hs -L. -lhw-driver,因为 .lib 和 .dll 文件都在工作目录中可用。

2个回答

5

FFI规范 # 4.1.1 导入声明,

impent → " [static] [chname] [&] [cid] "
            | " dynamic "
            | " wrapper "

其中chname是"C头文件名",不是"库名"。

FFI规范 # 4.1.4 头文件的规定

在导入声明中指定的C头文件始终被包含在#include "chname"中。没有明确支持#include <chname>样式的包含方式。ISO C99[3]标准保证任何用于#include <chname>的搜索路径也用于#include "chname",并保证这些路径在所有独特于#include "chname"的路径之后搜索。此外,我们要求chname.h结尾,以使解析外部实体规范不含糊。

请使用正确的头文件名进行尝试,

foreign import stdcall "hw-driver.h HW_Init" hwInit :: IO CInt

或者干脆不指定标题名称。
foreign import stdcall "HW_Init" hwInit :: IO CInt

您的命令行似乎没有将 . 包含在库搜索路径中。这很可能是问题所在。GHCi会自动将 . 包含在库搜索路径中。
ghc --make dlltest.hs -L. -lhwdriver
如果仍然失败,也许是静态库引起了问题。虽然不太可能,但还是要试一试...
Windows上的GHC默认使用动态链接。由于您有一个 .lib ,它是一个静态库,请尝试通知链接器您想要进行静态链接。
ghc --make dlltest.hs -L. -optl-Bstatic -lhwdriver -optl-Bdynamic
关于自动生成的绑定,有以下三种方法: 我发现C->Haskell最容易使用,但我从未在需要 stdcall 的任何内容上尝试过。
如果只有大约25个调用,手动编写所有 foreign 内容并不是很麻烦。几年前,我曾手动编写过 libvlc 的绑定,用于一些小项目...

ghci dlltest.hs -lhw-driver让我能够在ghci中运行主函数,但是我在gcc编译方面遇到了问题:C:\temp\hs>ghc --make dlltest.hs -lhw-driver Linking dlltest.exe ... C:\ghc\ghc-6.10.3\gcc-lib\ld.exe: cannot find -lhw-driver collect2: ld returned 1 exit status这对我来说非常奇怪,因为它在ghci中正常工作。我将继续尝试解决这个问题。 - Mark Rushakoff
1
GHCi不使用ld,而是实现了自己的链接器。更常见的情况是库可以在编译时使用,但不能在交互式环境中使用,除非使用hackish的解决方法,但这种反向情况似乎也很可能发生。你能否使用-v运行,并发布ghc运行的中间命令? - ephemient
-L.选项将我的输出从“无法找到-lhw-driver”更改为“对'HW_Init@0'的未定义引用”,但是两个建议都没有使我成功链接。在http://www.nabble.com/OpenVG:Linker-errors-with-ghc---make,-but-not-with-ghci--td22321487.html找到了类似的问题,但没有有用的答案。可能明天就要加入GHC邮件列表了……看起来这只是某个地方链接器选项的问题。我相信我不是第一个使用ld链接DLL的人。 - Mark Rushakoff
我基本上只使用Linux,而且GHC与.a和.so文件链接对我来说非常顺利,所以任何进一步的帮助都将是我个人的猜测。抱歉,希望你能找到答案。 - ephemient
如果不是为了工作,我根本不会使用Windows :X你帮我确定了最初的问题,所以我接受了你的答案,并且稍后会开一个新问题来讨论奇怪的ld行为。谢谢。 - Mark Rushakoff

3
以下是一个可工作的示例,调用来自kernel32.dll的[GetComputerName](http://msdn.microsoft.com/en-us/library/ms724295(VS.85).aspx)
{-# LANGUAGE ForeignFunctionInterface #-}

module Main where

import Control.Monad
import Foreign.C
import Foreign.Marshal.Alloc
import Foreign.Marshal.Array
import System.Win32.Types

foreign import stdcall "GetComputerNameW"
  win32_getComputerName :: LPTSTR -> LPDWORD -> IO Bool

getComputerName :: IO String
getComputerName = do
  withTString maxBuf $
    \buf -> do
      alloca $ \len -> do
        pokeArray len [fromIntegral maxLength]

        success <- win32_getComputerName buf len
        when (not success) $ fail "GetComputerName failed"

        [len'] <- peekArray 1 len
        peekTStringLen (buf, (fromIntegral len'))
  where
    maxBuf = take maxLength $ repeat 'x'
    maxLength = 15  -- cheating

main :: IO ()
main = getComputerName >>= putStrLn

使用它来构建

ghc --make compname.hs -lkernel32

1
这将C:\windows\system32路径硬编码到输出可执行文件中,我认为这并不理想。 - ephemient
那请提供一个可取的替代方案吧! - Greg Bacon
1
确保库搜索路径正确(尽管该路径应已被搜索),然后使用-lkernel32 - ephemient
1
谢谢指针。使用-Lc:/windows/system32,我得到了大量未定义引用错误。省略-L参数让所有人都很高兴。简单易行,我想。 - Greg Bacon

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接