packages feed

keel-dyn-0.1.0.0: src-posix/Keel/Dyn/Platform.hs

-- | POSIX implementation over @dlopen@\/@dlsym@\/@dlclose@ (via the unix
-- package). Search paths follow the platform loader: rpath,
-- @LD_LIBRARY_PATH@ \/ @DYLD_LIBRARY_PATH@, then system defaults.
module Keel.Dyn.Platform
  ( Library
  , libraryPath
  , DynError (..)
  , loadLibrary
  , loadLibraryGlobal
  , closeLibrary
  , withLibrary
  , resolveSym
  , resolveOptional
  , addSearchDir
  ) where

import Control.Exception (Exception, IOException, finally, mask, try)
import Control.Monad (void)
import Foreign.Ptr (FunPtr, castFunPtr)
import qualified System.Posix.DynamicLinker as DL

-- | A loaded shared library. Constructor deliberately not exported.
data Library = Library
  { libDL :: !DL.DL
  , libraryPath :: !FilePath
    -- ^ The path\/name the library was requested as.
  }

-- | Failure modes of loading and symbol resolution.
data DynError
  = LibraryNotFound FilePath String
    -- ^ Library could not be loaded; the 'String' carries OS detail.
  | SymbolNotFound FilePath String
    -- ^ The named symbol is absent from the named library.
  deriving (Eq, Show)

instance Exception DynError

-- | Load a shared library by bare name or path. Search order is documented
-- in "Keel.Dyn". Symbols stay private to the handle (@RTLD_LOCAL@).
loadLibrary :: FilePath -> IO (Either DynError Library)
loadLibrary = loadWith [DL.RTLD_NOW, DL.RTLD_LOCAL]

-- | Like 'loadLibrary' but with @RTLD_GLOBAL@: the library's symbols
-- become visible to everything loaded afterwards. Needed when later
-- loads expect this library's symbols to already be in the process —
-- the canonical case is @libpython@, whose extension modules
-- deliberately leave Python's symbols undefined (manylinux policy).
loadLibraryGlobal :: FilePath -> IO (Either DynError Library)
loadLibraryGlobal = loadWith [DL.RTLD_NOW, DL.RTLD_GLOBAL]

loadWith :: [DL.RTLDFlags] -> FilePath -> IO (Either DynError Library)
loadWith flags path = do
  r <- try (DL.dlopen path flags)
  pure $ case r of
    Left (e :: IOException) -> Left (LibraryNotFound path (show e))
    Right dl -> Right (Library dl path)

-- | Release the OS handle, best-effort: a failed @dlclose@ is ignored —
-- matching the Windows side, there is no recovery, and 'withLibrary'
-- must not let a cleanup failure replace the action's own exception.
-- 'FunPtr's resolved from this 'Library' must not be called afterwards.
closeLibrary :: Library -> IO ()
closeLibrary lib = void (try @IOException (DL.dlclose (libDL lib)))

-- | 'loadLibrary' \/ 'closeLibrary' bracket, async-exception-safe: the
-- window between a successful load and the cleanup registration is
-- masked, so a timeout cannot leak the handle.
withLibrary :: FilePath -> (Library -> IO a) -> IO (Either DynError a)
withLibrary path act = mask $ \restore -> do
  r <- loadLibrary path
  case r of
    Left e -> pure (Left e)
    Right lib -> restore (Right <$> act lib) `finally` closeLibrary lib

-- | Resolve an exported symbol to a 'FunPtr', to be invoked through a
-- @foreign import ccall \"dynamic\"@ wrapper. The result type is the
-- caller's unchecked claim about the C signature.
resolveSym :: Library -> String -> IO (Either DynError (FunPtr a))
resolveSym lib name = do
  r <- try (DL.dlsym (libDL lib) name)
  pure $ case r of
    Left (e :: IOException) ->
      Left (SymbolNotFound (libraryPath lib) (name <> ": " <> show e))
    Right fp -> Right (castFunPtr fp)

-- | 'resolveSym' flattened to 'Maybe', for symbols whose absence is an
-- expected, degradable condition rather than an error.
resolveOptional :: Library -> String -> IO (Maybe (FunPtr a))
resolveOptional lib name = either (const Nothing) Just <$> resolveSym lib name

-- | POSIX loaders take their search path from the environment
-- (@LD_LIBRARY_PATH@\/rpath) before process start; there is no runtime
-- registration equivalent to Windows' AddDllDirectory. Documented no-op.
addSearchDir :: FilePath -> IO Bool
addSearchDir _ = pure False