Haskell libraries are suitable for the large majority of a functional programmer's use cases. Sometimes, however, there are needs that Haskell alone cannot fulfill. One common example is interfacing with low-level OS utilities. Since many of these tools are written in C, Haskell provides a useful
-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 >>= print
Notice 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
1416980517
Generally, 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_timeGetTime
If 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 use windows.h
since it automatically brings all of the definitions of timeapi.h
into scope, as well as other needed definitions (such as DWORD
).
- 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 where Main2.hsc
is located.
Now we can dynamically link the DLL file by removing the
#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
95496824
Great! 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 directory
Urgh. 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 the Main2.hs
in the project, and put dll_example.dll
in <package root>/lib
.
- In
Main2.cabal
, make sure that extra-source-files
includes lib/dll_example.dll
, and that extra-libraries
includes Winmm
and dll_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
}
That code should be sufficient to tell cabal where
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).