如何从 Haskell link 到 C#(即托管)DLL?

How to link to a C# (i.e., managed) DLL from Haskell?

我正在尝试从我的 Haskell 代码构建一个 Windows DLL。此 DLL 中的函数应该从 C# 中的托管代码调用。并且,至少一个函数(在 c# 代码中定义)将从此 DLL 中的函数调用。

冒着过度解释的风险,这里有一张小图来描述我想要的:

+----------------------+           +------------------------+
|   Managed C# code    |           |  Haskell code (in DLL) |
|                      |     (1)   |                        |
|  fn_calling_hs()  -----------------> fn_called_from_cs()  |   
|                      |           |                        |
|                      |           |                        |          
| fn_called_from_hs() <--------------- fn_calling_cs()      |
|                      |     (2)   |                        |
+----------------------+           +------------------------+

我设法使 (1) 完美运行,即 DLL 中的一个 Haskell 函数被 C# 代码调用,结构和数组的编组正确,函数执行的结果在Haskell 也是正确的。到目前为止,还不错。

问题在于 (2),即来自 Haskell(在 DLL 中)的函数调用在 C# 中定义的托管函数。问题出在构建本身——我还没有超越它来实际检查 (2) 的结果。

由于c#托管代码中的fn_called_from_hs()是在C#中定义的,所以我在Haskell代码中(在DLL中)只有函数符号"imported":

foreign import ccall fn_called_from_hs :: IO CString

