{-|
Module      : Util.DynamicLinker
Description : Platform-specific dynamic linking support. Add new platforms to this file through conditional compilation.

License     : BSD3
Maintainer  : The Idris Community.
-}
{-# LANGUAGE CPP, ExistentialQuantification, ScopedTypeVariables #-}
module Util.DynamicLinker ( ForeignFun(..)
                          , DynamicLib(..)
                          , tryLoadLib
                          , tryLoadFn
                          ) where

#ifdef IDRIS_FFI
import System.Directory
#ifdef mingw32_HOST_OS
import qualified Control.Exception as Exception (IOException, catch)
import Foreign.Ptr (FunPtr, castPtrToFunPtr, nullFunPtr, nullPtr)
import System.FilePath.Windows ((</>))
import System.Win32.DLL
import System.Win32.Types
#else
import Control.Exception (IOException, throwIO, try)
import Foreign.Ptr (FunPtr, nullFunPtr, nullPtr)
#ifdef linux_HOST_OS
import Data.Array (bounds, inRange, (!))
import Data.Functor ((<$>))
import Data.Maybe (catMaybes)
#else
import Data.Array (bounds, (!))
#endif
import System.FilePath.Posix ((</>))
import System.Posix.DynamicLinker
import Text.Regex.TDFA
#endif

#ifdef mingw32_HOST_OS
type DL = HMODULE
#endif

hostDynamicLibExt :: String
#if defined(linux_HOST_OS) || defined(freebsd_HOST_OS) \
    || defined(dragonfly_HOST_OS) || defined(openbsd_HOST_OS) \
    || defined(netbsd_HOST_OS)
hostDynamicLibExt = "so"
#elif defined(darwin_HOST_OS)
hostDynamicLibExt = "dylib"
#elif defined(mingw32_HOST_OS)
hostDynamicLibExt = "dll"
#else
hostDynamicLibExt = error $ unwords
  [ "Undefined file extension for dynamic libraries"
  , "in Idris' Util.DynamicLinker."
  ]
#endif

data ForeignFun = forall a. Fun { fun_name :: String
                                , fun_handle :: FunPtr a
                                }

data DynamicLib = Lib { lib_name :: String
                      , lib_handle :: DL
                      }

instance Eq DynamicLib where
    (Lib a _) == (Lib b _) = a == b

firstExisting :: [FilePath] -> IO (Maybe FilePath)
firstExisting [] = return Nothing
firstExisting (f:fs) = do exists <- doesFileExist f
                          if exists
                            then return (Just f)
                            else firstExisting fs

libFileName :: [FilePath] -> String -> IO String
libFileName dirs lib = do let names = [lib, lib ++ "." ++ hostDynamicLibExt]
                          cwd <- getCurrentDirectory
                          found <- firstExisting $
                                   map ("."</>) names ++ [d </> f | d <- cwd:dirs, f <- names]
                          return $ maybe (lib ++ "." ++ hostDynamicLibExt) id found

#ifndef mingw32_HOST_OS
-- Load a dynamic library on POSIX systems.
-- In the simple case, we just find the appropriate filename and call dlopen().
-- In the complicated case our "foo.so" isn't actually a library. Some of the
-- .so files on modern Linux systems are linker scripts instead. dlopen()
-- doesn't know anything about those. We need to look inside the script for the
-- actual library path and load that. This is a horrible hack, the correct
-- method would be to actually parse the scripts and execute them. The approach
-- below is what GHC does.
tryLoadLib :: [FilePath] -> String -> IO (Maybe DynamicLib)
tryLoadLib dirs lib = do
  filename <- libFileName dirs lib
  res :: Either IOException DL <- try $
    dlopen filename [RTLD_NOW, RTLD_GLOBAL]
  mbDL <- case res of
    Right handle -> return $ Just handle
#ifdef linux_HOST_OS
    Left ex ->
      -- dlopen failed, run a regex to see if the error message looks like it
      -- could be a linker script.
      case matchAllText invalidLibRegex (show ex) of
        (x:_) -> do
          if inRange (bounds x) 1
            then do
              -- filename above may be a relative path. Get the full path out of
              -- the error message.
              let realPath = fst $ x ! 1
              fileLines <- lines <$> readFile realPath
              -- Go down the linker script line by line looking for .so
              -- filenames and try each one.
              let matches = catMaybes $ map
                    (getLastMatch . matchAllText linkerScriptRegex)
                    fileLines
              mapMFirst (\f -> dlopen f [RTLD_NOW, RTLD_GLOBAL]) matches
            else return Nothing
        [] -> return Nothing
#else
    Left ex -> throwIO ex
#endif
  case mbDL of
    Just handle -> if undl handle == nullPtr
                   then return Nothing
                   else return . Just $ Lib lib handle
    Nothing     -> return Nothing

getLastMatch :: [MatchText String] -> Maybe String
getLastMatch [] = Nothing
getLastMatch (x:_) = case bounds x of
  (low, high) -> if low > high
                 then Nothing
                 else Just $ fst $ x ! high

mapMFirst :: (a -> IO b) -> [a] -> IO (Maybe b)
mapMFirst f []     = return Nothing
mapMFirst f (a:as) = do res <- try (f a)
                        case res of
                          Left (ex :: IOException) -> mapMFirst f as
                          Right res -> return $ Just res

-- Both regexes copyright 2009-2011 Howard B. Golden, CJ van den Berg and Ian
-- Lynagh. From the Glasgow Haskell Compiler. BSD licensed.
invalidLibRegex :: Regex
invalidLibRegex = makeRegex "(([^ \t()])+\\.so([^ \t:()])*):([ \t])*(invalid ELF header|file too short)"

linkerScriptRegex :: Regex
linkerScriptRegex = makeRegex "(GROUP|INPUT) *\\( *([^ )]+)"

tryLoadFn :: String -> DynamicLib -> IO (Maybe ForeignFun)
tryLoadFn fn (Lib _ h) = do cFn <- dlsym h fn
                            if cFn == nullFunPtr
                               then return Nothing
                               else return . Just $ Fun fn cFn
#else
tryLoadLib :: [FilePath] -> String -> IO (Maybe DynamicLib)
tryLoadLib dirs lib = do filename <- libFileName dirs lib
                         handle <- Exception.catch (loadLibrary filename) nullPtrOnException
                         if handle == nullPtr
                             then return Nothing
                             else return . Just $ Lib lib handle
  where nullPtrOnException :: Exception.IOException -> IO DL
        nullPtrOnException e = return nullPtr
        -- `show e` will however give broken error message

tryLoadFn :: String -> DynamicLib -> IO (Maybe ForeignFun)
tryLoadFn fn (Lib _ h) = do cFn <- getProcAddress h fn
                            if cFn == nullPtr
                                then return Nothing
                                else return . Just $ Fun fn (castPtrToFunPtr cFn)
#endif
#else
-- no libffi, just add stubbs.

data DynamicLib = Lib { lib_name :: String
                      , lib_handle :: ()
                      }
    deriving Eq

data ForeignFun = forall a. Fun { fun_name :: String
                                , fun_handle :: ()
                                }

tryLoadLib :: [FilePath] -> String -> IO (Maybe DynamicLib)
tryLoadLib fps lib = do putStrLn $ "WARNING: Cannot load '" ++ lib ++ "' at compile time because Idris was compiled without libffi support."
                        return Nothing

tryLoadFn :: String -> DynamicLib -> IO (Maybe ForeignFun)
tryLoadFn fn lib = do putStrLn $ "WARNING: Cannot load '" ++ fn ++ "' at compile time because Idris was compiled without libffi support."
                      return Nothing
#endif