-- This file is part of Hoppy. -- -- Copyright 2015-2021 Bryan Gardiner -- -- This program is free software: you can redistribute it and/or modify -- it under the terms of the GNU Affero General Public License as published by -- the Free Software Foundation, either version 3 of the License, or -- (at your option) any later version. -- -- This program is distributed in the hope that it will be useful, -- but WITHOUT ANY WARRANTY; without even the implied warranty of -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -- GNU Affero General Public License for more details. -- -- You should have received a copy of the GNU Affero General Public License -- along with this program. If not, see . -- | Interface for defining foreign language callbacks. module Foreign.Hoppy.Generator.Spec.Callback ( -- * Data type Callback, callbackT, -- * Construction makeCallback, -- * Properties callbackExtName, callbackParams, callbackReturn, callbackReqs, callbackAddendum, -- ** Exceptions callbackThrows, callbackSetThrows, -- * C++ generator cppCallbackToTFn, -- ** Names callbackClassName, callbackImplClassName, callbackFnName, -- * Haskell generator hsCallbackToTFn, -- ** Names toHsCallbackCtorName, toHsCallbackCtorName', toHsCallbackNewFunPtrFnName, toHsCallbackNewFunPtrFnName', ) where import Control.Monad (forM_, when) import Data.Function (on) import Data.Maybe (fromMaybe, isJust) import qualified Foreign.Hoppy.Generator.Language.Cpp as LC import qualified Foreign.Hoppy.Generator.Language.Haskell as LH import Foreign.Hoppy.Generator.Spec.Base import qualified Foreign.Hoppy.Generator.Spec.Function as Function import Foreign.Hoppy.Generator.Types (boolT, constT, fnT, fnT', intT, manualT, ptrT, voidT) import Language.Haskell.Syntax ( HsName (HsIdent), HsQName (Special, UnQual), HsSpecialCon (HsUnitCon), HsType (HsTyApp, HsTyCon, HsTyFun), ) -- | A non-C++ function that can be invoked via a C++ functor or function -- pointer. -- -- Use this data type's 'HasReqs' instance to add extra requirements, however -- manually adding requirements for parameter and return types is not necessary. data Callback = Callback { callbackExtName :: ExtName -- ^ The callback's external name. , callbackParams :: [Parameter] -- ^ The callback's parameters. , callbackReturn :: Type -- ^ The callback's return type. , callbackThrows :: Maybe Bool -- ^ Whether the callback supports throwing C++ exceptions from Haskell into -- C++ during its execution. When absent, the value is inherited from -- 'moduleCallbacksThrow' and 'interfaceCallbacksThrow'. , callbackReqs :: Reqs -- ^ Extra requirements for the callback. , callbackAddendum :: Addendum -- ^ The callback's addendum. } instance Eq Callback where (==) = (==) `on` callbackExtName instance Show Callback where show cb = concat [" ExtName -> [p] -- ^ Parameter types. -> Type -- ^ Return type. -> Callback makeCallback extName paramTypes retType = Callback extName (toParameters paramTypes) retType Nothing mempty mempty -- | Sets whether a callback supports handling thrown C++ exceptions and passing -- them into C++. callbackSetThrows :: Bool -> Callback -> Callback callbackSetThrows value cb = cb { callbackThrows = Just value } makeConversion :: Callback -> ConversionSpec makeConversion cb = (makeConversionSpec (show cb) cpp) { conversionSpecHaskell = hs } where reqsGen = do -- TODO Should this be includeStd? cbClassReqs <- reqInclude . includeLocal . moduleHppPath <$> LC.findExportModule (callbackExtName cb) -- TODO Is the right 'ReqsType' being used recursively here? fnTypeReqs <- LC.typeReqs =<< cppCallbackToTFn cb return $ cbClassReqs `mappend` fnTypeReqs cpp = (makeConversionSpecCpp (callbackClassName cb) reqsGen) { conversionSpecCppConversionType = return $ Just $ ptrT callbackImplClassType , conversionSpecCppConversionToCppExpr = Just $ \fromVar maybeToVar -> case maybeToVar of Just toVar -> LC.says [callbackClassName cb, " "] >> toVar >> LC.say "(" >> fromVar >> LC.say ");\n" Nothing -> LC.says [callbackClassName cb, "("] >> fromVar >> LC.say ")" -- No from-C++ conversion; we don't support passing callbacks back out again. } hs = Just $ makeConversionSpecHaskell (LH.cppTypeToHsTypeAndUse LH.HsHsSide =<< hsCallbackToTFn LH.HsHsSide cb) (Just $ do LH.addImports hsImportForRuntime HsTyApp (HsTyCon $ UnQual $ HsIdent "HoppyFHR.CCallback") <$> (LH.cppTypeToHsTypeAndUse LH.HsCSide =<< hsCallbackToTFn LH.HsCSide cb)) (CustomConversion $ LH.sayLn =<< toHsCallbackCtorName cb) ConversionUnsupported -- Can't receive a callback from C++. callbackImplClassType = manualT $ makeConversionSpec implClass $ makeConversionSpecCpp implClass reqsGen implClass = callbackImplClassName cb -- | Constructs a type value for a callback. callbackT :: Callback -> Type -- (Keep docs in sync with hs-boot.) callbackT = manualT . makeConversion -- | Returns the name of the outer, copyable C++ class for a callback. callbackClassName :: Callback -> String callbackClassName = fromExtName . callbackExtName -- | Returns the name of the internal, non-copyable implementation C++ class for -- a callback. callbackImplClassName :: Callback -> String callbackImplClassName = (++ "_impl") . fromExtName . callbackExtName -- | Returns the name of the C++ binding function that creates a C++ callback -- wrapper object from a function pointer to foreign code. callbackFnName :: Callback -> String callbackFnName = LC.externalNameToCpp . callbackExtName sayCppExport :: LC.SayExportMode -> Callback -> LC.Generator () sayCppExport mode cb = do throws <- cppGetEffectiveCallbackThrows cb let className = callbackClassName cb implClassName = callbackImplClassName cb fnName = callbackFnName cb params = callbackParams cb paramTypes = map parameterType params paramCount = length params retType = callbackReturn cb fnType = fnT' params retType -- The function pointer we receive from foreign code will work with C-types, -- so determine what that function looks like. paramCTypes <- zipWith fromMaybe paramTypes <$> mapM LC.typeToCType paramTypes retCType <- fromMaybe retType <$> LC.typeToCType retType -- Add requirements specified manually by the callback, and for its parameter -- and return types. LC.addReqsM . mconcat . (callbackReqs cb:) =<< mapM LC.typeReqs (retType:paramTypes) let fnCType = fnT ((if throws then (++ [ptrT intT, ptrT $ ptrT voidT]) else id) paramCTypes) retCType fnPtrCType = ptrT fnCType case mode of LC.SayHeader -> do -- Render the class declarations into the header file. (sharedPtrReqs, sharedPtrStr) <- interfaceSharedPtr <$> LC.askInterface LC.addReqsM sharedPtrReqs LC.says ["\nclass ", implClassName, " {\n"] LC.say "public:\n" LC.says [" explicit ", implClassName, "("] >> LC.sayType Nothing fnPtrCType >> LC.say ", void(*)(void(*)()), bool);\n" LC.says [" ~", implClassName, "();\n"] LC.say " " >> LC.sayVar "operator()" Nothing fnType >> LC.say ";\n" LC.say "private:\n" LC.says [" ", implClassName, "(const ", implClassName, "&);\n"] LC.says [" ", implClassName, "& operator=(const ", implClassName, "&);\n"] LC.say "\n" LC.say " " >> LC.sayVar "f_" Nothing (constT fnPtrCType) >> LC.say ";\n" LC.say " void (*const release_)(void(*)());\n" LC.say " const bool releaseRelease_;\n" LC.say "};\n" LC.says ["\nclass ", className, " {\n"] LC.say "public:\n" LC.says [" ", className, "() {}\n"] LC.says [" explicit ", className, "(", implClassName, "* impl) : impl_(impl) {}\n"] LC.say " " >> LC.sayVar "operator()" Nothing fnType >> LC.say ";\n" LC.say " operator bool() const;\n" LC.say "private:\n" LC.says [" ", sharedPtrStr, "<", implClassName, "> impl_;\n"] LC.say "};\n" LC.SaySource -> do -- Render the classes' methods into the source file. First render the -- impl class's constructor. LC.says ["\n", implClassName, "::", implClassName, "("] >> LC.sayVar "f" Nothing fnPtrCType >> LC.say ", void (*release)(void(*)()), bool releaseRelease) :\n" LC.say " f_(f), release_(release), releaseRelease_(releaseRelease) {}\n" -- Then render the destructor. LC.says ["\n", implClassName, "::~", implClassName, "() {\n"] LC.say " if (release_) {\n" LC.say " release_(reinterpret_cast(f_));\n" LC.say " if (releaseRelease_) {\n" LC.say " release_(reinterpret_cast(release_));\n" LC.say " }\n" LC.say " }\n" LC.say "}\n" -- Render the impl operator() method, which does argument decoding and -- return value encoding and passes C++ values to underlying function -- poiner. -- -- TODO Abstract the duplicated code here and in sayExportFn. paramCTypeMaybes <- mapM LC.typeToCType paramTypes retCTypeMaybe <- LC.typeToCType retType LC.sayFunction (implClassName ++ "::operator()") (zipWith3 (\pt ctm -> -- TManual needs special handling to determine whether a -- conversion is necessary. 'typeToCType' doesn't suffice -- because for TManual this check relies on the direction of -- the call. See the special case in 'sayCppArgRead' as -- well. let hasConversion = case pt of Internal_TManual s -> isJust $ conversionSpecCppConversionToCppExpr $ conversionSpecCpp s _ -> isJust ctm in if hasConversion then LC.toArgNameAlt else LC.toArgName) paramTypes paramCTypeMaybes [1..paramCount]) fnType $ Just $ do -- Convert arguments that aren't passed in directly. mapM_ (Function.sayCppArgRead Function.FromCpp) $ zip3 [1..] paramTypes paramCTypeMaybes when throws $ do LC.says ["int ", LC.exceptionIdArgName, " = 0;\n"] LC.says ["void *", LC.exceptionPtrArgName, " = 0;\n"] -- Add an include for the exception support module to be able to call the -- C++ rethrow function. iface <- LC.askInterface currentModule <- LC.askModule case interfaceExceptionSupportModule iface of Just exceptionSupportModule -> when (exceptionSupportModule /= currentModule) $ -- TODO Should this be includeStd? LC.addReqsM $ reqInclude $ includeLocal $ moduleHppPath exceptionSupportModule Nothing -> LC.abort $ "sayExportCallback: " ++ show iface ++ " uses exceptions, so it needs an exception support " ++ "module. Please use interfaceSetExceptionSupportModule." -- Invoke the function pointer into foreign code. let -- | Generates the call to the foreign language function pointer. sayCall :: LC.Generator () sayCall = do LC.say "f_(" Function.sayCppArgNames paramCount when throws $ do when (paramCount /= 0) $ LC.say ", " LC.says ["&", LC.exceptionIdArgName, ", &", LC.exceptionPtrArgName] LC.say ")" -- | Generates code to check whether an exception was thrown by the -- callback, and rethrows it in C++ if so. sayExceptionCheck :: LC.Generator () sayExceptionCheck = when throws $ do LC.says ["if (", LC.exceptionIdArgName, " != 0) { ", LC.exceptionRethrowFnName, "(", LC.exceptionIdArgName, ", ", LC.exceptionPtrArgName, "); }\n"] case (retType, retCTypeMaybe) of (Internal_TVoid, Nothing) -> do sayCall >> LC.say ";\n" sayExceptionCheck (_, Nothing) -> do LC.sayVar "result" Nothing retType >> LC.say " = " >> sayCall >> LC.say ";\n" sayExceptionCheck LC.say "return result;\n" (Internal_TObj cls1, Just retCType'@(Internal_TPtr (Internal_TConst (Internal_TObj cls2)))) | cls1 == cls2 -> do LC.sayVar "resultPtr" Nothing retCType' >> LC.say " = " >> sayCall >> LC.say ";\n" sayExceptionCheck LC.sayVar "result" Nothing retType >> LC.say " = *resultPtr;\n" LC.say "delete resultPtr;\n" LC.say "return result;\n" (Internal_TRef (Internal_TConst (Internal_TObj cls1)), Just (Internal_TPtr (Internal_TConst (Internal_TObj cls2)))) | cls1 == cls2 -> do LC.sayVar "resultPtr" Nothing retCType >> LC.say " = " >> sayCall >> LC.say ";\n" sayExceptionCheck LC.say "return *resultPtr;\n" (Internal_TRef (Internal_TObj cls1), Just (Internal_TPtr (Internal_TObj cls2))) | cls1 == cls2 -> do LC.sayVar "resultPtr" Nothing retCType >> LC.say " = " >> sayCall >> LC.say ";\n" sayExceptionCheck LC.say "return *resultPtr;\n" ts -> LC.abort $ concat ["sayExportCallback: Unexpected return types ", show ts, "."] -- Render the non-impl operator() method, which simply passes C++ values -- along to the impl object. LC.sayFunction (className ++ "::operator()") (map LC.toArgName [1..paramCount]) fnType $ Just $ do case retType of Internal_TVoid -> LC.say "(*impl_)(" _ -> LC.say "return (*impl_)(" Function.sayCppArgNames paramCount LC.say ");\n" -- Render "operator bool", which detects whether the callback was not -- default-constructed with no actual impl object. LC.says [className, "::operator bool() const {\n"] LC.say "return static_cast(impl_);\n" LC.say "}\n" -- Render the function that creates a new callback object. let newCallbackFnType = fnT [ fnPtrCType , ptrT (fnT [ptrT $ fnT [] voidT] voidT) , boolT ] $ Internal_TManual $ makeConversionSpec ("") $ makeConversionSpecCpp (implClassName ++ "*") (return mempty) LC.sayFunction fnName ["f", "release", "releaseRelease"] newCallbackFnType $ Just $ LC.says ["return new ", implClassName, "(f, release, releaseRelease);\n"] -- | Prints \"foreign import\" statements and an internal callback construction -- function for a given 'Callback' specification. For example, for a callback -- of 'LH.HsHsSide' type @Int -> String -> IO Int@, we will generate the -- following bindings: -- -- > foreign import ccall "wrapper" name'newFunPtr -- > :: (CInt -> Ptr CChar -> IO CInt) -- > -> IO (FunPtr (CInt -> Ptr CChar -> IO CInt)) -- > -- > -- (This is an ad-hoc generated binding for C++ callback impl class constructor.) -- > foreign import ccall "genpop__name_impl" name'newCallback -- > :: FunPtr (CInt -> Ptr CChar -> IO CInt) -- > -> FunPtr (FunPtr (IO ()) -> IO ()) -- > -> Bool -- > -> IO (CCallback (CInt -> Ptr CChar -> IO CInt)) -- > -- > name_newFunPtr :: (Int -> String -> IO Int) -> IO (FunPtr (CInt -> Ptr CChar -> IO CInt)) -- > name_newFunPtr f'hs = name'newFunPtr $ \excIdPtr excPtrPtr arg1 arg2 -> -- > internalHandleCallbackExceptions excIdPtr excPtrPtr $ -- > coerceIntegral arg1 >>= \arg1' -> -- > (...decode the C string) >>= \arg2' -> -- > fmap coerceIntegral -- > (f'hs arg1' arg2') -- > -- > name_new :: (Int -> String -> IO Int) -> IO (CCallback (CInt -> Ptr CChar -> IO CInt)) -- > name_new f = do -- > f'p <- name_newFunPtr f -- > name'newCallback f'p freeHaskellFunPtrFunPtr False sayHsExport :: LH.SayExportMode -> Callback -> LH.Generator () sayHsExport mode cb = LH.withErrorContext ("generating callback " ++ show (callbackExtName cb)) $ do let name = callbackExtName cb params = callbackParams cb retType = callbackReturn cb hsNewFunPtrFnName <- toHsCallbackNewFunPtrFnName cb hsCtorName <- toHsCallbackCtorName cb let hsCtorName'newCallback = hsCtorName ++ "'newCallback" hsCtorName'newFunPtr = hsCtorName ++ "'newFunPtr" hsFnCType <- LH.cppTypeToHsTypeAndUse LH.HsCSide =<< hsCallbackToTFn LH.HsCSide cb hsFnHsType <- LH.cppTypeToHsTypeAndUse LH.HsHsSide =<< hsCallbackToTFn LH.HsHsSide cb let getWholeNewFunPtrFnType = do LH.addImports $ mconcat [hsImportForForeign, hsImportForPrelude] return $ HsTyFun hsFnHsType $ HsTyApp (HsTyCon $ UnQual $ HsIdent "HoppyP.IO") $ HsTyApp (HsTyCon $ UnQual $ HsIdent "HoppyF.FunPtr") hsFnCType getWholeCtorType = do LH.addImports $ mconcat [hsImportForPrelude, hsImportForRuntime] return $ HsTyFun hsFnHsType $ HsTyApp (HsTyCon $ UnQual $ HsIdent "HoppyP.IO") $ HsTyApp (HsTyCon $ UnQual $ HsIdent "HoppyFHR.CCallback") hsFnCType case mode of LH.SayExportForeignImports -> do LH.addImports $ mconcat [hsImportForForeign, hsImportForPrelude, hsImportForRuntime] let hsFunPtrType = HsTyApp (HsTyCon $ UnQual $ HsIdent "HoppyF.FunPtr") hsFnCType hsFunPtrImportType = HsTyFun hsFnCType $ HsTyApp (HsTyCon $ UnQual $ HsIdent "HoppyP.IO") hsFunPtrType hsCallbackCtorImportType = HsTyFun hsFunPtrType $ HsTyFun (HsTyApp (HsTyCon $ UnQual $ HsIdent "HoppyF.FunPtr") $ HsTyFun (HsTyApp (HsTyCon $ UnQual $ HsIdent "HoppyF.FunPtr") $ HsTyApp (HsTyCon $ UnQual $ HsIdent "HoppyP.IO") $ HsTyCon $ Special HsUnitCon) $ HsTyApp (HsTyCon $ UnQual $ HsIdent "HoppyP.IO") $ HsTyCon $ Special HsUnitCon) $ HsTyFun (HsTyCon $ UnQual $ HsIdent "HoppyP.Bool") $ HsTyApp (HsTyCon $ UnQual $ HsIdent "HoppyP.IO") $ HsTyApp (HsTyCon $ UnQual $ HsIdent "HoppyFHR.CCallback") hsFnCType LH.saysLn ["foreign import ccall \"wrapper\" ", hsCtorName'newFunPtr, " :: ", LH.prettyPrint hsFunPtrImportType] LH.saysLn ["foreign import ccall \"", LC.externalNameToCpp name, "\" ", hsCtorName'newCallback, " :: ", LH.prettyPrint hsCallbackCtorImportType] LH.SayExportDecls -> do LH.addExports [hsNewFunPtrFnName, hsCtorName] -- Generate the *_newFunPtr function. wholeNewFunPtrFnType <- getWholeNewFunPtrFnType let paramCount = length params argNames = map LH.toArgName [1..paramCount] argNames' = map (++ "'") argNames throws <- hsGetEffectiveCallbackThrows cb LH.addImports $ mconcat [hsImport1 "Prelude" "($)", hsImportForRuntime] LH.ln LH.saysLn [hsNewFunPtrFnName, " :: ", LH.prettyPrint wholeNewFunPtrFnType] LH.saysLn $ hsNewFunPtrFnName : " f'hs = " : hsCtorName'newFunPtr : " $" : case (if throws then (++ ["excIdPtr", "excPtrPtr"]) else id) argNames of [] -> [] argNames'' -> [" \\", unwords argNames'', " ->"] LH.indent $ do when throws $ LH.sayLn "HoppyFHR.internalHandleCallbackExceptions excIdPtr excPtrPtr $" forM_ (zip3 params argNames argNames') $ \(p, argName, argName') -> Function.sayHsArgProcessing Function.FromCpp (parameterType p) argName argName' Function.sayHsCallAndProcessReturn Function.FromCpp retType $ "f'hs" : map (' ':) argNames' -- Generate the *_new function. wholeCtorType <- getWholeCtorType LH.ln LH.saysLn [hsCtorName, " :: ", LH.prettyPrint wholeCtorType] LH.saysLn [hsCtorName, " f'hs = do"] LH.indent $ do LH.saysLn ["f'p <- ", hsNewFunPtrFnName, " f'hs"] LH.saysLn [hsCtorName'newCallback, " f'p HoppyFHR.freeHaskellFunPtrFunPtr HoppyP.False"] LH.SayExportBoot -> do LH.addExports [hsNewFunPtrFnName, hsCtorName] wholeNewFunPtrFnType <- getWholeNewFunPtrFnType wholeCtorType <- getWholeCtorType LH.ln LH.saysLn [hsNewFunPtrFnName, " :: ", LH.prettyPrint wholeNewFunPtrFnType] LH.ln LH.saysLn [hsCtorName, " :: ", LH.prettyPrint wholeCtorType] -- | The name of the function that takes a Haskell function and wraps it in a -- callback object. This is internal to the binding; normal users can pass -- Haskell functions to be used as callbacks inplicitly. toHsCallbackCtorName :: Callback -> LH.Generator String toHsCallbackCtorName callback = LH.inFunction "toHsCallbackCtorName" $ LH.addExtNameModule (callbackExtName callback) $ toHsCallbackCtorName' callback -- | Pure version of 'toHsCallbackCtorName' that doesn't create a qualified -- name. toHsCallbackCtorName' :: Callback -> String toHsCallbackCtorName' callback = LH.toHsFnName' $ toExtName $ fromExtName (callbackExtName callback) ++ "_new" -- | The name of the function that takes a Haskell function with Haskell-side -- types and wraps it in a 'Foreign.Ptr.FunPtr' that does appropriate -- conversions to and from C-side types. toHsCallbackNewFunPtrFnName :: Callback -> LH.Generator String toHsCallbackNewFunPtrFnName callback = LH.inFunction "toHsCallbackNewFunPtrFnName" $ LH.addExtNameModule (callbackExtName callback) $ toHsCallbackNewFunPtrFnName' callback -- | Pure version of 'toHsCallbackNewFunPtrFnName' that doesn't create a qualified -- name. toHsCallbackNewFunPtrFnName' :: Callback -> String toHsCallbackNewFunPtrFnName' callback = LH.toHsFnName' $ toExtName $ fromExtName (callbackExtName callback) ++ "_newFunPtr" cppGetEffectiveCallbackThrows :: Callback -> LC.Generator Bool cppGetEffectiveCallbackThrows cb = case callbackThrows cb of Just b -> return b Nothing -> moduleCallbacksThrow <$> LC.askModule >>= \case Just b -> return b Nothing -> interfaceCallbacksThrow <$> LC.askInterface hsGetEffectiveCallbackThrows :: Callback -> LH.Generator Bool hsGetEffectiveCallbackThrows cb = case callbackThrows cb of Just b -> return b Nothing -> moduleCallbacksThrow <$> LH.askModule >>= \case Just b -> return b Nothing -> interfaceCallbacksThrow <$> LH.askInterface -- | Constructs the function type for a callback. A callback that throws has -- additional parameters. -- -- Keep this in sync with 'hsCallbackToTFn'. cppCallbackToTFn :: Callback -> LC.Generator Type cppCallbackToTFn cb = do throws <- mayThrow return $ Internal_TFn ((if throws then addExcParams else id) $ callbackParams cb) (callbackReturn cb) where mayThrow = case callbackThrows cb of Just t -> return t Nothing -> moduleCallbacksThrow <$> LC.askModule >>= \mt -> case mt of Just t -> return t Nothing -> interfaceCallbacksThrow <$> LC.askInterface addExcParams = (++ [toParameter $ ptrT intT, toParameter $ ptrT $ ptrT voidT]) -- | Constructs the function type for a callback. For Haskell, the type depends -- on the side; the C++ side has additional parameters. -- -- Keep this in sync with 'cppCallbackToTFn'. hsCallbackToTFn :: LH.HsTypeSide -> Callback -> LH.Generator Type hsCallbackToTFn side cb = do needsExcParams <- case side of LH.HsCSide -> mayThrow LH.HsHsSide -> return False return $ Internal_TFn ((if needsExcParams then addExcParams else id) $ callbackParams cb) (callbackReturn cb) where mayThrow = case callbackThrows cb of Just t -> return t Nothing -> moduleCallbacksThrow <$> LH.askModule >>= \mt -> case mt of Just t -> return t Nothing -> interfaceCallbacksThrow <$> LH.askInterface addExcParams = (++ [toParameter $ ptrT intT, toParameter $ ptrT $ ptrT voidT])