46

I just spent the last week or so figuring out how to execute C++ code from C# as part of my day job. It took us forever to figure it out, but the final solution is fairly simple.

Now I'm curious... How hard would it be to call Haskell from C#? (Note carefully: That's call Haskell from C#, not the other way around. So the main executable is C#.)

If it's really hard, I won't bother. But if it's reasonably easy, I might have to have a play with it...

Basically, we wrote some C++ code. On Windows it gets compiled into a DLL, on Linux it gets compiled into a shared object (*.so). Then on the C# side you do a DllImport and write some manual memory management code if you're trying to pass anything nontrivial across. (E.g., arrays, strings, etc.)

I know GHC is supposed to support building shared libraries on both platforms, but I'm not sure of the technical details. What's the syntax for exporting stuff, and does the caller have to do anything special to initialise the DLL first?

To be concrete: Suppose there exists a function foobar :: FilePath -> IO Int32. Can somebody throw together a small sketch showing:

  • What Haskell declarations I need to write to expose this to the outside world.
  • How do I tell GHC to build a single self-contained DLL / SO file.
  • Anything special the caller needs to do, beyond the usual process of binding foobar itself.

I'm not too worried about the actual syntax for the C# side; I think I've more or less puzzled that out.

P.S. I did briefly look at hs-dotnet, but this appears to be Windows-specific. (I.e., won't work with Mono, so won't work on Linux.)

siddstuff
  • 1,215
  • 4
  • 16
  • 37
MathematicalOrchid
  • 61,854
  • 19
  • 123
  • 220
  • 5
    You'll always have a Plan B for FFI bindings that is "write a thin wrapper in C". Most languages that have any sort of FFI at all can interop with C. – C. A. McCann May 17 '13 at 18:45
  • 2
    Pointers: chapters 4.13 and 8.2 from GHC user guide, http://www.haskell.org/haskellwiki/Calling_Haskell_from_C – Cat Plus Plus May 17 '13 at 20:07
  • It appears GHC has a chapter on DLL creation: http://www.haskell.org/ghc/docs/latest/html/users_guide/win32-dlls.html It also appears this section has changed in recent versions of GHC. (!) – MathematicalOrchid May 17 '13 at 20:42
  • Be aware of how you compile and link your c/c++ code though (http://stackoverflow.com/questions/5829170/ghc-foreign-hs-init-hs-add-root-crashes). I don't know if this effect is exposed on .net/managed code though. – Jonke May 19 '13 at 21:30
  • @Jonke We had seven shades of fun linking C++ to C# for the exact same reason. I have no idea what this stuff is, but apparently it's critical to get it correct... – MathematicalOrchid May 20 '13 at 07:53

2 Answers2

55

As far as both languages are concerned, you can basically pretend you're trying to interface with C code.

This is a complex topic, so rather than try to explain all of it, I will focus on making a simple example that you can build on using the resources linked below.

  1. First, you need to write wrappers for your Haskell functions that use types from the Foreign.C.* modules instead of the usual haskell types. CInt instead of Int, CString instead of String, etc. This is the most complicated step, especially when you have to deal with user-defined types.

    You also have to write foreign export declarations for those functions using the ForeignFunctionInterface extension.

    {-# 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. Then, when compiling, you tell GHC to make a shared library:

    $ ghc -O2 --make -no-hs-main -optl '-shared' -o Foo.so Foo.hs
    
  3. From the C# side, in addition to importing the function you want to call, you also have to import hs_init() and call it to initialize the runtime system before you can call any Haskell functions. You should also call hs_exit() when you're done.

    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. Now we compile and run:

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

    It works!

Resources:

hammar
  • 138,522
  • 17
  • 304
  • 385
  • This looks like pretty much exactly what I wanted. However... #1 Don't you also need to call `hs_exit()` too? #2 when I run this, I get a MarshalDirectiveException for the second argument in `hs_init(null, null)`. Wuh? – MathematicalOrchid May 18 '13 at 11:11
  • @MathematicalOrchid: #1: Yes. #2: Hm. The above code works on my machine, but I was trying to figure out how to marshal it properly. For some reason `hs_init` takes a `char** argv[]`, even though as far as I understand a `char *argv[]` would have sufficed. From the examples I've been able to find, an array of `StringBuilder` should work in the latter case, but I don't know how to deal with the extra level of indirection (at least not without doing everything manually). In any case, as long as you're just going to pass `null`, any pointer type should work. – hammar May 18 '13 at 11:38
  • 1
    Fixed it. I changed both parameters to `IntPtr`, but then I got unbalanced stack warnings. Apparently I need to add `CallingConvention=CallingConvention.Cdecl` for some reason... Now it appears to work perfectly. – MathematicalOrchid May 18 '13 at 11:44
  • Whilst compiling the Haskell part I ran into the error: "undefined reference to 'WinMain'". This can be solved by changing "-optl" -> "-optl-mwindows". I got this solution from: https://gitlab.haskell.org/ghc/ghc/issues/2459 – L. Blommers Mar 16 '20 at 12:25
12

For reference, I was able to get the following procedure to work under 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

Compile this with

ghc --make -shared Fibonacci.hs

This produces half a dozen files, one of which is HSdll.dll. I then copied that into a Visual Studio C# project, and did the following:

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
    }
}

The Console.WriteLine() calls are obviously optional.

I haven't tried running this under Mono / Linux yet, but it's presumably similar.

In summary, it's approximately the same difficulty as getting a C++ DLL to work. (I.e., getting the type signatures to match up and making marshaling work correctly is the hard bit.)

I also had to edit the project settings and select "allow unsafe code".

MathematicalOrchid
  • 61,854
  • 19
  • 123
  • 220
  • 1
    The `unsafe` bits were only needed when I was using raw pointers. With `IntPtr`, it should work without them. – hammar May 18 '13 at 11:54
  • 2
    Yep, it's all true. Change to `IntPtr`, remove all the `unsafe` keywords, and turn off the compiler option, and it still compiles and runs just fine. – MathematicalOrchid May 19 '13 at 08:55