packages feed

haskell-gi-0.8: src/GI/Signal.hs

module GI.Signal
    ( genSignal
    , genCallback
    , signalHaskellName
    ) where

#if !MIN_VERSION_base(4,8,0)
import Control.Applicative ((<$>))
#endif
import Control.Monad (forM, forM_, when, unless)

import Data.List (intercalate)
import Data.Typeable (typeOf)
import Data.Bool (bool)
import qualified Data.Text as T

import Text.Show.Pretty (ppShow)

import GI.API
import GI.Callable (hOutType, arrayLengths, wrapMaybe)
import GI.Code
import GI.Conversions
import GI.SymbolNaming
import GI.Transfer (freeContainerType)
import GI.Type
import GI.Util (split, parenthesize, withComment)

-- The prototype of the callback on the Haskell side (what users of
-- the binding will see)
genHaskellCallbackPrototype :: Callable -> String -> [Arg] -> [Arg] ->
                               ExcCodeGen ()
genHaskellCallbackPrototype cb name' hInArgs hOutArgs = do
  group $ do
    line $ "type " ++ name' ++ " ="
    indent $ do
      forM_ hInArgs $ \arg -> do
        ht <- haskellType (argType arg)
        wrapMaybe arg >>= bool
                          (line $ show ht ++ " ->")
                          (line $ show (maybeT ht) ++ " ->")
      ret <- hOutType cb hOutArgs False
      line $ show $ io ret

  -- For optional parameters, in case we want to pass Nothing.
  group $ do
    line $ "no" ++ name' ++ " :: Maybe " ++ name'
    line $ "no" ++ name' ++ " = Nothing"

-- Prototype of the callback on the C side
genCCallbackPrototype :: Callable -> String -> Bool -> CodeGen ()
genCCallbackPrototype cb name' isSignal =
  group $ do
    line $ "type " ++ name' ++ "C ="
    indent $ do
      when isSignal $ line $ withComment "Ptr () ->" "object"
      forM_ (args cb) $ \arg -> do
        ht <- foreignType $ argType arg
        let ht' = if direction arg /= DirectionIn
                  then ptr ht
                  else ht
        line $ show ht' ++ " ->"
      when isSignal $ line $ withComment "Ptr () ->" "user_data"
      ret <- io <$> case returnType cb of
                      TBasicType TVoid -> return $ typeOf ()
                      t -> foreignType t
      line $ show ret

-- Generator for wrappers callable from C
genCallbackWrapperFactory :: String -> CodeGen ()
genCallbackWrapperFactory name' =
  group $ do
    line "foreign import ccall \"wrapper\""
    indent $ line $ "mk" ++ name' ++ " :: "
               ++ name' ++ "C -> IO (FunPtr " ++ name' ++ "C)"

-- Generator of closures
genClosure :: String -> String -> Bool -> CodeGen ()
genClosure callback closure isSignal =
    group $ do
      line $ closure ++ " :: " ++ callback ++ " -> IO Closure"
      line $ closure ++ " cb = newCClosure =<< mk" ++ callback ++ " wrapped"
      indent $
         line $ "where wrapped = " ++ lcFirst callback ++ "Wrapper " ++
              if isSignal
              then "cb"
              else "Nothing cb"

-- Wrap a conversion of a nullable object into "Maybe" object, by
-- checking whether the pointer is NULL.
convertNullable :: String -> BaseCodeGen e String -> BaseCodeGen e String
convertNullable aname c = do
  line $ "maybe" ++ ucFirst aname ++ " <-"
  indent $ do
    line $ "if " ++ aname ++ " == nullPtr"
    line   "then return Nothing"
    line   "else do"
    indent $ do
             unpacked <- c
             line $ "return $ Just " ++ unpacked
    return $ "maybe" ++ ucFirst aname

-- Convert a non-zero terminated out array, stored in a variable
-- named "aname", into the corresponding Haskell object.
convertCallbackInCArray :: Callable -> Arg -> Type -> String -> ExcCodeGen String
convertCallbackInCArray callable arg t@(TCArray False (-1) length _) aname =
  if length > -1
  then wrapMaybe arg >>= bool convertAndFree
                         (convertNullable aname convertAndFree)
  else
    -- Not much we can do, we just pass the pointer along, and let
    -- the callback deal with it.
    return aname
  where
    lname = escapeReserved $ argName $ args callable !! length

    convertAndFree :: ExcCodeGen String
    convertAndFree = do
      unpacked <- convert aname $ unpackCArray lname t (transfer arg)
      -- Free the memory associated with the array
      freeContainerType (transfer arg) t aname lname
      return unpacked

