GHC 6.12.1 dynamic executables fun

Recently I started to play with the new GHC 6.12.1. One thing I wanted to do is to link executables with -dynamic flag. It works pretty well when invoked like this:

$ ghc --make -dynamic program.hs

The resulting program will have plenty of dependencies (50+ is pretty common). It will also be quite small: for example 26K instead of 681K.

However, when building cabalized program/library one would like to have everything done automatically. However cabal install doesn’t make -dynamic executables. There is a ticket filled about this on Cabal Trac. One possible workaround is to call cabal install with –ghc-options=-dynamic flag. The other is to include this flag in ghc-options field in .cabal file. This is better since we can add it selectively. Consider this example binlib:

name: dynbin
version: 0.0
license: BSD3
license-file: LICENSE
copyright: (c) Christopher Skrzetnicki
author: Christopher Skrzetnicki
maintainer: Christopher Skrzetnicki <gtener@gmail.com>
stability: experimental
synopsis: 
description:
category: Other
cabal-version: >= 1.2
build-type: Simple
 
library
  hs-source-dirs: src
  exposed-modules: Dynbin.Main
  build-depends: base == 4.*
 
executable dynbin-dyn
  executable: dynbin-dyn
  hs-source-dirs: src
  ghc-options: -dynamic
  main-is: dynamic_main.hs
 
executable dynbin-stat
  executable: dynbin-stat
  hs-source-dirs: src
  main-is: static_main.hs

Files used are pretty straightforward.

dynamic_main.hs:

import qualified Dynbin.Main
 
main :: IO ()
main = putStrLn "dynamic_main.hs" >> Dynbin.Main.main

static_main.hs:

import qualified Dynbin.Main
 
main :: IO ()
main = putStrLn "static_main.hs" >> Dynbin.Main.main

Dynbin.Main:

module Dynbin.Main ( main ) where
 
main :: IO ()
main = putStrLn "Hello from Dynbin.Main.main !!!"

After installing it works like charm:

$ dynbin-stat
static_main.hs
Hello from Dynbin.Main.main !!!
 
$ dynbin-dyn
dynamic_main.hs
Hello from Dynbin.Main.main !!!

Calling ldd confirms that first executable is static, and the second is dynamic.

$ ldd `which dynbin-stat`
    linux-gate.so.1 =>  (0xb78dd000)
    librt.so.1 => /lib/librt.so.1 (0xb78ab000)
    libutil.so.1 => /lib/libutil.so.1 (0xb78a7000)
    libdl.so.2 => /lib/libdl.so.2 (0xb78a3000)
    libgmp.so.3 => /usr/lib/libgmp.so.3 (0xb7856000)
    libm.so.6 => /lib/libm.so.6 (0xb782f000)
    libc.so.6 => /lib/libc.so.6 (0xb76e8000)
    libpthread.so.0 => /lib/libpthread.so.0 (0xb76cf000)
    /lib/ld-linux.so.2 (0xb78de000)
 
$ ldd `which dynbin-dyn` 
    linux-gate.so.1 =>  (0xb77f6000)
    libHSunix-2.4.0.0-ghc6.12.1.so => /usr/lib/ghc-6.12.1/unix-2.4.0.0/libHSunix-2.4.0.0-ghc6.12.1.so (0xb7765000)
    librt.so.1 => /lib/librt.so.1 (0xb7734000)
    libutil.so.1 => /lib/libutil.so.1 (0xb7730000)
    libdl.so.2 => /lib/libdl.so.2 (0xb772b000)
    libHSbase-4.2.0.0-ghc6.12.1.so => /usr/lib/ghc-6.12.1/base-4.2.0.0/libHSbase-4.2.0.0-ghc6.12.1.so (0xb72ae000)
    libHSinteger-gmp-0.2.0.0-ghc6.12.1.so => /usr/lib/ghc-6.12.1/integer-gmp-0.2.0.0/libHSinteger-gmp-0.2.0.0-ghc6.12.1.so (0xb729e000)
    libgmp.so.3 => /usr/lib/libgmp.so.3 (0xb7251000)
    libHSghc-prim-0.2.0.0-ghc6.12.1.so => /usr/lib/ghc-6.12.1/ghc-prim-0.2.0.0/libHSghc-prim-0.2.0.0-ghc6.12.1.so (0xb71de000)
    libHSrts-ghc6.12.1.so => /usr/lib/ghc-6.12.1/libHSrts-ghc6.12.1.so (0xb7198000)
    libm.so.6 => /lib/libm.so.6 (0xb7172000)
    libHSffi-ghc6.12.1.so => /usr/lib/ghc-6.12.1/libHSffi-ghc6.12.1.so (0xb7169000)
    libc.so.6 => /lib/libc.so.6 (0xb7022000)
    libpthread.so.0 => /lib/libpthread.so.0 (0xb7009000)
    /lib/ld-linux.so.2 (0xb77f7000)

