从C#调用Haskell

46

我刚刚花费了最后一周的时间来解决如何将C++代码作为我的日常工作的一部分从C#中执行。我们花费了很长时间才弄清楚,但最终的解决方案相当简单。

现在我很好奇...从C#调用Haskell有多难?(请注意:这是从C#调用Haskell,而不是反过来。因此,主要可执行文件是C#。)

如果确实很难,那我就不烦恼了。但是如果它相对容易,我可能会尝试一下...

基本上,我们编写了一些C ++代码。 在Windows上,它被编译为DLL,在Linux上,它被编译为共享对象( * .so )。 然后在C#端,您使用DllImport 并编写一些手动内存管理代码,如果您正在尝试传递任何非平凡的内容(例如数组、字符串等)。

我知道GHC应该支持在两个平台上构建共享库,但我不确定技术细节。 导出东西的语法是什么,调用者是否需要执行任何特殊操作来初始化DLL呢?

具体地说:假设存在一个函数 foobar :: FilePath -> IO Int32 。 有人能否构建一个小草图,展示:

  • 我需要编写哪些Haskell声明才能将其暴露给外部世界。
  • 如何告诉GHC构建一个单独的自包含DLL / SO文件。
  • 除绑定 foobar 本身的通常流程之外,调用者还需要做任何特殊的事情吗?

我不太担心C#方面的实际语法; 我认为我已经更多或更少地将其解决了。

附言:我简要查看了 hs-dotnet ,但这似乎是特定于Windows的。(即,无法在Mono上工作,因此无法在Linux上工作。)


