2013-05-17 41 views
40

我刚刚花了最后一个星期左右的时间,弄清楚如何从C#执行C++代码作为我日常工作的一部分。我们永远需要弄清楚,但最终的解决方案非常简单。从C#调用Haskell#

现在我很好奇......从C#调用Haskell有多难? (注意:这就是C#的的Haskell ,所以主要的可执行文件是C#。)

如果这真的很难,我不会打扰。但是,如果它相当容易,我可能不得不去玩它...

基本上,我们写了一些C++代码。在Windows上它被编译成一个DLL,在Linux上它被编译成一个共享对象(*.so)。然后在C#方面,你做一个DllImport并写一些手动的内存管理代码,如果你试图传递任何不平凡的东西。 (例如,数组,字符串等)

我知道GHC是应该支持这两个平台上构建共享库,但我不知道的技术细节。什么是导出东西的语法,调用者必须做任何特殊的事情来初始化DLL?

要具体:假设存在一个函数foobar :: FilePath -> IO Int32。有人可以把一个小小的草图显示出来:

  • 什么Haskell声明我需要写出来揭露这个外部世界。
  • 如何告诉GHC构建一个自包含的DLL/SO文件。
  • 除了绑定foobar本身的通常流程之外,呼叫者需要做的任何特殊操作。

我并不太担心C#端的实际语法;我想我已经或多或少地困惑了。

P.S.我简单地看过hs-dotnet,但这似乎是Windows特有的。 (即不使用Mono的工作,所以不会在Linux上运行。)

+5

对于FFI绑定,您将始终有一个计划B,即“用C编写一个薄包装器”。大多数具有任何类型的FFI的语言都可以与C互操作。 –

+2

指针:GHC用户指南的第4.13和8.2章,http://www.haskell.org/haskellwiki/Calling_Haskell_from_C –

+0

看来GHC有一章创建了DLL :http://www.haskell.org/ghc/docs/latest/html/users_guide/win32-dl​​ls.html此部分在GHC的最新版本中也有所变化。 (!) – MathematicalOrchid

回答

48

至于这两种语言而言,你基本上可以假装你想用C代码的接口。

这是一个复杂的话题,因此,而不是试图解释这一切,我将专注于做一个简单的例子,你可以建立使用下面链接的资源。

  1. 首先,你需要编写包装器使用类型从Foreign.C.*模块,而不是通常的Haskell类型的Haskell函数。 CInt而不是Int,CString而不是String等。这是最复杂的一个步骤,尤其是当您必须处理用户定义的类型时。

    你还必须写foreign export声明使用ForeignFunctionInterface扩展这些功能。

    {-# 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) 
    
  2. 然后,编译时,你告诉GHC使共享库:

    $ ghc -O2 --make -no-hs-main -optl '-shared' -o Foo.so Foo.hs 
    
  3. 从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(); 
          } 
         } 
        } 
    } 
    
  4. 现在我们编译并运行:

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

    它的工作原理!

资源:

+0

这看起来像我想要的几乎完全一样。然而......#1你不也需要调用'hs_exit()'吗?#2当我运行这个时,我得到了'hs_init(null,null)'中第二个参数的MarshalDirectiveException。胡务? – MathematicalOrchid

+0

@MathematicalOrchid:#1:是的。 #2:嗯。上述代码适用于我的机器,但我试图弄清楚如何正确编组。出于某种原因'hs_init'需要'char ** argv []',尽管据我所知'char * argv []'是足够的。从我已经能够找到的例子中,'StringBuilder'数组应该在后一种情况下工作,但我不知道如何处理额外的间接级别(至少在没有手动执行所有操作的情况下)。在任何情况下,只要你传递'null',任何指针类型都可以工作。 – hammar

+1

修正了它。我将这两个参数都改为了'IntPtr',但之后我得到了不平衡的堆栈警告。显然,我需要添加'CallingConvention = CallingConvention.Cdecl'出于某种原因...现在看起来很完美。 – MathematicalOrchid

10

仅供参考,我能得到下面的步骤在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

+0

@hammar好吧,稍后再测试...... – MathematicalOrchid

+2

没错,这都是真的。切换到'IntPtr',删除所有'unsafe'关键字,并关闭编译器选项,它仍然编译并运行得很好。 – MathematicalOrchid