-- This file is part of Hoppy. -- -- Copyright 2015-2020 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 . {-# LANGUAGE ViewPatterns #-} -- | Interface for defining bindings to C++ functions. module Foreign.Hoppy.Generator.Spec.Function ( -- * Data type Function, fnT, fnT', -- * Construction makeFn, -- * Properties fnExtName, fnCName, fnPurity, fnParams, fnReturn, fnReqs, fnAddendum, fnExceptionHandlers, -- * Code generators CallDirection (..), -- ** C++ generator CppCallType (..), sayCppArgRead, sayCppArgNames, -- * Internal -- ** C++ generator sayCppExportFn, -- ** Haskell generator sayHsExportFn, sayHsArgProcessing, sayHsCallAndProcessReturn, ) where import Control.Monad (forM_, unless, when) import Control.Monad.Except (throwError) import Data.Function (on) import Data.List (intersperse) import Data.Maybe (catMaybes, fromMaybe, isJust) import Foreign.Hoppy.Generator.Common (fromMaybeM) import qualified Foreign.Hoppy.Generator.Language.Cpp as LC import qualified Foreign.Hoppy.Generator.Language.Haskell as LH import {-# SOURCE #-} qualified Foreign.Hoppy.Generator.Spec.Class as Class import Foreign.Hoppy.Generator.Spec.Base import Foreign.Hoppy.Generator.Types (constT, intT, objT, objToHeapT, ptrT, refT, voidT) import Language.Haskell.Syntax ( HsContext, HsName (HsIdent), HsQName (UnQual), HsQualType (HsQualType), HsType (HsTyApp, HsTyCon, HsTyFun, HsTyVar), ) -- | A C++ function declaration. -- -- Use this data type's 'HasReqs' instance to make the function accessible. You -- do not need to add requirements for parameter or return types. data Function = Function { fnCName :: FnName Identifier -- ^ The identifier used to call the function. , fnExtName :: ExtName -- ^ The function's external name. , fnPurity :: Purity -- ^ Whether the function is pure. , fnParams :: [Parameter] -- ^ The function's parameters. , fnReturn :: Type -- ^ The function's return type. , fnReqs :: Reqs -- ^ Requirements for bindings to access this function. , fnExceptionHandlers :: ExceptionHandlers -- ^ Exceptions that the function might throw. , fnAddendum :: Addendum -- ^ The function's addendum. } instance Eq Function where (==) = (==) `on` fnExtName instance Show Function where show fn = concat [""] instance Exportable Function where sayExportCpp = sayCppExport sayExportHaskell = sayHsExport instance HasExtNames Function where getPrimaryExtName = fnExtName instance HasReqs Function where getReqs = fnReqs setReqs reqs fn = fn { fnReqs = reqs } instance HasAddendum Function where getAddendum = fnAddendum setAddendum addendum fn = fn { fnAddendum = addendum } instance HandlesExceptions Function where getExceptionHandlers = fnExceptionHandlers modifyExceptionHandlers f fn = fn { fnExceptionHandlers = f $ fnExceptionHandlers fn } -- | Creates a binding for a C++ function. makeFn :: (IsFnName Identifier name, IsParameter p) => name -> Maybe ExtName -- ^ An optional external name; will be automatically derived from -- the identifier if absent. -> Purity -> [p] -- ^ Parameter types. -> Type -- ^ Return type. -> Function makeFn cName maybeExtName purity paramTypes retType = let fnName = toFnName cName in Function fnName (extNameOrFnIdentifier fnName maybeExtName) purity (toParameters paramTypes) retType mempty mempty mempty -- | A function taking parameters and returning a value (or 'voidT'). Function -- pointers must wrap a 'fnT' in a 'ptrT'. -- -- See also 'fnT'' which accepts parameter information. fnT :: [Type] -> Type -> Type -- (Keep docs in sync with hs-boot.) fnT = Internal_TFn . map toParameter -- | A version of 'fnT' that accepts additional information about parameters. fnT' :: [Parameter] -> Type -> Type -- (Keep docs in sync with hs-boot.) fnT' = Internal_TFn sayCppExport :: LC.SayExportMode -> Function -> LC.Generator () sayCppExport mode fn = case mode of LC.SayHeader -> return () LC.SaySource -> do LC.addReqsM $ fnReqs fn sayCppExportFn (fnExtName fn) (case fnCName fn of FnName identifier -> CallFn $ LC.sayIdentifier identifier FnOp op -> CallOp op) Nothing (fnParams fn) (fnReturn fn) (fnExceptionHandlers fn) True -- Render the body. -- | The direction between languages in which a value is being passed. data CallDirection = ToCpp -- ^ Haskell code is calling out to C++. | FromCpp -- ^ C++ is invoking a callback. deriving (Show) -- | The name of a function to call. data CppCallType = CallOp Operator -- ^ A call to the given operator, for example @x++@, @x * y@, @a[i]@. | CallFn (LC.Generator ()) -- ^ A call to the function whose name is emitted by the given action. | VarRead (LC.Generator ()) -- ^ Not a function call, but a read from a variable whose name is emitted -- by the given action. | VarWrite (LC.Generator ()) -- ^ Not a function call, but a write to a variable whose name is emitted by -- the given action. -- | Generates a C++ wrapper function for calling a C++ function (or method, or -- reading from or writing to a variable). The generated function handles -- C++-side marshalling of values and propagating exceptions as requested. -- -- See also 'sayHsExportFn'. sayCppExportFn :: ExtName -- ^ The external name of the function. -> CppCallType -- ^ The C++ name at which the function can be invoked. -> Maybe Type -- ^ If present, then we are wrapping a method within some class, and the -- type is that of the class. -> [Parameter] -- ^ Info about the function's parameters. -> Type -- ^ The function's return type. -> ExceptionHandlers -- ^ Exception handlers configured on the function itself. No need to call -- 'LC.getEffectiveExceptionHandlers' to combine the function's handlers -- with those from the module and interface; this function does that already. -> Bool -- ^ Whether to generate the function definition. If false, only the -- declaration is generated (no function body). -> LC.Generator () sayCppExportFn extName callType maybeThisType params retType exceptionHandlers sayBody = do handlerList <- exceptionHandlersList <$> LC.getEffectiveExceptionHandlers exceptionHandlers let paramTypes = map parameterType params catches = not $ null handlerList addExceptionParamNames = if catches then (++ [LC.exceptionIdArgName, LC.exceptionPtrArgName]) else id addExceptionParamTypes = if catches then (++ [ptrT intT, ptrT $ ptrT voidT]) else id paramCount = length paramTypes paramCTypeMaybes <- mapM LC.typeToCType paramTypes let paramCTypes = zipWith fromMaybe paramTypes paramCTypeMaybes retCTypeMaybe <- LC.typeToCType retType let retCType = fromMaybe retType retCTypeMaybe LC.addReqsM . mconcat =<< mapM LC.typeReqs (retType:paramTypes) LC.sayFunction (LC.externalNameToCpp extName) (maybe id (const ("self":)) maybeThisType $ addExceptionParamNames $ 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]) (fnT (addExceptionParamTypes $ maybe id (:) maybeThisType paramCTypes) retCType) $ if not sayBody then Nothing else Just $ do when catches $ do LC.say "try {\n" LC.says ["*", LC.exceptionIdArgName, " = 0;\n"] -- Convert arguments that aren't passed in directly. mapM_ (sayCppArgRead ToCpp) $ zip3 [1..] paramTypes paramCTypeMaybes let -- Determines how to call the exported function or method. sayCall = case callType of CallOp op -> do LC.say "(" let effectiveParamCount = paramCount + if isJust maybeThisType then 1 else 0 paramNames@(p1:p2:_) = (if isJust maybeThisType then ("(*self)":) else id) $ map LC.toArgName [1..] assertParamCount n = when (effectiveParamCount /= n) $ LC.abort $ concat ["sayCppExportFn: Operator ", show op, " for export ", show extName, " requires ", show n, " parameter(s), but has ", show effectiveParamCount, "."] case operatorType op of UnaryPrefixOperator symbol -> assertParamCount 1 >> LC.says [symbol, p1] UnaryPostfixOperator symbol -> assertParamCount 1 >> LC.says [p1, symbol] BinaryOperator symbol -> assertParamCount 2 >> LC.says [p1, symbol, p2] CallOperator -> LC.says $ p1 : "(" : take (effectiveParamCount - 1) (drop 1 paramNames) ++ [")"] ArrayOperator -> assertParamCount 2 >> LC.says [p1, "[", p2, "]"] LC.say ")" CallFn sayCppName -> do when (isJust maybeThisType) $ LC.say "self->" sayCppName LC.say "(" sayCppArgNames paramCount LC.say ")" VarRead sayVarName -> do when (isJust maybeThisType) $ LC.say "self->" sayVarName VarWrite sayVarName -> do when (isJust maybeThisType) $ LC.say "self->" sayVarName LC.says [" = ", LC.toArgName 1] -- Writes the call, transforming the return value if necessary. -- These translations should be kept in sync with typeToCType. sayCallAndReturn retType' retCTypeMaybe' = case (retType', retCTypeMaybe') of -- Void needs special handling because we don't want a return statement. (Internal_TVoid, Nothing) -> sayCall >> LC.say ";\n" -- Custom conversions. (Internal_TManual s, _) -> do -- The ConversionSpec s may or may not specify an intermediate -- type to pass over the FFI boundary: the second value in the -- pair (we check this before the (_, Nothing) case below). We -- don't actually care what it is though, because s already -- specifies how to convert. case conversionSpecCppConversionToCppExpr $ conversionSpecCpp s of -- If there is a custom conversion expression defined, use it. Just convFn -> LC.say "return " >> convFn sayCall Nothing >> LC.say ";\n" -- Otherwise, assume we can just return the value directly. Nothing -> sayCallAndReturnDirect -- The case of a value for which no conversion is necessary. (_, Nothing) -> sayCallAndReturnDirect -- Object cases. (Internal_TRef cls, Just (Internal_TPtr cls')) | cls == cls' -> LC.say "return &(" >> sayCall >> LC.say ");\n" (Internal_TObj cls, Just (Internal_TPtr (Internal_TConst (Internal_TObj cls')))) | cls == cls' -> sayReturnNew cls sayCall (Internal_TObjToHeap cls, Just (Internal_TPtr (Internal_TObj cls'))) | cls == cls' -> sayReturnNew cls sayCall (Internal_TToGc (Internal_TObj cls), Just (Internal_TPtr (Internal_TObj cls'))) | cls == cls' -> sayReturnNew cls sayCall (Internal_TToGc retType'', _) -> sayCallAndReturn retType'' retCTypeMaybe' ts -> LC.abort $ concat ["sayCppExportFn: Unexpected return types ", show ts, " while generating binding for ", show extName, "."] sayCallAndReturnDirect = LC.say "return " >> sayCall >> LC.say ";\n" sayCallAndReturn retType retCTypeMaybe when catches $ do iface <- LC.askInterface forM_ handlerList $ \handler -> do LC.say "} catch (" case handler of CatchClass cls -> LC.sayVar LC.exceptionVarName Nothing $ refT $ constT $ objT cls CatchAll -> LC.say "..." LC.say ") {\n" exceptionId <- case handler of CatchClass cls -> case interfaceExceptionClassId iface cls of Just exceptionId -> return exceptionId Nothing -> LC.abort $ concat ["sayCppExportFn: Trying to catch non-exception class ", show cls, " while generating binding for ", show extName, "."] CatchAll -> return exceptionCatchAllId LC.says ["*", LC.exceptionIdArgName, " = ", show $ getExceptionId exceptionId, ";\n"] case handler of CatchAll -> LC.says ["*", LC.exceptionPtrArgName, " = 0;\n"] CatchClass cls -> do -- Object pointers don't convert automatically to void*. LC.says ["*", LC.exceptionPtrArgName, " = reinterpret_cast(new "] LC.sayType Nothing $ objT cls LC.says ["(", LC.exceptionVarName, "));\n"] -- For all of the types our gateway functions actually return, "return -- 0" is a valid statement. when (retType /= Internal_TVoid) $ LC.say "return 0;\n" LC.say "}\n" where sayReturnNew cls sayCall = LC.say "return new" >> LC.sayIdentifier (Class.classIdentifier cls) >> LC.say "(" >> sayCall >> LC.say ");\n" -- | Generates code to marshal a value between a C++ type and the intermediate -- type to be used over the FFI. If @dir@ is 'ToCpp', then we are a C++ -- function reading an argument from foreign code. If @dir@ is 'FromCpp', then -- we are invoking a foreign callback. sayCppArgRead :: CallDirection -> (Int, Type, Maybe Type) -> LC.Generator () sayCppArgRead dir (n, stripConst . normalizeType -> cppType, maybeCType) = case cppType of t@(Internal_TPtr (Internal_TFn params retType)) -> do -- Assert that all types referred to in a function pointer type are all -- representable as C types. let paramTypes = map parameterType params check label t' = ((label ++ " " ++ show t') <$) <$> LC.typeToCType t' mismatches <- fmap catMaybes $ (:) <$> check "return type" retType <*> mapM (\paramType -> check "parameter" paramType) paramTypes unless (null mismatches) $ LC.abort $ concat $ "sayCppArgRead: Some types within a function pointer type use non-C types, " : "but only C types may be used. The unsupported types are: " : intersperse "; " mismatches ++ [". The whole function type is ", show t, "."] convertDefault Internal_TRef t -> convertObj t Internal_TObj _ -> convertObj $ constT cppType Internal_TObjToHeap cls -> case dir of ToCpp -> error $ objToHeapTWrongDirectionErrorMsg (Just "sayCppArgRead") cls FromCpp -> do LC.sayIdentifier $ Class.classIdentifier cls LC.says ["* ", LC.toArgName n, " = new "] LC.sayIdentifier $ Class.classIdentifier cls LC.says ["(", LC.toArgNameAlt n, ");\n"] Internal_TToGc t' -> case dir of ToCpp -> error $ toGcTWrongDirectionErrorMsg (Just "sayCppArgRead") t' FromCpp -> do let newCppType = case t' of -- In the case of (TToGc (TObj _)), we copy the temporary object to -- the heap and let the foreign language manage that value. Internal_TObj cls -> objToHeapT cls _ -> t' cType <- LC.typeToCType newCppType sayCppArgRead dir (n, newCppType, cType) -- In case of a manual type, apply the custom conversion, if there is one. Internal_TManual s -> do let maybeConvExpr = (case dir of ToCpp -> conversionSpecCppConversionToCppExpr FromCpp -> conversionSpecCppConversionFromCppExpr) $ conversionSpecCpp s forM_ maybeConvExpr $ \gen -> gen (LC.say $ LC.toArgNameAlt n) (Just $ LC.say $ LC.toArgName n) _ -> convertDefault where -- Primitive types don't need to be encoded/decoded. But if maybeCType is a -- Just, then we're expected to do some encoding/decoding, so something is -- wrong. -- -- TODO Do we need to handle TConst? convertDefault = forM_ maybeCType $ \cType -> LC.abort $ concat ["sayCppArgRead: Don't know how to convert ", show dir, " between C-type ", show cType, " and C++-type ", show cppType, "."] convertObj cppType' = case dir of ToCpp -> do LC.sayVar (LC.toArgName n) Nothing $ refT cppType' LC.says [" = *", LC.toArgNameAlt n, ";\n"] FromCpp -> do LC.sayVar (LC.toArgName n) Nothing $ ptrT cppType' LC.says [" = &", LC.toArgNameAlt n, ";\n"] -- | Prints a comma-separated list of the argument names used for C++ gateway -- functions. The number specifies how many names to print. sayCppArgNames :: Int -> LC.Generator () sayCppArgNames count = LC.says $ intersperse ", " $ map LC.toArgName [1..count] sayHsExport :: LH.SayExportMode -> Function -> LH.Generator () sayHsExport mode fn = (sayHsExportFn mode <$> fnExtName <*> fnExtName <*> fnPurity <*> fnParams <*> fnReturn <*> fnExceptionHandlers) fn -- | Generates a Haskell wrapper function for calling a C++ function (or method, -- or reading from or writing to a variable, as with 'sayCppExportFn'). The -- generated function handles Haskell-side marshalling of values and propagating -- exceptions as requested. sayHsExportFn :: LH.SayExportMode -- ^ The phase of code generation. -> ExtName -- ^ The external name for the entity we're generating. For class -- entities, this will include the class's external name as a prefix. -> ExtName -- ^ An alternate external name to use to generate Haskell function names. -- For non-class entities, this can be just the regular external name. For -- class entities, in order to strip off the class name that was added so -- that the entity's external name is unique, this can just be the name of -- the function, variable, etc. -> Purity -- ^ Whether or not the function is pure (free of side effects). -> [Parameter] -- ^ Parameter info. -> Type -- ^ The return type. -> ExceptionHandlers -- ^ Any exception handlers to apply to the binding, in addition to what -- its module and interface provide. -> LH.Generator () sayHsExportFn mode extName foreignName purity params retType exceptionHandlers = do effectiveHandlers <- LH.getEffectiveExceptionHandlers exceptionHandlers let handlerList = exceptionHandlersList effectiveHandlers catches = not $ null handlerList paramTypes = map parameterType params -- We use the pure version of toHsFnName here; because foreignName isn't an -- ExtName present in the interface's lookup table, toHsFnName would bail on -- it. Since functions don't reference each other (e.g. we don't put anything -- in .hs-boot files for them in circular modules cases), this isn't a problem. let hsFnName = LH.toHsFnName' foreignName hsFnImportedName = hsFnName ++ "'" case mode of LH.SayExportForeignImports -> LH.withErrorContext ("generating imports for function " ++ show extName) $ do -- Print a "foreign import" statement. hsCType <- fnToHsTypeAndUse LH.HsCSide purity params retType effectiveHandlers LH.saysLn ["foreign import ccall \"", LC.externalNameToCpp extName, "\" ", hsFnImportedName, " :: ", renderFnHsType hsCType] LH.SayExportDecls -> LH.withErrorContext ("generating function " ++ show extName) $ do -- Print the type signature. LH.ln LH.addExport hsFnName hsHsType <- fnToHsTypeAndUse LH.HsHsSide purity params retType effectiveHandlers LH.saysLn [hsFnName, " :: ", renderFnHsTypeWithNames hsHsType] case purity of Nonpure -> return () Pure -> LH.saysLn ["{-# NOINLINE ", hsFnName, " #-}"] -- Print the function body. let argNames = map LH.toArgName [1..length paramTypes] convertedArgNames = map (++ "'") argNames -- Operators on this line must bind more weakly than operators used below, -- namely ($) and (>>=). (So finish the line with ($).) lineEnd <- case purity of Nonpure -> return [" ="] Pure -> do LH.addImports $ mconcat [hsImport1 "Prelude" "($)", hsImportForUnsafeIO] return [" = HoppySIU.unsafePerformIO $"] LH.saysLn $ hsFnName : map (' ':) argNames ++ lineEnd LH.indent $ do forM_ (zip3 paramTypes argNames convertedArgNames) $ \(t, argName, argName') -> sayHsArgProcessing ToCpp t argName argName' exceptionHandling <- if catches then do iface <- LH.askInterface currentModule <- LH.askModule let exceptionSupportModule = interfaceExceptionSupportModule iface when (exceptionSupportModule /= Just currentModule) $ LH.addImports . hsWholeModuleImport . LH.getModuleName iface =<< fromMaybeM (throwError "Internal error, an exception support module is not available") exceptionSupportModule LH.addImports $ mconcat [hsImport1 "Prelude" "($)", hsImportForRuntime] return "HoppyFHR.internalHandleExceptions exceptionDb' $" else return "" let callWords = exceptionHandling : hsFnImportedName : map (' ':) convertedArgNames sayHsCallAndProcessReturn ToCpp retType callWords LH.SayExportBoot -> -- Functions (methods included) cannot be referenced from other exports, -- so we don't need to emit anything. -- -- If this changes, revisit the comment on hsFnName above. return () -- | Generates Haskell code to perform marshalling of a function's argument in a -- specified direction. -- -- This function either generates a line or lines such that subsequent lines can -- refer to the output binding. The final line is either terminated with -- -- > ... $ \value -> -- -- or -- -- > let ... in -- -- so that precedence is not an issue. sayHsArgProcessing :: CallDirection -- ^ The direction of the FFI call. -> Type -- ^ The type of the value to be marshalled. -> String -- ^ The name of the binding holding the input value. -> String -- ^ The name of the binding to create for the output value. -> LH.Generator () sayHsArgProcessing dir t fromVar toVar = LH.withErrorContext ("processing argument of type " ++ show t) $ case t of Internal_TVoid -> throwError $ "TVoid is not a valid argument type" -- References and pointers are handled equivalently. Internal_TPtr (Internal_TObj cls) -> case dir of ToCpp -> do LH.addImports $ mconcat [hsImport1 "Prelude" "($)", hsImportForRuntime] castMethodName <- Class.toHsCastMethodName Nonconst cls LH.saysLn ["HoppyFHR.withCppPtr (", castMethodName, " ", fromVar, ") $ \\", toVar, " ->"] FromCpp -> do ctorName <- Class.toHsDataCtorName LH.Unmanaged Nonconst cls LH.saysLn ["let ", toVar, " = ", ctorName, " ", fromVar, " in"] Internal_TPtr (Internal_TConst (Internal_TObj cls)) -> case dir of ToCpp -> do -- Same as the (TObj _), ToCpp case. LH.addImports $ mconcat [hsImport1 "Prelude" "($)", hsImportForPrelude, hsImportForRuntime] withValuePtrName <- Class.toHsWithValuePtrName cls LH.saysLn [withValuePtrName, " ", fromVar, " $ HoppyP.flip HoppyFHR.withCppPtr $ \\", toVar, " ->"] FromCpp -> do ctorName <- Class.toHsDataCtorName LH.Unmanaged Const cls LH.saysLn ["let ", toVar, " = ", ctorName, " ", fromVar, " in"] Internal_TPtr _ -> noConversion Internal_TRef t' -> sayHsArgProcessing dir (ptrT t') fromVar toVar Internal_TFn {} -> throwError "TFn unimplemented" Internal_TObj cls -> case dir of ToCpp -> do -- Same as the (TPtr (TConst (TObj _))), ToPtr case. LH.addImports $ mconcat [hsImport1 "Prelude" "($)", hsImportForPrelude, hsImportForRuntime] withValuePtrName <- Class.toHsWithValuePtrName cls LH.saysLn [withValuePtrName, " ", fromVar, " $ HoppyP.flip HoppyFHR.withCppPtr $ \\", toVar, " ->"] FromCpp -> case Class.classHaskellConversionFromCppFn $ LH.getClassHaskellConversion cls of Just _ -> do LH.addImports $ mconcat [hsImport1 "Prelude" "(>>=)", hsImportForRuntime] ctorName <- Class.toHsDataCtorName LH.Unmanaged Const cls LH.saysLn ["HoppyFHR.decode (", ctorName, " ", fromVar, ") >>= \\", toVar, " ->"] Nothing -> throwError $ concat ["Can't pass a TObj of ", show cls, " from C++ to Haskell because no class decode conversion is defined"] Internal_TObjToHeap cls -> case dir of ToCpp -> throwError $ objToHeapTWrongDirectionErrorMsg Nothing cls FromCpp -> sayHsArgProcessing dir (ptrT $ objT cls) fromVar toVar Internal_TToGc t' -> case dir of ToCpp -> throwError $ toGcTWrongDirectionErrorMsg Nothing t' FromCpp -> do LH.addImports $ mconcat [hsImport1 "Prelude" "(>>=)", hsImportForRuntime] ctorName <- maybe (throwError $ tToGcInvalidFormErrorMessage Nothing t') (Class.toHsDataCtorName LH.Unmanaged Nonconst) $ case stripConst t' of Internal_TObj cls -> Just cls Internal_TRef (Internal_TConst (Internal_TObj cls)) -> Just cls Internal_TRef (Internal_TObj cls) -> Just cls Internal_TPtr (Internal_TConst (Internal_TObj cls)) -> Just cls Internal_TPtr (Internal_TObj cls) -> Just cls _ -> Nothing LH.saysLn ["HoppyFHR.toGc (", ctorName, " ", fromVar, ") >>= \\", toVar, " ->"] Internal_TConst t' -> sayHsArgProcessing dir t' fromVar toVar Internal_TManual s -> do let maybeGen = fmap (case dir of ToCpp -> conversionSpecHaskellToCppFn FromCpp -> conversionSpecHaskellFromCppFn) $ conversionSpecHaskell s throwForNoConversion = throwError $ concat ["No conversion defined for ", show s, case dir of ToCpp -> " to C++ from Haskell" FromCpp -> " from C++ to Haskell"] case maybeGen of Just (CustomConversion gen) -> do LH.addImports $ hsImport1 "Prelude" "(>>=)" LH.sayLn "(" LH.indent gen LH.saysLn [") ", fromVar, " >>= \\", toVar, " ->"] Just BinaryCompatible -> LH.saysLn ["let ", toVar, " = ", fromVar, " in"] Just ConversionUnsupported -> throwForNoConversion Nothing -> throwForNoConversion where noConversion = LH.saysLn ["let ", toVar, " = ", fromVar, " in"] -- | Note that the 'CallDirection' is the direction of the call, not the -- direction of the return. 'ToCpp' means we're returning to the foreign -- language, 'FromCpp' means we're returning from it. sayHsCallAndProcessReturn :: CallDirection -> Type -> [String] -> LH.Generator () sayHsCallAndProcessReturn dir t callWords = LH.withErrorContext ("processing return value of type " ++ show t) $ case t of Internal_TVoid -> sayCall -- The same as TPtr (TConst (TObj _)), but nonconst. Internal_TPtr (Internal_TObj cls) -> do case dir of ToCpp -> do LH.addImports hsImportForPrelude ctorName <- Class.toHsDataCtorName LH.Unmanaged Nonconst cls LH.saysLn ["HoppyP.fmap ", ctorName] sayCall FromCpp -> do LH.addImports $ mconcat [hsImportForPrelude, hsImportForRuntime] LH.sayLn "HoppyP.fmap HoppyFHR.toPtr" sayCall -- The same as TPtr (TConst (TObj _)), but nonconst. Internal_TPtr (Internal_TConst (Internal_TObj cls)) -> case dir of ToCpp -> do LH.addImports hsImportForPrelude ctorName <- Class.toHsDataCtorName LH.Unmanaged Const cls LH.saysLn ["HoppyP.fmap ", ctorName] sayCall FromCpp -> do LH.addImports $ mconcat [hsImportForPrelude, hsImportForRuntime] LH.sayLn "HoppyP.fmap HoppyFHR.toPtr" sayCall Internal_TPtr _ -> sayCall Internal_TRef t' -> sayHsCallAndProcessReturn dir (ptrT t') callWords Internal_TFn {} -> throwError "TFn unimplemented" Internal_TObj cls -> case dir of ToCpp -> case Class.classHaskellConversionFromCppFn $ LH.getClassHaskellConversion cls of Just _ -> do LH.addImports $ mconcat [hsImports "Prelude" ["(.)", "(=<<)"], hsImportForRuntime] ctorName <- Class.toHsDataCtorName LH.Unmanaged Const cls LH.saysLn ["(HoppyFHR.decodeAndDelete . ", ctorName, ") =<<"] sayCall Nothing -> throwError $ concat ["Can't return a TObj of ", show cls, " from C++ to Haskell because no class decode conversion is defined"] FromCpp -> do LH.addImports $ mconcat [hsImports "Prelude" ["(.)", "(=<<)"], hsImportForPrelude, hsImportForRuntime] LH.sayLn "(HoppyP.fmap (HoppyFHR.toPtr) . HoppyFHR.encode) =<<" sayCall Internal_TObjToHeap cls -> case dir of ToCpp -> sayHsCallAndProcessReturn dir (ptrT $ objT cls) callWords FromCpp -> throwError $ objToHeapTWrongDirectionErrorMsg Nothing cls Internal_TToGc t' -> case dir of ToCpp -> do LH.addImports $ mconcat [hsImport1 "Prelude" "(=<<)", hsImportForRuntime] LH.sayLn "HoppyFHR.toGc =<<" -- TToGc (TObj _) should create a pointer rather than decoding, so we -- change the TObj _ into a TPtr (TObj _). case t' of Internal_TObj _ -> sayHsCallAndProcessReturn dir (ptrT t') callWords _ -> sayHsCallAndProcessReturn dir t' callWords FromCpp -> throwError $ toGcTWrongDirectionErrorMsg Nothing t' Internal_TConst t' -> sayHsCallAndProcessReturn dir t' callWords Internal_TManual s -> do -- Remember 'dir' is backward here, because we're dealing with a return -- value, so these functions look backward. let maybeGen = fmap (case dir of ToCpp -> conversionSpecHaskellFromCppFn FromCpp -> conversionSpecHaskellToCppFn) $ conversionSpecHaskell s throwForNoConversion = throwError $ concat ["No conversion defined for ", show s, case dir of ToCpp -> " from C++ to Haskell" FromCpp -> " to C++ from Haskell"] case maybeGen of Just (CustomConversion gen) -> do LH.addImports $ hsImport1 "Prelude" "(=<<)" LH.sayLn "(" LH.indent gen LH.sayLn ") =<<" Just BinaryCompatible -> return () Just ConversionUnsupported -> throwForNoConversion Nothing -> throwForNoConversion sayCall where sayCall = LH.saysLn $ "(" : callWords ++ [")"] -- | The Haskell type of a 'Function', as computed by 'fnToHsTypeAndUse'. This -- combines a 'HsQualType' with a list of parameter names. data FnHsType = FnHsType { fnHsQualType :: HsQualType , fnHsParamNameMaybes :: [Maybe String] } -- | Implements special logic on top of 'LH.cppTypeToHsTypeAndUse', that -- computes the Haskell __qualified__ type for a function, including typeclass -- constraints, and bundles it with parameter names. fnToHsTypeAndUse :: LH.HsTypeSide -> Purity -> [Parameter] -> Type -> ExceptionHandlers -- ^ These should be the effective exception handlers for the function, as -- returned by -- @'LH.getEffectiveExceptionHandlers' . 'fnExceptionHandlers'@, -- not just the function's exception handlers directly from -- @fnExceptionHandlers@. -> LH.Generator FnHsType fnToHsTypeAndUse side purity params returnType exceptionHandlers = do let catches = not $ null $ exceptionHandlersList exceptionHandlers getsExcParams = catches && side == LH.HsCSide paramTypes = (if getsExcParams then (++ [ptrT intT, ptrT $ ptrT voidT]) else id) $ map parameterType params paramNameMaybes = (if getsExcParams then (++ [Just "excId", Just "excPtr"]) else id) $ map parameterName params defaultParamNames = map LH.toArgName [1..] defaultedParamNames = zipWith fromMaybe defaultParamNames paramNameMaybes paramQualTypes <- mapM contextForParam $ zip defaultedParamNames paramTypes let context = concatMap (\(HsQualType ctx _) -> ctx) paramQualTypes :: HsContext hsParams = map (\(HsQualType _ t) -> t) paramQualTypes -- Determine the 'HsHsSide' return type for the function. Do the conversion -- to a Haskell type, and wrap the result in 'IO' if the function is impure. -- (HsCSide types always get wrapped in IO.) hsReturnInitial <- LH.cppTypeToHsTypeAndUse side returnType hsReturnForPurity <- case (purity, side) of (Pure, LH.HsHsSide) -> return hsReturnInitial _ -> do LH.addImports hsImportForPrelude return $ HsTyApp (HsTyCon $ UnQual $ HsIdent "HoppyP.IO") hsReturnInitial return FnHsType { fnHsQualType = HsQualType context $ foldr HsTyFun hsReturnForPurity hsParams , fnHsParamNameMaybes = paramNameMaybes } where contextForParam :: (String, Type) -> LH.Generator HsQualType contextForParam (s, t) = case t of Internal_TPtr (Internal_TObj cls) -> receivePtr s cls Nonconst Internal_TPtr (Internal_TConst (Internal_TObj cls)) -> receiveValue s t cls Internal_TRef (Internal_TObj cls) -> receivePtr s cls Nonconst Internal_TRef (Internal_TConst (Internal_TObj cls)) -> receiveValue s t cls Internal_TObj cls -> receiveValue s t cls Internal_TManual spec -> -- We add a typeclass constraint iff we're generating an exposed -- Haskell function (HsHsSide) and there is a constraint declared. -- If we're generating the underlying C FFI function, or if there is -- no constraint declared, then don't add one. case (side, conversionSpecHaskell spec >>= conversionSpecHaskellHsArgType) of (LH.HsHsSide, Just f) -> f $ HsIdent s _ -> handoff side t Internal_TConst t' -> contextForParam (s, t') _ -> handoff side t -- Use whatever type 'cppTypeToHsTypeAndUse' suggests, with no typeclass -- constraints. handoff :: LH.HsTypeSide -> Type -> LH.Generator HsQualType handoff side' t = HsQualType [] <$> LH.cppTypeToHsTypeAndUse side' t -- Receives a @FooPtr this => this@. receivePtr :: String -> Class.Class -> Constness -> LH.Generator HsQualType receivePtr s cls cst = case side of LH.HsHsSide -> do ptrClassName <- Class.toHsPtrClassName cst cls let t' = HsTyVar $ HsIdent s return $ HsQualType [(UnQual $ HsIdent ptrClassName, [t'])] t' LH.HsCSide -> do LH.addImports hsImportForForeign typeName <- Class.toHsDataTypeName cst cls return $ HsQualType [] $ HsTyApp (HsTyCon $ UnQual $ HsIdent "HoppyF.Ptr") (HsTyVar $ HsIdent typeName) -- Receives a @FooValue a => a@. receiveValue :: String -> Type -> Class.Class -> LH.Generator HsQualType receiveValue s t cls = case side of LH.HsCSide -> handoff side t LH.HsHsSide -> do LH.addImports hsImportForRuntime valueClassName <- Class.toHsValueClassName cls let t' = HsTyVar $ HsIdent s return $ HsQualType [(UnQual $ HsIdent valueClassName, [t'])] t' -- | Renders a 'FnHsType' as a Haskell type, ignoring parameter names. This -- implementation uses haskell-src. renderFnHsType :: FnHsType -> String renderFnHsType = LH.prettyPrint . fnHsQualType -- | Renders a 'FnHsType' as a Haskell type, including Haddock for parameter -- names. -- -- Unfortunately, we have to implement this ourselves, because haskell-src -- doesn't support comments, and haskell-src-exts's comments implementation -- relies on using specific source spans, and we don't want all that complexity -- here. So instead we render it ourselves, inserting "{- ^ ... -}" tags where -- appropriate. renderFnHsTypeWithNames :: FnHsType -> String renderFnHsTypeWithNames fnHsType = concat $ renderedContextStrs ++ renderedParamStrs where HsQualType assts unqualType = fnHsQualType fnHsType paramNameMaybes = fnHsParamNameMaybes fnHsType renderedContextStrs :: [String] renderedContextStrs = if null assts then [] else "(" : intersperse ", " (map renderAsst assts) ++ [") => "] renderAsst :: (HsQName, [HsType]) -> String renderAsst asst = case asst of (UnQual (HsIdent typeclass), [HsTyVar (HsIdent typeVar)]) -> concat [typeclass, " ", typeVar] _ -> error $ "renderAsst: Unexpected argument: " ++ show asst renderedParamStrs :: [String] renderedParamStrs = renderParams unqualType paramNameMaybes renderParams :: HsType -> [Maybe String] -> [String] renderParams fnType' paramNameMaybes' = case (fnType', paramNameMaybes') of -- If there's a parameter name, then generate a Haddock comment -- showing the name. (HsTyFun a b, (Just name):restNames) -> "(" : LH.prettyPrint a : ") {- ^ " : name : " -} -> " : renderParams b restNames -- If there's no parameter name, then don't generate any documentation -- for it, but continue to recur in case there are other parameters -- with names. (HsTyFun a b, Nothing:restNames) -> "(" : LH.prettyPrint a : ") -> " : renderParams b restNames -- If we've reached the end of the TyFun chain, then we don't need to -- recur further. We can use 'prettyPrint' to render the rest. _ -> "(" : LH.prettyPrint fnType' : [")"]