-- Remove the warning, this should never be reached.
convertCallbackInCArray _ t _ _ =
    error $ "convertOutCArray : unexpected " ++ show t

-- Prepare an argument for passing into the Haskell side.
prepareArgForCall :: Callable -> Arg -> ExcCodeGen String
prepareArgForCall cb arg = case direction arg of
  DirectionIn -> prepareInArg cb arg
  DirectionInout -> prepareInoutArg arg
  DirectionOut -> error "Unexpected DirectionOut!"

prepareInArg :: Callable -> Arg -> ExcCodeGen String
prepareInArg cb arg = do
  let name = (escapeReserved . argName) arg
  case argType arg of
    t@(TCArray False _ _ _) -> convertCallbackInCArray cb arg t name
    _ -> do
      let c = convert name $ fToH (argType arg) (transfer arg)
      wrapMaybe arg >>= bool c (convertNullable name c)

prepareInoutArg :: Arg -> ExcCodeGen String
prepareInoutArg arg = do
  let name = (escapeReserved . argName) arg
  name' <- genConversion name $ apply $ M "peek"
  convert name' $ fToH (argType arg) (transfer arg)

saveOutArg :: Arg -> ExcCodeGen ()
saveOutArg arg = do
  let name = (escapeReserved . argName) arg
      name' = "out" ++ name
  when (transfer arg /= TransferEverything) $
       notImplementedError $ "Unexpected transfer type for \"" ++ name ++ "\""
  name'' <- convert name' $ hToF (argType arg) TransferEverything
  line $ "poke " ++ name ++ " " ++ name''

-- The wrapper itself, marshalling to and from Haskell. The first
-- argument is possibly a pointer to a FunPtr to free (via
-- freeHaskellFunPtr) once the callback is run once, or Nothing if the
-- FunPtr will be freed by someone else (the function registering the
-- callback for ScopeTypeCall, or a destroy notifier for
-- ScopeTypeNotified).
genCallbackWrapper :: Callable -> String -> [Arg] -> [Arg] -> [Arg] ->
                      Bool -> ExcCodeGen ()