5
对于FFI(外部函数接口)绑定,您总是可以拥有备选方案——“编写C的薄包装器”。几乎所有具有任何类型的FFI的语言都可以与C进行互操作。 - C. A. McCann
2
指针:GHC用户指南的第4.13章和第8.2章,http://www.haskell.org/haskellwiki/Calling_Haskell_from_C - Cat Plus Plus
GHC似乎有一个关于动态链接库创建的章节:http://www.haskell.org/ghc/docs/latest/html/users_guide/win32-dlls.html 但是最近版本的GHC中似乎这个章节已经发生了改变。(!) - MathematicalOrchid
请注意如何编译和链接您的C/C++代码(http://stackoverflow.com/questions/5829170/ghc-foreign-hs-init-hs-add-root-crashes)。我不知道这种影响是否会在.NET/托管代码上暴露。 - Jonke
@Jonke 我们为了完全相同的原因,将C++与C#链接起来,玩得不亦乐乎。我不知道这些东西是什么,但显然正确性至关重要... - MathematicalOrchid
2个回答

55

就这两种语言而言,你基本上可以假装自己在尝试与C代码进行交互。

这是一个复杂的主题,所以我不会尝试解释所有内容,而是专注于提供一个简单的示例,您可以使用下面链接的资源进行构建。

  1. 首先,您需要编写包装器(wrapper)来使用Foreign.C.*模块中的类型,而不是通常的Haskell类型。例如用CInt代替Int,用CString代替String等。这是最复杂的步骤,特别是当您必须处理用户定义的类型时。

    您还必须使用ForeignFunctionInterface扩展编写foreign export声明来导出这些函数。

{-# LANGUAGE ForeignFunctionInterface #-}
module Foo where

import Foreign.C.String
import Foreign.C.Types

foreign export ccall
  foo :: CString -> IO CInt

foo :: CString -> IO CInt
foo c_str = do
  str    <- peekCString c_str
  result <- hs_foo str 
 return $ fromIntegral result

hs_foo :: String -> IO Int
hs_foo str = do
  putStrLn $ "Hello, " ++ str
  return (length str + 42)
然后,在编译时,您告诉 GHC 制作一个共享库:
$ ghc -O2 --make -no-hs-main -optl '-shared' -o Foo.so Foo.hs
  • 从C#方面来看,除了导入要调用的函数之外,您还必须导入 hs_init()并在调用任何Haskell函数之前调用它以初始化运行时系统。在完成后,您还应该调用 hs_exit()

  • using System;
    using System.Runtime.InteropServices;
    
    namespace Foo {
        class MainClass {
            [DllImport("Foo.so", CallingConvention = CallingConvention.Cdecl)]
            private static extern void hs_init(IntPtr argc, IntPtr argv);
    
            [DllImport("Foo.so", CallingConvention = CallingConvention.Cdecl)]
            private static extern void hs_exit();
    
            [DllImport("Foo.so", CallingConvention = CallingConvention.Cdecl)]
            private static extern int foo(string str);
    
            public static void Main(string[] args) {
                Console.WriteLine("Initializing runtime...");
                hs_init(IntPtr.Zero, IntPtr.Zero);
    
                try {
                    Console.WriteLine("Calling to Haskell...");
                    int result = foo("C#");
                    Console.WriteLine("Got result: {0}", result);
                } finally {
                    Console.WriteLine("Exiting runtime...");
                    hs_exit();
                }
            }
        }
    }
    
  • 现在我们进行编译和运行:

    $ mcs -unsafe Foo.cs
    $ LD_LIBRARY_PATH=. mono Foo.exe
    Initializing runtime...
    Calling to Haskell...
    Hello, C#
    Got result: 44
    Exiting runtime...
    

    它有效!

  • 资源:


    这看起来几乎就是我想要的。但是... #1 你不也需要调用 hs_exit() 吗?#2 当我运行这个程序时,hs_init(null, null) 的第二个参数会抛出 MarshalDirectiveException 异常。为什么? - MathematicalOrchid
    @MathematicalOrchid: #1:是的。#2:嗯。上面的代码在我的机器上可以运行,但我正在尝试弄清楚如何正确地进行编组。由于某种原因,hs_init需要一个char ** argv [],即使据我所知,char *argv []就足够了。从我能找到的例子来看,在后一种情况下,StringBuilder数组应该可以工作,但我不知道如何处理额外的间接级别(至少不是手动完成所有操作)。无论如何,只要你打算传递null,任何指针类型都应该可以工作。 - hammar
    1
    修复了。我将两个参数都改为 IntPtr,但是接着我收到了不平衡堆栈警告。显然我需要添加 CallingConvention=CallingConvention.Cdecl 以某种原因...现在它看起来完美地工作了。 - MathematicalOrchid
    在编译Haskell部分时,我遇到了错误:“undefined reference to 'WinMain'”。 这可以通过更改“-optl”->“-optl-mwindows”来解决。 我从以下网址获得了此解决方案:https://gitlab.haskell.org/ghc/ghc/issues/2459 - L. Blommers

    12

    供参考,我能够在Windows下使用以下过程。

    {-# LANGUAGE ForeignFunctionInterface #-}
    
    module Fibonacci () where
    
    import Data.Word
    import Foreign.C.Types
    
    fibs :: [Word32]
    fibs = 1 : 1 : zipWith (+) fibs (tail fibs)
    
    fibonacci :: Word8 -> Word32
    fibonacci n =
      if n > 47
        then 0
        else fibs !! (fromIntegral n)
    
    c_fibonacci :: CUChar -> CUInt
    c_fibonacci (CUChar n) = CUInt (fibonacci n)
    
    foreign export ccall c_fibonacci :: CUChar -> CUInt
    

    使用以下编译

    ghc --make -shared Fibonacci.hs
    

    这将生成半打文件,其中一个是HSdll.dll。然后我将其复制到Visual Studio C#项目中,并执行以下操作:

    using System;
    using System.Runtime.InteropServices;
    
    namespace ConsoleApplication1
    {
        public sealed class Fibonacci : IDisposable
        {
            #region DLL imports
    
            [DllImport("HSdll.dll", CallingConvention=CallingConvention.Cdecl)]
            private static extern unsafe void hs_init(IntPtr argc, IntPtr argv);
    
            [DllImport("HSdll.dll", CallingConvention = CallingConvention.Cdecl)]
            private static extern unsafe void hs_exit();
    
            [DllImport("HSdll.dll", CallingConvention = CallingConvention.Cdecl)]
            private static extern UInt32 c_fibonacci(byte i);
    
            #endregion
    
            #region Public interface
    
            public Fibonacci()
            {
                Console.WriteLine("Initialising DLL...");
                unsafe { hs_init(IntPtr.Zero, IntPtr.Zero); }
            }
    
            public void Dispose()
            {
                Console.WriteLine("Shutting down DLL...");
                unsafe { hs_exit(); }
            }
    
            public UInt32 fibonacci(byte i)
            {
                Console.WriteLine(string.Format("Calling c_fibonacci({0})...", i));
                var result = c_fibonacci(i);
                Console.WriteLine(string.Format("Result = {0}", result));
                return result;
            }
    
            #endregion
        }
    }
    
    Console.WriteLine()调用显然是可选的。
    我还没有尝试在Mono/Linux下运行这个程序,但它可能是类似的。
    总之,这和使C++ DLL工作的难度大致相同。(即,使类型签名匹配并使编组工作正确是困难的部分。)
    我还需要编辑项目设置并选择“允许不安全的代码”。

    1
    “unsafe” 部分只有在使用原始指针时才需要。使用 “IntPtr” 时,它应该可以在没有这些部分的情况下工作。 - hammar
    2
    是的,这都是真的。将其更改为“IntPtr”,删除所有“unsafe”关键字,并关闭编译器选项,它仍然可以编译和运行得很好。 - MathematicalOrchid

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