How can I use and call Haskell functions with higher-order type signatures from C# (DLLImport), like…
double :: (Int -> Int) -> Int -> Int -- higher order function
typeClassFunc :: ... -> Maybe Int -- type classes
data MyData = Foo | Bar -- user data type
dataFunc :: ... -> MyData
What are the corresponding type signature in C#?
[DllImport ("libHSDLLTest")]
private static extern ??? foo( ??? );
Additionally (because it may be easier): How can I use “unknown” Haskell types within C#, so I can at least pass them around, without C# knowing any specific type? The most important functionality I need right know is to pass around a type class (like Monad or Arrow).
I already know how to compile a Haskell library to DLL and use within C#, but only for first-order functions. I’m also aware of Stackoverflow – Call a Haskell function in .NET, Why isn’t GHC available for .NET and hs-dotnet, where I didn’t find ANY documentation and samples (for the C# to Haskell direction).
I’ll elaborate here on my comment on FUZxxl’s post.
The examples you posted are all possible using
FFI. Once you export your functions using FFI you can as you’ve already figured out compile the program into a DLL..NET was designed with the intention of being able to interface easily with C, C++, COM, etc. This means that once you’re able to compile your functions to a DLL, you can call it (relatively) easy from .NET. As I’ve mentioned before in my other post that you’ve linked to, keep in mind which calling convention you specify when exporting your functions. The standard in .NET is
stdcall, while (most) examples of HaskellFFIexport usingccall.So far the only limitation I’ve found on what can be exported by FFI is
polymorphic types, or types that are not fully applied. e.g. anything other than kind*(You can’t exportMaybebut you can exportMaybe Intfor instance).I’ve written a tool Hs2lib that would cover and export automatically any of the functions you have in your example. It also has the option of generating
unsafeC# code which makes it pretty much “plug and play”. The reason I’ve choosen unsafe code is because it’s easier to handle pointers with, which in turn makes it easier to do the marshalling for datastructures.To be complete I’ll detail how the tool handles your examples and how I plan on handling polymorphic types.
When exporting higher order functions, the function needs to be slightly changed. The higher-order arguments need to become elements of FunPtr. Basically They’re treated as explicit function pointers (or delegates in c#), which is how higher orderedness is typically done in imperative languages.
Assuming we convert
IntintoCIntthe type of double is transformed frominto
These types are generated for a wrapper function (
doubleAin this case) which is exported instead ofdoubleitself. The wrapper functions maps between the exported values and the expected input values for the original function. The IO is needed because constructing aFunPtris not a pure operation.One thing to remember is that the only way to construct or dereference a
FunPtris by statically creating imports which instruct GHC to create stubs for this.The “wrapper” function allows us to create a
FunPtrand the “dynamic”FunPtrallows one to deference one.In C# we declare the input as a
IntPtrand then use theMarshallerhelper function Marshal.GetDelegateForFunctionPointer to create a function pointer that we can call, or the inverse function to create aIntPtrfrom a function pointer.Also remember that the calling convention of the function being passed as an argument to the FunPtr must match the calling convention of the function to which the argument is being passed to. In other words, passing
&footobarrequiresfooandbarto have the same calling convention.Exporting a user datatype is actually quite straight forward. For every datatype that needs to be exported a Storable instance has to be created for this type. This instances specifies the marshalling information that GHC needs in order to be able to export/import this type. Among other things you would need to define the
sizeandalignmentof the type, along with how to read/write to a pointer the values of the type. I partially use Hsc2hs for this task (hence the C macros in the file).newtypesordatatypeswith just one constructor is easy. These become a flat struct since there’s only one possible alternative when constructing/destructing these types. Types with multiple constructors become a union (a struct withLayoutattribute set toExplicitin C#). However we also need to include an enum to identify which construct is being used.in general, the datatype
Singledefined ascreates the following
Storableinstanceand the C struct
The function
foo :: Int -> Singlewould be exported asfoo :: CInt -> Ptr SingleWhile a datatype with multiple constructor
generates the following C code:
The
Storableinstance is relatively straight forward and should follow easier from the C struct definition.My dependency tracer would for emit for for the type
Maybe Intthe dependency on both the typeIntandMaybe. This means, that when generating theStorableinstance forMaybe Intthe head looks likeThat is, aslong as there’s a Storable instance for the arguments of the application the type itself can also be exported.
Since
Maybe ais defined as having a polymorphic argumentJust a, when creating the structs, some type information is lost. The structs would contain avoid*argument, which you have to manually convert to the right type. The alternative was too cumbersome in my opinion, which was to create specialized structs aswell. E.g. struct MaybeInt. But the amount of specialized structures that could be generated from a normal module can quickly explode this way. (might add this as a flag later on).To ease this loss of information my tool will export any
Haddockdocumentation found for the function as comments in the generated includes. It will also place the original Haskell type signature in the comment as well. An IDE would then present these as part of its Intellisense (code compeletion).As with all of these examples I’ve ommited the code for the .NET side of things, If you’re interested in that you can just view the output of Hs2lib.
There are a few other types that need special treatment. In particular
ListsandTuples.Tuples are special build in types, In order to export them, we have to first map them to a “normal” datatype, and export those. In the tool this is done up untill 8-tuples.
The problem with polymorphic types
e.g. map :: (a -> b) -> [a] -> [b]is that thesizeofaandbare not know. That is, there’s no way to reserve space for the arguments and return value since we don’t know what they are. I plan to support this by allowing you to specify possible values foraandband create specialized wrapper function for these types. On the other size, in the imperative language I would useoverloadingto present the types you’ve chosen to the user.As for classes, Haskell’s open world assumption is usually a problem (e.g. an instance can be added any time). However at the time of compilation only a statically known list of instances is available. I intend to offer an option that would automatically export as much specialized instances as possible using these list. e.g. export
(+)exports a specialized function for all knownNuminstances at compile time (e.g.Int,Double, etc).The tool is also rather trusting. Since I can’t really inspect the code for purity, I always trust that the programmer is honest. E.g. you don’t pass a function that has side-effects to a function that expects a pure function. Be honest and mark the higher-ordered argument as being impure to avoid problems.
I hope this helps, and I hope this wasn’t too long.
Update : There’s somewhat of a big gotcha that I’ve recently discovered. We have to remember that the String type in .NET is immutable. So when the marshaller sends it to out Haskell code, the CWString we get there is a copy of the original. We have to free this. When GC is performed in C# it won’t affect the the CWString, which is a copy.
The problem however is that when we free it in the Haskell code we can’t use freeCWString. The pointer was not allocated with C (msvcrt.dll)’s alloc. There are three ways (that I know of) to solve this.