现在,当我用堆栈构建我的 Haskell 项目时,它构建 Haskell DLL 没有问题,但构建继续 link "main.exe" - 并且这失败了(很明显),因为在 Haskell 代码中的任何地方都没有定义函数 fn_called_from_hs() (它是在 c# 中定义的)。

有什么方法可以阻止堆栈在构建 HsDLL.dll 后继续构建 main.exe?我同意 HsDLL.dll 具有未解析的符号 (fn_called_from_hs()),因为在托管 C# 代码加载此 DLL 期间,运行时 linker 将找到此符号。

到目前为止,我已经尝试了这些步骤,但其中 none 个有帮助:

  1. 从 package.yaml
  2. 中删除了 "executables" 和 "test"
  3. 在 package.yaml 中添加了 GHC 选项:-no-hs-main。 package.yaml 包含 HsDLL 构建的部分如下所示:

    library:   
      source-dirs: 
      - src
      - src/csrc   
      include-dirs: src/csrc   
      ghc-options:
      - -shared
      - -fno-shared-implib 
      - -no-hs-main
    
    1. 完全删除了主模块(即从 "app" 文件夹中删除了堆栈自动创建的 Main.hs)
    2. 我在 ghc-options 中添加了 -dynamic 标志,希望 GHC 假定未解析的符号将在别处定义,但这带来了其他问题:GHC 现在抱怨它需要 "dyn" 库库等

所以,最后,我总是这样结束:

PS C:\workspace\Haskell\hscs\src\csrc> stack build
hscs-0.1.0.0: configure (lib)
Configuring hscs-0.1.0.0...
hscs-0.1.0.0: build (lib)
Preprocessing library for hscs-0.1.0.0..
Building library for hscs-0.1.0.0..
Linking main.exe ...
.stack-work\distc8418a7\build\HsLib.o:fake:(.text+0x541): undefined reference to `fn_called_from_hs'
collect2.exe: error: ld returned 1 exit status
`gcc.exe' failed in phase `Linker'. (Exit code: 1)

--  While building custom Setup.hs for package hscs-0.1.0.0 using:
      C:\tools\HaskellStack\setup-exe-cache\x86_64-windows\Cabal-simple_Z6RU0evB_2.0.1.0_ghc-8.2.2.exe --builddir=.stack-work\distc8418a7 build lib:hscs --ghc-options " -ddump-hi -ddump-to-file -fdiagnostics-color=always"
    Process exited with code: ExitFailure 1

所以,我的问题是: (1) 我完全不知道如何停止 linking "main.exe"!我知道函数 fn_called_from_hs() 没有在 HsDLL 中定义,但是,正如我所说,我没问题,因为它是在托管 C# 代码中定义的。我只想main.exe不建

(2) 我是否应该继续向 GHC 添加 -dynamic 标志(保留所有其他标志如上)?在这种情况下,如何获取堆栈以安装 GHC 抱怨的 "dyn" 库?

有人可以帮助我吗?预先感谢您耐心阅读这个(相当)长的问题!

最后,我自己解决了这个问题!经过一周的奋斗,就是这样。欢迎任何有用的评论添加这个答案。

我是这样做的:

在 C# 中 class DLL:

我必须找到一种方法 "export" 我的函数 fn_called_from_hs() 不安全的本机代码。我发现这并不是很简单,互联网上确实有很多文章来解释这是如何完成的。一切相当于通过工具ildasm实际反汇编.NET DLL,并在生成的中间IL文件中,为我们要导出的函数添加“.export”前缀,然后再次组装IL文件使用 ilasm 到 DLL 形式。

我发现所有这些步骤都是由 NUGetPackage Unmanaged Exports 自动完成的,所以第一步是将此包安装为 .NET 项目的一部分,然后将 DLLExport 属性添加到您要导出的函数。确保您的导入列表中有 RGiesecke.DllExport

using RGiesecke.DllExport;

[DllExport("fn_called_from_hs", CallingConvention=CallingConvention.Cdecl)]
public static string FnCalledFromHs()
{
      // Your function code here
}

如您所见,我将实际函数命名为 FnCalledFromHs()(按照 C# 中的命名约定),但导出的函数与 fn_called_from_hs 相同(按照命名约定Haskell 中的惯例)。这样,当您查看 Haskell 代码时,您将不会看到任何看起来不合适的内容。

要使其真正起作用,最重要的步骤之一是确保将导出函数的项目设为目标 x64 或 x86 - 默认情况下,项目目标 "Any CPU" - [如果项目目标为 "Any CPU".

,则 =20=] 不起作用

现在构建项目以获取 csharp.dll,其中包含您导出的 fn_called_from_hs.

在 linking Haskell 代码之前

Mingw GCC(Windows 上的 ghc 内部使用)实际上可以直接 link 使用 DLL,前提是它们之前是用 gcc 创建的。但是,由于我们已经使用 .NET 编译器 csc 创建了 C# DLL,因此我们需要专门创建一个我们的 Haskell 可以看到的导入库。

我们使用两个工具来帮助我们:gendefdlltool,它们都在您的 ghc 安装中的 "mingw\bin" 文件夹中(因此,当然,您需要在你的 PATH env 变量中有这个来访问这些工具)。

我是这样处理的:

  • 创建了一个 .def 文件,该文件又可用于创建导入库:

    gendef csharp.dll
    
  • 使用 dlltool 创建了导入库:

    dlltool -k -d csharp.def -l csharp.lib
    
  • 将上述导入库复制到 DLL 所在的同一目录。

最后一步(下方)现在将使用此导入库实际 linking with csharp DLL。

链接Haskell代码与上面的导入库

这有点棘手,可能让我遇到了堆栈/GHC 中的错误(不确定),但已经 filed here

我是这样处理的:

  • 在我的stack.yaml中添加了extra-lib-dirs,并添加了上面创建import-lib的目录:

    extra-lib-dirs: ["<drive>:\path\to\importlib"]
    

(请注意,这也可以添加到 "libraries" 下的 package.yaml,但我选择将其添加到我的 stack.yaml 中)。

  • extra-libraries 添加到我的 stack.yaml,在库下。

    extra-libraries: csharp
    
  • 并且,还在我的 ghc-options 中添加了选项 -l 和 -L 用于 linking 我的库。这就是我为规避 (可能的)bug 所做的,堆栈不知何故没有通过 extra-lib-dirsextra-libraries在 linking 期间到 ghc 和 ld。所以,我在 package.yaml 中的最后 "library" 部分看起来像这样(将其与我上面的问题之前的情况进行比较):

     library:
       source-dirs: 
       - src
       - src/csrc
       include-dirs: src/csrc
       ghc-options:
       - -shared
       - -fno-shared-implib
       - -lcslib
       - -L<drive>:\path\to\importlib
       extra-libraries: csharp
    

结论

完成所有这些后,我的 Haskell 代码现在可以使用正常的 stack build 命令轻松构建,没有任何 "unreferenced symbols" 错误。在执行我的 Haskell 代码时,我还检查了 c# 函数 fn_called_from_hs 是否实际被调用,结果是否正确返回。

当然,从 C# 方面来看还有更多:正确的参数编组等,我还必须处理这些才能使我的结果正确。我唯一可以涵盖所有这些细节的地方是博客 :-)

请随时交叉验证我的解决方案,并对任何更好的方法发表评论。这是我经过一番努力后想出的最好方法!