Llamar a Haskell desde C#


Acabo de pasar la última semana averiguando cómo ejecutar código C++ desde C# como parte de mi trabajo diario. Nos llevó una eternidad averiguarlo, pero la solución final es bastante simple.

Ahora tengo curiosidad... ¿Qué tan difícil sería llamar a Haskell desde C#? (Nota cuidadosamente: Eso es call Haskell desde C#, no al revés. Así que el ejecutable principal es C#.)

Si es realmente difícil, no me molestaré. Pero si es razonablemente fácil, podría tener que jugar con se...

Básicamente, escribimos algo de código C++. En Windows se compila en una DLL, en Linux se compila en un objeto compartido (*.so). Luego, en el lado de C#, haces un DllImport y escribes un código de administración de memoria manual si estás tratando de pasar algo no trivial. (Por ejemplo, matrices, cadenas, etc.)

Sé que se supone que GHC soporta la construcción de bibliotecas compartidas en ambas plataformas, pero no estoy seguro de los detalles técnicos. ¿Cuál es la sintaxis para exportar cosas, y hace el la persona que llama tiene que hacer algo especial para inicializar el DLL primero?

Para ser concreto: Supongamos que existe una función foobar :: FilePath -> IO Int32. ¿Puede alguien armar un pequeño boceto mostrando:

  • Qué declaraciones de Haskell necesito escribir para exponer esto al mundo exterior.
  • Cómo le digo a GHC que construya un único archivo DLL / SO autónomo.
  • Cualquier cosa especial que la persona que llama necesita hacer, más allá del proceso habitual de enlace foobar en sí.

No estoy demasiado preocupado sobre la sintaxis real para el lado de C#; creo que he más o menos desconcertado eso.

P.D. Miré brevemente hs-dotnet, pero esto parece ser específico de Windows. (Es decir, no funcionará con Mono, por lo que no funcionará en Linux.)

Author: siddstuff, 2013-05-17

2 answers

En lo que respecta a ambos lenguajes, básicamente puedes fingir que estás tratando de interactuar con código C.

Este es un tema complejo, por lo que en lugar de tratar de explicar todo, me centraré en hacer un ejemplo simple que pueda construir utilizando los recursos vinculados a continuación.

  1. Primero, necesita escribir envoltorios para sus funciones de Haskell que usen tipos de los módulos Foreign.C.* en lugar de los tipos habituales de haskell. CInt en lugar de Int, CString en lugar de String, sucesivamente. Este es el paso más complicado, especialmente cuando tienes que lidiar con tipos definidos por el usuario.

    También tiene que escribir declaraciones foreign export para esas funciones usando la extensión 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. Luego, al compilar, le dices a GHC que haga una biblioteca compartida:

    $ ghc -O2 --make -no-hs-main -optl '-shared' -o Foo.so Foo.hs
    
  3. Desde el lado de C#, además de importar la función que desea llamar, también tiene que importar hs_init() y llamarla para inicializar el sistema de tiempo de ejecución antes de poder llamar cualquier función de Haskell. También debe llamar a hs_exit() cuando haya terminado.

    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. Ahora compilamos y ejecutamos:

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

    Funciona!

Recursos:

 49
Author: hammar,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-10-06 14:32:56

Como referencia, pude hacer que el siguiente procedimiento funcionara bajo 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

Compilar esto con

ghc --make -shared Fibonacci.hs

Esto produce media docena de archivos, uno de los cuales es HSdll.dll. Luego copié eso en un proyecto de Visual Studio C#, e hice lo siguiente:

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

Las llamadas Console.WriteLine() son obviamente opcionales.

Todavía no he intentado ejecutar esto bajo Mono / Linux, pero es presumiblemente similar.

En resumen, es aproximadamente la misma dificultad que conseguir una DLL de C++ para trabajar. (Es decir, conseguir que las firmas de tipo coincidan y hacer que el marshal funcione correctamente es la parte difícil.)

También tuve que editar la configuración del proyecto y seleccionar "permitir código inseguro".

 10
Author: MathematicalOrchid,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-12-28 19:56:49