如何从 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 个有帮助:
- 从 package.yaml
中删除了 "executables" 和 "test"
在 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
- 完全删除了主模块(即从 "app" 文件夹中删除了堆栈自动创建的 Main.hs)
- 我在 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 可以看到的导入库。
我们使用两个工具来帮助我们:gendef
和 dlltool
,它们都在您的 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-dirs
和 extra-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# 方面来看还有更多:正确的参数编组等,我还必须处理这些才能使我的结果正确。我唯一可以涵盖所有这些细节的地方是博客 :-)
请随时交叉验证我的解决方案,并对任何更好的方法发表评论。这是我经过一番努力后想出的最好方法!
我正在尝试从我的 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 个有帮助:
- 从 package.yaml 中删除了 "executables" 和 "test"
在 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
- 完全删除了主模块(即从 "app" 文件夹中删除了堆栈自动创建的 Main.hs)
- 我在 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 可以看到的导入库。
我们使用两个工具来帮助我们:gendef
和 dlltool
,它们都在您的 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-dirs
和extra-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# 方面来看还有更多:正确的参数编组等,我还必须处理这些才能使我的结果正确。我唯一可以涵盖所有这些细节的地方是博客 :-)
请随时交叉验证我的解决方案,并对任何更好的方法发表评论。这是我经过一番努力后想出的最好方法!