genCallbackWrapper cb name' dataptrs hInArgs hOutArgs isSignal = do
  let cName arg = if arg `elem` dataptrs
                  then "_"
                  else (escapeReserved . argName) arg
      cArgNames = map cName (args cb)

  group $ do
    line $ lcFirst name' ++ "Wrapper ::"
    indent $ do
      unless isSignal $
           line $ "Maybe (Ptr (FunPtr (" ++ name' ++ "C))) ->"
      line $ name' ++ " ->"
      when isSignal $ line "Ptr () ->"
      forM_ (args cb) $ \arg -> do
        ht <- foreignType $ argType arg
        let ht' = if direction arg /= DirectionIn
                  then ptr ht
                  else ht
        line $ show ht' ++ " ->"
      when isSignal $ line "Ptr () ->"
      ret <- io <$> case returnType cb of
                      TBasicType TVoid -> return $ typeOf ()
                      t -> foreignType t
      line $ show ret

    let allArgs = if isSignal
                  then unwords $ ["_cb", "_"] ++ cArgNames ++ ["_"]
                  else unwords $ ["funptrptr", "_cb"] ++ cArgNames
    line $ lcFirst name' ++ "Wrapper " ++ allArgs ++ " = do"
    indent $ do
      hInNames <- forM hInArgs (prepareArgForCall cb)

      let maybeReturn = case returnType cb of
                          TBasicType TVoid -> []
                          _                -> ["result"]
          argName' = escapeReserved . argName
          returnVars = maybeReturn ++ map (("out"++) . argName') hOutArgs
          returnBind = case returnVars of
                         []  -> ""
                         [r] -> r ++ " <- "
                         _   -> parenthesize (intercalate ", " returnVars) ++ " <- "
      line $ returnBind ++ "_cb " ++ concatMap (" " ++) hInNames

      forM_ hOutArgs saveOutArg

      unless isSignal $ line "maybeReleaseFunPtr funptrptr"

      when (returnType cb /= TBasicType TVoid) $
           if returnMayBeNull cb
           then do
             line "maybeM nullPtr result $ \\result' -> do"
             indent $ unwrapped "result'"
           else unwrapped "result"
           where
             unwrapped rname = do
               result' <- convert rname $ hToF (returnType cb) (returnTransfer cb)
               line $ "return " ++ result'

genCallback :: Name -> Callback -> CodeGen ()
genCallback n (Callback cb) = do
  name' <- upperName n
  line $ "-- callback " ++ name'

  let -- user_data pointers, which we generically omit
      dataptrs = map (args cb !!) . filter (/= -1) . map argClosure $ args cb
      hidden = dataptrs ++ arrayLengths cb

      inArgs = filter ((/= DirectionOut) . direction) $ args cb
      hInArgs = filter (not . (`elem` hidden)) inArgs
      outArgs = filter ((/= DirectionIn) . direction) $ args cb
      hOutArgs = filter (not . (`elem` hidden)) outArgs

  if skipReturn cb
  then group $ do
    line $ "-- XXX Skipping callback " ++ name'
    line $ "-- Callbacks skipping return unsupported :\n"
             ++ ppShow n ++ "\n" ++ ppShow cb
  else do
    let closure = lcFirst name' ++ "Closure"

    handleCGExc (\e -> line ("-- XXX Could not generate callback wrapper for "
                             ++ name' ++
                             "\n-- Error was : " ++ describeCGError e))
       (genClosure name' closure False >>
        genCCallbackPrototype cb name' False >>
        genCallbackWrapperFactory name' >>
        genHaskellCallbackPrototype cb name' hInArgs hOutArgs >>
        genCallbackWrapper cb name' dataptrs hInArgs hOutArgs False)

-- | Return the name for the signal in Haskell CamelCase conventions.
signalHaskellName :: String -> String
signalHaskellName sn = let (w:ws) = split '-' sn
                       in w ++ concatMap ucFirst ws

genSignal :: Signal -> Name -> ExcCodeGen ()
genSignal (Signal { sigName = sn, sigCallable = cb }) on = do
  on' <- upperName on

  line $ "-- signal " ++ on' ++ "::" ++ T.unpack sn

  let inArgs = filter ((/= DirectionOut) . direction) $ args cb
      hInArgs = filter (not . (`elem` arrayLengths cb)) inArgs
      outArgs = filter ((/= DirectionIn) . direction) $ args cb
      hOutArgs = filter (not . (`elem` arrayLengths cb)) outArgs
      sn' = signalHaskellName (T.unpack sn)
      signalConnectorName = on' ++ ucFirst sn'
      cbType = signalConnectorName ++ "Callback"

  genHaskellCallbackPrototype cb cbType hInArgs hOutArgs

  genCCallbackPrototype cb cbType True

  genCallbackWrapperFactory cbType

  let closure = lcFirst signalConnectorName ++ "Closure"
  genClosure cbType closure True

  genCallbackWrapper cb cbType [] hInArgs hOutArgs True

  -- Wrapper for connecting functions to the signal
  -- We can connect to a signal either before the default handler runs
  -- ("on...") or after the default handler runs (after...). We
  -- provide convenient wrappers for both cases.
  group $ do
    let signatureConstraints = "(GObject a, MonadIO m) =>"
        signatureArgs = "a -> " ++ cbType ++ " -> m SignalHandlerId"
        signature = " :: " ++ signatureConstraints ++ " " ++ signatureArgs
        onName = "on" ++ signalConnectorName
        afterName = "after" ++ signalConnectorName
    line $ onName ++ signature
    line $ onName ++ " obj cb = liftIO $ connect"
             ++ signalConnectorName ++ " obj cb SignalConnectBefore"
    line $ afterName ++ signature
    line $ afterName ++ " obj cb = connect"
             ++ signalConnectorName ++ " obj cb SignalConnectAfter"

  group $ do
    let fullName = "connect" ++ signalConnectorName
        signatureConstraints = "(GObject a, MonadIO m) =>"
        signatureArgs = "a -> " ++ cbType
                        ++ " -> SignalConnectMode -> m SignalHandlerId"
    line $ fullName ++ " :: " ++ signatureConstraints
    line $ replicate (4 + length fullName) ' ' ++ signatureArgs
    line $ fullName ++ " obj cb after = liftIO $ do"
    indent $ do
        line $ "cb' <- mk" ++ cbType ++ " (" ++ lcFirst cbType ++ "Wrapper cb)"
        line $ "connectSignalFunPtr obj \"" ++ T.unpack sn ++ "\" cb' after"