So this is OK for now. But what happens if we choose to have some TH code? Let’s use some. The following code uses packages hslogger and hslogger-template to add some logging to our code.

{-# LANGUAGE TemplateHaskell, ForeignFunctionInterface, CPP #-}
 
module Dynbin.Main ( main ) where
 
import System.Log.Logger.TH (deriveLoggers)
import qualified System.Log.Logger as HSL
import System.Log.Logger ( updateGlobalLogger, rootLoggerName, setLevel )
 
$(deriveLoggers "HSL" [HSL.WARNING, HSL.NOTICE, HSL.INFO])
 
-- dynamic linking
#ifdef MAIN_LIB
main_lib = MAIN_LIB
foreign export ccall MAIN_LIB main :: IO ()
#endif
 
main :: IO ()
main = do updateGlobalLogger rootLoggerName (setLevel HSL.INFO)
          infoM "Oh hai!"

dynbin.cabal:

name: dynbin
version: 0.0
license: BSD3
license-file: LICENSE
copyright: (c) Christopher Skrzetnicki
author: Christopher Skrzetnicki
maintainer: Christopher Skrzetnicki <gtener@gmail.com>
stability: experimental
synopsis: 
description:
category: Other
cabal-version: >= 1.2
build-type: Simple
 
library
  hs-source-dirs: src
  exposed-modules: Dynbin.Main
  cpp-options: -DMAIN_LIB="main_lib"
  build-depends: base == 4.*, hslogger, hslogger-template
 
executable dynbin-dyn
  executable: dynbin-dyn
  hs-source-dirs: src
  ghc-options: -dynamic
  cpp-options: -DMAIN_LIB="main_lib"
  main-is: dynamic_dlopen.hs
  other-modules: Paths_dynbin
  build-depends: unix, filepath, directory
 
executable dynbin-stat
  executable: dynbin-stat
  hs-source-dirs: src
  main-is: static_main.hs

The static executable builds and works well:

$ dynbin-stat  
static_main.hs
Dynbin.Main: Oh hai!

However, dynbin-dyn doesn’t. Here is what happens:

Resolving dependencies...
Configuring dynbin-0.0...
Preprocessing library dynbin-0.0...
Preprocessing executables for dynbin-0.0...
Building dynbin-0.0...
Registering dynbin-0.0...
[1 of 2] Compiling Dynbin.Main      ( src/Dynbin/Main.hs, dist/build/dynbin-dyn/dynbin-dyn-tmp/Dynbin/Main.o )
Loading package ghc-prim ... linking ... done.
Loading package integer-gmp ... linking ... done.
Loading package base ... linking ... done.
Loading package array-0.3.0.0 ... linking ... done.
Loading package filepath-1.1.0.3 ... linking ... done.
Loading package old-locale-1.0.0.2 ... linking ... done.
Loading package old-time-1.0.0.3 ... linking ... done.
Loading package unix-2.4.0.0 ... linking ... done.
Loading package directory-1.0.1.0 ... linking ... done.
Loading package process-1.0.1.2 ... linking ... done.
Loading package time-1.1.4 ... linking ... done.
Loading package random-1.0.0.2 ... linking ... done.
Loading package haskell98 ... linking ... done.
Loading package syb-0.1.0.2 ... linking ... done.
Loading package base-3.0.3.2 ... linking ... done.
Loading package containers-0.3.0.0 ... linking ... done.
Loading package mtl-1.1.0.2 ... linking ... done.
Loading package parsec-2.1.0.1 ... linking ... done.
Loading package network-2.2.1.7 ... linking ... done.
Loading package hslogger-1.0.7 ... linking ... done.
Loading package pretty-1.0.1.1 ... linking ... done.
Loading package template-haskell ... linking ... done.
Loading package hslogger-template-1.0.0 ... linking ... done.
Loading package ffi-1.0 ... linking ... done.
src/Dynbin/Main.hs:1:0:
    Dynamic linking required, but this is a non-standard build (eg. prof).
    You need to build the program twice: once the normal way, and then
    in the desired way using -osuf to set the object file suffix.
cabal: Error: some packages failed to install:
dynbin-0.0 failed during the building phase. The exception was:
ExitFailure 1

What does it mean? Well, we can’t simply mix dynamic builds with TH. I’m not sure why is it so, but the description how to solve it when using ghc –make is here. But here we are in a cabal install world. So how we are going to solve it?

There are two solutions. The first, mentioned in discussion for ticket #600, is this: move all executables to a different package and make it depend on the first one. This solution works, but is boring 😉 Here is a .cabal file for this:

name: dynbin-prog
version: 0.0
license: BSD3
license-file: LICENSE
copyright: (c) Christopher Skrzetnicki
author: Christopher Skrzetnicki
maintainer: Christopher Skrzetnicki <gtener@gmail.com>
stability: experimental
synopsis: 
description:
category: Other
cabal-version: >= 1.2
build-type: Simple
 
executable dynbin-dyn
  executable: dynbin-dyn
  hs-source-dirs: src
  ghc-options: -dynamic
  main-is: dynamic_main.hs
  build-depends: dynbin, base == 4.*
 
executable dynbin-stat
  executable: dynbin-stat
  hs-source-dirs: src
  main-is: static_main.hs
  build-depends: dynbin, base == 4.*

There is another solution which is much more fun.

HERE BE DRAGONS

The solution is this: build dynamic library and then use dlopen to open it, then dlsym to select and run specific symbol from the library itself. Pretty simple, huh? So let’s get to work.

First lest define a common symbol for calling. We might choose to take something from output from this command:

$ nm -D dist/build/libHSdynbin-0.0-ghc6.12.1.so
         w _Jv_RegisterClasses
00002dd8 A __bss_start
         w __cxa_finalize
         w __gmon_start__
         U __stginit_base_Prelude_dyn
000019f8 T __stginit_dynbinzm0zi0_DynbinziMain
000019a4 T __stginit_dynbinzm0zi0_DynbinziMain_dyn
         U __stginit_hsloggerzm1zi0zi7_SystemziLogziLogger_dyn
         U __stginit_hsloggerzmtemplatezm1zi0zi0_SystemziLogziLoggerziTH_dyn
00002dd8 A _edata
00002de0 A _end
00001ad8 T _fini
000014f0 T _init
         U base_GHCziBase_unpackCStringzh_info
         U base_GHCziTopHandler_runIO_closure
00002dc0 D dynbinzm0zi0_DynbinziMain_main_closure
00001984 T dynbinzm0zi0_DynbinziMain_main_info
00002dbc D dynbinzm0zi0_DynbinziMain_main_srt
00002db4 D dynbinzm0zi0_DynbinziMain_zdfmainzuaOs1_closure
00001934 T dynbinzm0zi0_DynbinziMain_zdfmainzuaOs1_info
00002da0 D dynbinzm0zi0_DynbinziMain_zdfmainzuaOs1_srt
00002d80 D dynbinzm0zi0_DynbinziMain_zdfmainzuaOs2_closure
00001674 T dynbinzm0zi0_DynbinziMain_zdfmainzuaOs2_info
00002d90 D dynbinzm0zi0_DynbinziMain_zdfmainzuaOs3_closure
000016f8 T dynbinzm0zi0_DynbinziMain_zdfmainzuaOs3_info
00002dcc D dynbinzm0zi0_DynbinziMain_zdfmainzuaOs_closure
0000199c T dynbinzm0zi0_DynbinziMain_zdfmainzuaOs_info
00002dc8 D dynbinzm0zi0_DynbinziMain_zdfmainzuaOs_srt
         U getStablePtr
         U ghczmprim_GHCziTypes_ZMZN_closure
         U hsloggerzm1zi0zi7_SystemziLog_INFO_closure
         U hsloggerzm1zi0zi7_SystemziLogziLogger_Logger_con_info
         U hsloggerzm1zi0zi7_SystemziLogziLogger_alertM2_closure
         U hsloggerzm1zi0zi7_SystemziLogziLogger_alertM2_info
         U hsloggerzm1zi0zi7_SystemziLogziLogger_saveGlobalLogger1_closure
         U hsloggerzm1zi0zi7_SystemziLogziLogger_saveGlobalLogger1_info
         U hsloggerzm1zi0zi7_SystemziLogziLogger_zdwa_closure
         U hsloggerzm1zi0zi7_SystemziLogziLogger_zdwa_info
         U newCAF
         U rts_apply
         U rts_checkSchedStatus
         U rts_evalIO
         U rts_lock
         U rts_unlock
         U stg_CAF_BLACKHOLE_info
         U stg_IND_STATIC_info
         U stg_gc_ut
         U stg_upd_frame_info

But this would make the solution even more fun than it is already. Instead let’s take different approach.

We shall export Dynbin.Main.main with foreign export using symbol defined with CPP macro MAIN_LIB. Then we shall use the same macro in dynbin-dyn to call the exported symbol. Both simple and fun! Here comes the code:

dynamic_dlopen.hs:

{-# LANGUAGE ForeignFunctionInterface, CPP #-}
 
{-
 
Required CPP Macros:
- MAIN_LIB: entry point for dynamic library
 
-}
 
module Main(main) where
 
import System.Posix.DynamicLinker
import System.Directory
import System.FilePath
import Control.Applicative
import Foreign
import Paths_dynbin ( getLibDir )
 
main_lib = MAIN_LIB
 
main = do
  libdir <- getLibDir
  print libdir
 
  shared <- filter (\fn -> takeExtension fn == ".so") <$> getDirectoryContents libdir
  case shared of
    [] -> error "No shared libraries!"
    [x] -> putStrLn ("One shared lib: " ++ x) >> onShared (libdir </> x)
    _ -> error "Multiple shared libraries!"
 
foreign import ccall "dynamic" call_dyn_fun :: FunPtr (IO ()) -> IO ()
 
dlopen' a1 a2 = print (a1,a2) >> dlopen a1 a2
 
dlcall :: DL -> String -> IO ()
dlcall dynlib fun = do
  print (dynlib,fun)
  fptr <- dlsym dynlib fun
  case nullFunPtr == fptr of
    False -> call_dyn_fun fptr
    True -> putStrLn =<< dlerror
 
onShared :: FilePath -> IO ()
onShared libPath = do
  main <- dlopen' libPath [RTLD_LAZY, RTLD_LOCAL]
  dlcall main main_lib
  putStrLn "main_lib exited"
  return ()

static_main.hs , unchanged:

import qualified Dynbin.Main
 
main :: IO ()
main = putStrLn "static_main.hs" >> Dynbin.Main.main

Dynbin.Main:

{-# LANGUAGE TemplateHaskell #-}
 
module Dynbin.Main ( main ) where
 
import System.Log.Logger.TH (deriveLoggers)
import qualified System.Log.Logger as HSL
import System.Log.Logger ( updateGlobalLogger, rootLoggerName, setLevel )
 
$(deriveLoggers "HSL" [HSL.WARNING, HSL.NOTICE, HSL.INFO])
 
main :: IO ()
main = do updateGlobalLogger rootLoggerName (setLevel HSL.INFO)
          infoM "Oh hai!"

And this solution works as expected:

$ dynbin-dyn   
"/home/tener//lib/dynbin-0.0/ghc-6.12.1"
One shared lib: libHSdynbin-0.0-ghc6.12.1.so
("/home/tener//lib/dynbin-0.0/ghc-6.12.1/libHSdynbin-0.0-ghc6.12.1.so",[RTLD_LAZY,RTLD_LOCAL])
(DLHandle 0x09216080,"main_lib")
Dynbin.Main: Oh hai!
main_lib exited
$ dynbin-stat
static_main.hs
Dynbin.Main: Oh hai!

The whole setup is pretty fragile. If you build run-dyn without -dynamic flag you will get runtime errors. Note that this behaviour is likely to be linked with this ticket.

Without -dynamic:

$ dynbin-dyn                                   
"/home/tener//lib/dynbin-0.0/ghc-6.12.1"
One shared lib: libHSdynbin-0.0-ghc6.12.1.so
("/home/tener//lib/dynbin-0.0/ghc-6.12.1/libHSdynbin-0.0-ghc6.12.1.so",[RTLD_LAZY,RTLD_LOCAL])
dynbin-dyn: user error (dlopen: 
        /usr/lib/ghc-6.12.1/ghc-prim-0.2.0.0/libHSghc-prim-0.2.0.0-ghc6.12.1.so: 
        undefined symbol: stg_newByteArrayzh)

So, what do you think about the whole idea?


Configuration used:

$ uname -a
Linux laptener 2.6.32-ARCH #1 SMP PREEMPT Tue Jan 19 06:08:04
       UTC 2010 i686 Intel(R) Core(TM)2 Duo 
       CPU T7500 @ 2.20GHz GenuineIntel GNU/Linux
$ cabal -V
cabal-install version 0.8.0
using version 1.8.0.2 of the Cabal library 
$ ghc -V
The Glorious Glasgow Haskell Compilation System, version 6.12.1

I also have two non-default flags in my ~/.cabal/config file:

   library-profiling: True
   shared: True

~ by Tener on 26/01/2010.

One Response to “GHC 6.12.1 dynamic executables fun”

  1. […] Dynamic linking of Haskell libraries is supported (see this tutorial). […]

Leave a comment