9

我正在尝试从我的 Haskell 代码构建一个 Windows DLL。应该从 C# 中的托管代码调用此 DLL 中的函数。并且,至少有一个函数(在 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_call_from_hs() 是在 C# 中定义的,因此我在 Haskell 代码(在 DLL 中)中只有“导入”的函数符号:

foreign import ccall fn_called_from_hs :: IO CString

现在,当我使用堆栈构建我的 Haskell 项目时,它构建 Haskell DLL 没有问题,但构建继续也链接“main.exe” - 这失败(显然),因为在任何地方都没有定义函数 fn_call_from_hs() Haskell 代码(在 c# 中定义)。

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

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

  1. 从 package.yaml 中删除了“可执行文件”和“测试”
  2. 添加了 GHC 选项:-no-hs-main在 package.yaml 中。包含 HsDLL 构建的 package.yaml 部分如下所示:

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

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

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\dist\5c8418a7\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\dist\5c8418a7 build lib:hscs --ghc-options " -ddump-hi -ddump-to-file -fdiagnostics-color=always"
    Process exited with code: ExitFailure 1

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

或者

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

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

4

1 回答 1

7

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

我这样做如下:

在 C# 类 DLL 中:

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

我发现所有这些步骤都是由 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
}

如您所见,我将实际函数命名为(根据 C# 中的命名约定),但导出了与(根据 Haskell 中的命名约定)FnCalledFromHs()相同的函数。fn_called_from_hs这样,当您查看 Haskell 代码时,您不会看到任何不合适的地方。

实际工作的最重要步骤之一是确保导出函数的项目以 x64 或 x86 为目标 - 默认情况下,项目目标为“任何 CPU” -RGiesecke.DllExport如果项目目标不工作“任何 CPU”。

现在构建项目以获取包含您导出的csharp.dllfn_called_from_hs的项目。

在链接 Haskell 代码之前

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

我们使用两个工具来帮助我们:gendefdlltool,它们都位于 ghc 安装中的“mingw\bin”文件夹中(因此,当然,您需要在 PATH 环境变量中包含它才能访问这些工具)。

这是我的做法:

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

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

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

最后一步(如下)现在将使用这个导入库来实际链接 csharp DLL。

将 Haskell 代码与上述导入库链接

这有点棘手,可能让我在堆栈/GHC 中遇到了一个错误(不确定),但已经在此处提交

我这样做如下:

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

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

(请注意,这也可以添加到“库”下的 package.yaml 中,但我选择将它放在我的 stack.yaml 中)。

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

    extra-libraries: csharp
    
  • 并且,还将选项 -l 和 -L 添加到我的 ghc-options 以链接我的库。这就是我为规避堆栈在链接期间未将and传递给 ghc 和 ld的(可能的)错误所做的。所以,我在 package.yaml 中的最后一个“库”部分看起来像这样(将它与我上面的问题之前的情况进行比较):extra-lib-dirsextra-libraries

     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,没有任何“未引用符号”错误。在执行我的 Haskell 代码时,我还检查了 c# 函数fn_called_from_hs是否被实际调用,并且结果正确返回。

当然,c# 方面还有更多内容:正确编组参数等,我还必须在这些方面进行工作以使我的结果正确。我唯一能涵盖所有这些细节的地方是在博客中:-)

请随时交叉验证我的解决方案,并评论任何更好的方法。这是我在挣扎之后能想出的最好办法!

于 2018-07-06T05:32:50.503 回答