Combining Haskell and a well-known C library is easy. apt-get/yum install
, link with it, create bindings and we’re done. Problems start to emerge when we don’t want to, or can’t, install the library globally. Things get even worse, when the library uses C++. I faced this problem when writing hlibsass and I think that I’ve managed to solve it in a not-so-terrible way.
Background
Let’s say that we have a C++ library. There is a problem here - GHC does not support interfacing with it directly. There are tricks to achieve this, but I’m not going that crazy path. As it is C++, we may be able to write (or we already have) a C wrapper over the library which will allow us to use it from Haskell without all (or almost all) that hassle.
There is another problem here which, I think, is the main one - how to bundle the library with a package (as we excluded the possibility of relying on the library being installed in the system). Cabal does have a system to compile C sources during build phase, but it only works for small libraries with several files and simple build process and this makes it rather useless.
There is also another possibility - build the library separately and link against the resulting file. This is the way that I’ve used with hlibsass and it worked (at least for me ;) ). So what do we need to do? Let’s see!
Building
I assume that we’re working with this project - we have simple C++ library with C wrapper and a Haskell library with tests. We don’t have bindings yet, as we don’t have a way to link the Haskell code and external library together. First, we need to make
it, but Cabal does not provide straightforward way to do it, but we may adjust the Cabal as we like using hooks.
The idea is simple - call make
in a preConf
hook. We can’t do this on preBuild
because Cabal will scream missing C library on configure
. Sadly, we won’t be able to respect the -j
switch, as this information is not available anywhere outside the build phase, but we have no other choice.
So, let’s write our simple hook that just calls make
with rawSystemExit
:
makeExtLib :: Args -> ConfigFlags -> IO HookedBuildInfo
makeExtLib _ flags = do
let verbosity = fromFlag $ configVerbosity flags
rawSystemExit verbosity "env"
["make", "--directory=ext_lib"]
return emptyHookedBuildInfo
And hook it up:
main = defaultMainWithHooks simpleUserHooks
{
preConf = makeExtLib
}
Linking
We have the library built. Now we need to change build-type
to Custom, add ext
to extra-libraries
and specify extra-lib-dirs
(pointing at ext_lib/lib
).
We use C++, so we have to link against C++ standard library. We (probably) use G++, so we link against libstdc++
, but using different implementation should not be a problem. I assume that everyone has it installed globally, so simply adding stdc++
to extra-libraries
will suffice. It needs to be added at the end, as the order matters.
Fighting relative extra library path
Unfortunately, ghc-pkg
will not be happy now. ext_lib/lib
is relative path and although everything works, we have an awful warning. What to do? Use hooks!
This time, we need to modify LocalBuildInfo
and add absolute path to extra-lib-dirs
programmatically. This process is not so pleasant, as it’s mostly accessing properties, but the code isn’t complicated:
updateExtraLibDirs :: LocalBuildInfo -> IO LocalBuildInfo
updateExtraLibDirs localBuildInfo = do
let packageDescription = localPkgDescr localBuildInfo
lib = fromJust $ library packageDescription
libBuild = libBuildInfo lib
dir <- getCurrentDirectory
return localBuildInfo {
localPkgDescr = packageDescription {
library = Just $ lib {
libBuildInfo = libBuild {
extraLibDirs = (dir ++ "/ext_lib/lib") :
extraLibDirs libBuild
}
}
}
}
Also, specifying hook involves a little more work because we need to run the default configure before our hook:
main = defaultMainWithHooks simpleUserHooks
{
preConf = makeExtLib
, confHook = \a f -> confHook simpleUserHooks a f >>= updateExtraLibDirs
}
Distributing the library
There is one more thing to do - in order to allow other packages to link against your library, you have to copy the .a
file to the installation path. This isn’t great, as you may have different versions of the same file flying around, but we don’t have a choice. How to do it, you may ask? Hooks, of course. ;) This time - postCopy
.
copyExtLib :: Args -> CopyFlags -> PackageDescription -> LocalBuildInfo -> IO ()
copyExtLib _ flags pkg_descr lbi = do
let libPref = libdir . absoluteInstallDirs pkg_descr lbi
. fromFlag . copyDest
$ flags
let verbosity = fromFlag $ copyVerbosity flags
rawSystemExit verbosity "cp" ["ext_lib/lib/libext.a", libPref]
This code extracts desired installation path (~/.cabal/lib/...
, .cabal-sandbox
or somewhere globally) and just calls cp
to copy the file. Hooking is easy - just specify postCopy
.
You must also bundle the Makefile and sources with your package (if you want to put it on Hackage), but this requires only updating extra-source-files
in .cabal file.
Cleaning
One little addition may be made - we have to make clean
when we cabal clean
. The code is analogous to the first one presented here, so I leave any modifications necessary as an exercise.
Summary
So, now we have a simple cabal package that may be easily sdist
ributed and built on any machine without requiring manual installation of an additional library. This may not be the best option (especially flying .a
file) for every project, but for my hlibsass and hsass, it was the only sensible.
TL;DR
Use hooks and distribute library with your package - the code is on GitHub, so you can see it right away.