-XForeignFunctionInterface
(FFI) extension to use C functions and data directly. Below is an example file (Main1.hsc
) that utilizes the FFI to call srand()
and rand()
on Linux: {-# LANGUAGE ForeignFunctionInterface #-} module Main where import Foreign.C.Types #include <stdlib.h> foreign import ccall "srand" c_srand :: CUInt -> IO () foreign import ccall "rand" c_rand :: IO CInt main :: IO () main = readLn >>= c_srand >> c_rand >>= printNotice that the file extension is
.hsc
, not .hs
. This is because it uses a special #include
construct that must be processed by the hsc2hs
program (which comes with the Haskell Platform). If you use cabal, this is done automatically. Otherwise, hsc2hs
can be invoked like so:$ hsc2hs Main1.hsc $ runhaskell Main1.hs 42 71876166 $ runhaskell Main1.hs 27 1416980517Generally, this process is pretty straightforward for Linux header files. What about other operating systems? One OS in particular, Windows, also provides C headers for its API, but they are much more difficult for
hsc2hs
to use successfully. Here is a simple example (Main2.hsc
) that uses the Windows API function timeGetTime()
, which retrieves the current system time in milliseconds:{-# LANGUAGE ForeignFunctionInterface #-} module Main where #include <timeapi.h> import System.Win32.Types foreign import ccall "timeGetTime" c_timeGetTime :: IO DWORD main :: IO () main = putStrLn . show =<< c_timeGetTimeIf you attempt to simply run
hsc2hs Main2.hsc
, you will get an error resembling Main2.hsc:4:21: fatal error: timeapi.h: No such file or directory
. That's not surprising, since we're referencing a Windows API header that isn't shipped with the version of MinGW that the Haskell Platform for Windows uses. To get around this, we'll try including the necessary header location manually (using Cygwin):$ hsc2hs Main2.hsc -I"C:\Program Files (x86)\Windows Kits\8.1\Include\um" In file included from Main2.hsc:4:0: C:\Program Files (x86)\Windows Kits\8.1\Include\um/timeapi.h:17:20: fatal error: apiset.h: No such file or directory compilation terminated.Oh no!
timeapi.h
depends on a header file located in a different directory. That means we should only need to link that additional directory to resolve the issue, right?$ hsc2hs Main2.hsc -I"C:\Program Files (x86)\Windows Kits\8.1\Include\um" -I"C:\Program Files (x86)\Windows Kits\8.1\Include\shared" In file included from C:\Program Files (x86)\Windows Kits\8.1\Include\um/timeapi.h:21:0, from Main2.hsc:4: C:\Program Files (x86)\Windows Kits\8.1\Include\um/mmsyscom.h:94:21: error: expected '=', ',', ';', 'asm' or '__attribute__' before 'MMVERSION' C:\Program Files (x86)\Windows Kits\8.1\Include\um/mmsyscom.h:98:32: error: expected declaration specifiers or '...' before 'return' C:\Program Files (x86)\Windows Kits\8.1\Include\um/mmsyscom.h:98:9: error: function definition declared 'typedef' C:\Program Files (x86)\Windows Kits\8.1\Include\um/mmsyscom.h: In function '_Return_type_success_': C:\Program Files (x86)\Windows Kits\8.1\Include\um/mmsyscom.h:98:45: error: expected declaration specifiers before 'UINT' C:\Program Files (x86)\Windows Kits\8.1\Include\um/mmsyscom.h:102:14: error: expected '=', ',', ';', 'asm' or '__attribute__' before 'FAR' ...Ack! As far as I can tell, Windows-style headers are simply not compatible with MinGW. It is clear that using Windows-style header files directly in
hsc2hs
is a fool's errand.What can be done about this? One approach is to compile the code into a dynamically linked library (DLL) and pass it to the Haskell compiler's linker. This has the advantage of not needing the
#include
construct. As an example, we can create a simple DLL project in Microsoft Visual Studio (I used Visual Studio 2013 to perform the following steps):- Create a new Win32 Console Application project (I will give it the name
dll_example
). Make sure its application type is "DLL" in the correspding wizard menu. For good measure, check "Empty project" and uncheck "Security Development Lifecycle (SDL) checks". - In the Solution Explorer, right-click on "Header Files" and click Add > New Item. Select "Header file (.h)" and name it
dll_example.h
. - Use the following code for
dll_example.h
:#ifndef DLL_EXAMPLE_H #define DLL_EXAMPLE_H #include <windows.h> #ifdef DLL_EXAMPLE_DLL_EXPORTS #define DLL_EXAMPLE_DLL_API __declspec(dllexport) #else #define DLL_EXAMPLE_DLL_API __declspec(dllimport) #endif DLL_EXAMPLE_DLL_API DWORD time_get_time(); #endif
Note that we usewindows.h
since it automatically brings all of the definitions oftimeapi.h
into scope, as well as other needed definitions (such asDWORD
). - In the Solution Explorer, right-click on "Source Files" and click Add > New Item. Select "C++ File (.cpp)" and name it
dll_example.cpp
. - Use the following code for
dll_example.cpp
:
#include "dll_example.h" #define DLL_EXAMPLE_API #pragma comment(lib, "Winmm.lib") DWORD time_get_time() { return timeGetTime(); }
- In the Solution Explorer, right-click on "Resource Files" and click Add > New Item. In the left sidebar, click Visual C++ > Code, then click "Module-Definition File (.def)" and name it
dll_example.def
. - Give
dll_example.def
the following definition:LIBRARY dll_example EXPORTS time_get_time
- Click Build > Build Solution. When it is finished, copy the newly created
dll_example.dll
(it should be in a directory similar to<project directory>/Debug
) to the same directory whereMain2.hsc
is located.
#include <timeapi.h>
line from Main2.hsc
entirely, changing "timeGetTime"
to "time_get_time"
, renaming it to Main2.hs
, and compiling it like so:$ ghc --make Main2.hs -L. -lWinmm -ldll_example [1 of 1] Compiling Main ( Main2.hs, Main2.o ) Linking Main2.exe ... $ ./Main2.exe 95495217 $ ./Main2.exe 95496824Great! We seemed to have successfully used the DLL. But what happens when we attempt to execute
Main2.exe
independently of dll_example.dll
?$ cp Main2.exe ../Main2.exe $ cd .. $ ./Main2.exe .../Main2.exe: error while loading shared libraries: ?: cannot open shared object file: No such file or directoryUrgh. As it turns out, Windows needs to look up the dynamically linked libraries every time the executable is run. Notable locations that Windows searches include the executable's directory and the directories defined in the
PATH
environment variable.This is rather inconvenient for a Haskell programmer, as cabal installs all of its compiled Haskell executables in
%APPDATA%/cabal/bin
, far away from the custom DLL files it needs. What is the best solution to this problem? This GHC wiki page suggest several approaches, but since I am a fan of straightforward fixes, I prefer to simply copy the needed DLL files directly to %APPDATA%/cabal/bin
. Since that's tedious to do manually, we can configure cabal to automate this process.During my attempts to get cabal to use DLLs during compilation, I discovered that cabal's
extra-lib-dirs
field only accepts absolute paths. This is a problem for us, since we need to use a custom DLL file whose location is relative to the package's root directory. (There are claims that you can use ${pkgroot}
to retrieve this location, but I was not able to get it to work). This solution should resolve both of the aforementioned cabal issues:- Create a new cabal project (i.e.,
cabal init
). Put all of theMain2.hs
in the project, and putdll_example.dll
in<package root>/lib
. - In
Main2.cabal
, make sure thatextra-source-files
includeslib/dll_example.dll
, and thatextra-libraries
includesWinmm
anddll_example
. - Adapt
Setup.hs
to use this code:import Control.Monad import Debug.Trace import Distribution.PackageDescription import Distribution.Simple import Distribution.Simple.LocalBuildInfo import Distribution.Simple.Setup import System.Directory import System.FilePath dllFileName :: FilePath dllFileName = "dll_example" <.> "dll" dllSourceDir :: IO FilePath dllSourceDir = do curDir <- getCurrentDirectory return $ curDir </> "lib" dllSourcePath :: IO FilePath dllSourcePath = do sourceDir <- dllSourceDir return $ sourceDir </> dllFileName copyDll :: String -> FilePath -> FilePath -> IO () copyDll message sourcePath destPath = do putStrLn message putStr "Copying... " copyFile sourcePath destPath putStrLn "Done." patchDesc :: FilePath -> PackageDescription -> PackageDescription patchDesc sourceDir desc = let Just lib = library desc lbi = libBuildInfo lib newlbi = lbi { extraLibDirs = sourceDir : extraLibDirs lbi } in desc { library = Just $ lib { libBuildInfo = newlbi } } customBuild :: FilePath -> PackageDescription -> LocalBuildInfo -> UserHooks -> BuildFlags -> IO () customBuild sourceDir desc linfo hooks flags = do let installDir = bindir $ absoluteInstallDirs desc linfo NoCopyDest destPath = installDir </> dllFileName sourcePath <- dllSourcePath dllExists <- doesFileExist destPath when (not dllExists) $ copyDll (dllFileName ++ " is not in application data.") sourcePath destPath destTime <- getModificationTime destPath sourceTime <- getModificationTime sourcePath when (destTime < sourceTime) $ copyDll (dllFileName ++ " is out-of-date.") sourcePath destPath buildHook simpleUserHooks (patchDesc sourceDir desc) linfo hooks flags customInstall :: FilePath -> PackageDescription -> LocalBuildInfo -> UserHooks -> InstallFlags -> IO () customInstall sourceDir desc = instHook simpleUserHooks $ patchDesc sourceDir desc customPostConf :: FilePath -> Args -> ConfigFlags -> PackageDescription -> LocalBuildInfo -> IO () customPostConf sourceDir args conf desc linfo = postConf simpleUserHooks args conf (patchDesc sourceDir desc) linfo main :: IO () main = do sourceDir <- dllSourceDir defaultMainWithHooks $ simpleUserHooks { buildHook = customBuild sourceDir , instHook = customInstall sourceDir , postConf = customPostConf sourceDir }
dll_example.dll
is during compilation, where dll_example.dll
should be copied to, and when it needs to be copied (i.e., if it doesn't exist or is out-of-date). You should now be able to compile the project simply with cabal install
without worrying about ugly GCC flags.The downside to this approach is now there are extra files to manage if the user ever wants to uninstall Main2. One way to resolve this is to provide users with a makefile containing an
uninstall
command that automatically removes Main2.exe
and dll_example.dll
from %APPDATA%/cabal/bin
. If you want to see an example of this, check out the hermit-bluetooth repo, the project in which I encountered all of these problems (and motivated me to make this blog post so that maybe I can save other people some time).Working with DLLs in Haskell tends to be quite gruesome, and I'd recommend avoiding it whenever possible. In same cases, though, dynamic linking is the only feasible solution to one's problems (especially on Windows), so it's nice to know that the infrastructure for interfacing with DLLs exists (even if actually using that interface is a tad unpleasant).