packages feed

hsjuiz-0.2.6: src/Juiz/Process.hs

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
-- | Process のエントリポイント (C から呼ばれる関数) を Haskell の関数から作る。
--
-- ふつうは "Juiz.TH" の 'Juiz.TH.juizProcess' が、ここの関数を使って foreign export まで生成する。
-- Template Haskell を使わずに手で書く場合は次のようにする (README を参照)。
--
-- > increment :: Int64 -> IO Int64
-- > increment arg1 = pure (arg1 + 1)
-- >
-- > hsManifest :: ManifestFunction
-- > hsManifest = manifestEntryPoint manifest increment
-- > hsProcess :: ProcessFunction
-- > hsProcess = processEntryPoint manifest increment
-- > hsFactory :: ProcessFactoryFunction
-- > hsFactory = pure p_hsProcess
-- >
-- > foreign export ccall "manifest_entry_point" hsManifest :: ManifestFunction
-- > foreign export ccall "increment_process_entry_point" hsProcess :: ProcessFunction
-- > foreign import ccall "&increment_process_entry_point" p_hsProcess :: FunPtr ProcessFunction
-- > foreign export ccall "process_factory_entry_point" hsFactory :: ProcessFactoryFunction
module Juiz.Process
  ( ProcessFn (..)
  , manifestEntryPoint
  , processEntryPoint
  , checkedManifestEntryPoint
  , checkProcessManifest
  , checkArgumentTypes
  , resultType
  , readArguments
  , applyAndWrite
  , guardEntryPoint
  , tshow
  ) where

import Control.Exception (SomeException, catch, displayException, evaluate, fromException, try)
import Data.Int (Int64)
import Data.Proxy (Proxy (..))
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.IO as TIO
import Foreign.Ptr (Ptr)
import System.IO (stderr)

import Juiz.Capsule
import Juiz.Error
import Juiz.Internal.FFI
import Juiz.Internal.TypeInfo
import Juiz.Manifest
import Juiz.Value

-- | Process にできる関数: 引数が 'FromCapsule'、結果が @IO r@ の関数。
-- 結果が IO でない関数は 'Juiz.TH.juizProcess' が @pure@ で包む。
-- Process では @r@ を 'ToCapsule' に、Container を作る関数では @r@ を Container の状態にする
class ProcessFn f where
  -- | 結果の型 (@a -> b -> IO r@ なら @r@)
  type FnResult f
  -- | 引数の数
  processArity :: Proxy f -> Int
  -- | 引数 (名前と値の組) を順に渡して呼ぶ
  applyArgs :: f -> [(Text, Argument)] -> IO (FnResult f)
  -- | 引数ごとの型 (型付きの型なら 'Just'、'Juiz.Capsule.fromCapsuleType')
  argumentTypes :: Proxy f -> [Maybe TypeInfo]

instance (FromCapsule a, ProcessFn b) => ProcessFn (a -> b) where
  type FnResult (a -> b) = FnResult b
  processArity _ = 1 + processArity (Proxy :: Proxy b)
  argumentTypes _ = fromCapsuleType (Proxy :: Proxy a) : argumentTypes (Proxy :: Proxy b)
  applyArgs f ((name, v) : rest) = case fromArgument v of
    Right a -> applyArgs (f a) rest
    Left msg -> throwJuiz juizValueConverterError ("argument \"" <> name <> "\": " <> msg)
  applyArgs _ [] = throwJuiz juizValueNotFoundError "too few arguments"

instance ProcessFn (IO r) where
  type FnResult (IO r) = r
  processArity _ = 0
  argumentTypes _ = []
  applyArgs act [] = act
  applyArgs _ _ = throwJuiz juizValueConverterError "too many arguments"

-- | manifest_entry_point の中身。マニフェストの引数の数と関数の引数の数が違う、
-- または型付きの引数・戻り値の型が関数と合わなければエラーにする (juiz はプラグインを読み込むときに失敗する)
manifestEntryPoint :: forall f. (ProcessFn f, ToCapsule (FnResult f)) => ProcessManifest -> f -> ManifestFunction
manifestEntryPoint m _ cp = guardEntryPoint (pmTypeName m) $
  case checkProcessManifest m (Proxy :: Proxy f) of
    Left msg -> throwJuiz juizValueConverterError msg
    Right m' -> checkedManifestEntryPoint (pmTypeName m') (length (pmArguments m')) (processArity (Proxy :: Proxy f))
      (manifestToValue m') cp

-- | 関数の結果の型 (型付きの型なら 'Just')
resultType :: forall f. (ProcessFn f, ToCapsule (FnResult f)) => Proxy f -> Maybe TypeInfo
resultType _ = toCapsuleType (Proxy :: Proxy (FnResult f))

-- | Process / ContainerProcess のマニフェストの型を関数の型と比べ、戻り値の型を補う (docs/spec/datatype.md §22)。
--
-- * 型付きの型の引数は、マニフェストでも同じ型の型付きの引数 ('addTypedArg' など) にする
-- * 関数の結果が型付きの型なら output_schema を載せる ('returns' を書いたときは同じ型でなければならない)
checkProcessManifest :: forall f. (ProcessFn f, ToCapsule (FnResult f)) => ProcessManifest -> Proxy f -> Either Text ProcessManifest
checkProcessManifest m p = do
  checkArgumentTypes (pmArguments m) (argumentTypes p)
  case (resultType p, pmOutputType m) of
    (Just r, Nothing) -> Right m { pmOutputType = Just r }
    (Just r, Just o)
      | r == o -> Right m
      | otherwise -> Left $ "the manifest returns " <> typeInfoName o <> ", but the function returns " <> typeInfoName r
    (Nothing, _) -> Right m

-- | マニフェストの引数と関数の引数の型を比べる。引数の数は比べない ('checkedManifestEntryPoint' が比べる)
checkArgumentTypes :: [ArgumentManifest] -> [Maybe TypeInfo] -> Either Text ()
checkArgumentTypes args types = mapM_ check (zip args types)
  where
    check (a, Just t) = case argType a of
      Just at
        | at == t -> Right ()
        | otherwise -> Left $ "argument \"" <> argName a <> "\": the manifest declares " <> typeInfoName at
            <> ", but the function takes " <> typeInfoName t
      Nothing -> Left $ "argument \"" <> argName a <> "\": the function takes " <> typeInfoName t
        <> "; declare it with addTypedArg (or addTypedArgNoDefault)"
    -- 型なしの型 (Value など) は型付きの引数も受け取れる
    check (_, Nothing) = Right ()

-- | 引数の数を確かめてからマニフェストを書き込む manifest_entry_point
checkedManifestEntryPoint :: Text -> Int -> Int -> Value -> ManifestFunction
checkedManifestEntryPoint name nArgs arity v cp = guardEntryPoint name $
  if nArgs /= arity
    then throwJuiz juizValueConverterError $
      "the manifest has " <> tshow nArgs <> " arguments, but the function takes " <> tshow arity
    else case v of
      JObject obj -> writeCapsulePtr cp obj
      _ -> pure juizValueConverterError

-- | Process の関数の中身。マニフェストの引数を順に読み出して関数を呼び、結果を capsule に書き込む
processEntryPoint :: (ProcessFn f, ToCapsule (FnResult f)) => ProcessManifest -> f -> ProcessFunction
processEntryPoint m f cm out = guardEntryPoint (pmTypeName m) $ applyAndWrite (pmArguments m) f cm out

-- | マニフェストの引数を順に読み出す
readArguments :: [ArgumentManifest] -> Ptr CCapsuleMap -> IO [(Text, Argument)]
readArguments as cm = mapM read1 as
  where
    -- 型付きの引数は型で読む (足りないフィールドは既定値、型に合わなければ juizValueDoesNotMatchTypeError)。
    -- 型なしの引数は、画像ならそのまま画像として読む (docs/spec/large-data.md §10)
    read1 a = (,) (argName a) <$> case argType a of
      Just t -> ArgValue <$> readTypedArgument cm (argName a) t
      Nothing -> readArgumentOrImage cm (argName a)

-- | 引数を読み出して関数を呼び、結果を capsule に書き込む
applyAndWrite :: (ProcessFn f, ToCapsule (FnResult f)) => [ArgumentManifest] -> f -> Ptr CCapsuleMap -> Ptr CCapsule -> IO Int64
applyAndWrite as f cm out = do
  args <- readArguments as cm
  r <- applyArgs f args
  result <- evaluate (toResult r)
  writeResult out result

-- | Haskell の例外を C の呼び出し元に出さず、juiz のエラーコードに変える。
-- 'JuizError' はそのコード、それ以外の例外は 'juizProcessFunctionError'。内容は標準エラー出力に書く
guardEntryPoint :: Text -> IO Int64 -> IO Int64
guardEntryPoint name act = do
  r <- try (act >>= evaluate)
  case r of
    Right code -> pure code
    Left (e :: SomeException) -> do
      let (code, msg) = case fromException e of
            Just (JuizError c m) -> (c, m)
            Nothing -> (juizProcessFunctionError, T.pack (displayException e))
      TIO.hPutStrLn stderr ("hsjuiz: " <> name <> ": " <> msg <> " (error " <> tshow code <> ")")
        `catch` \(_ :: SomeException) -> pure ()
      pure code

tshow :: Show a => a -> Text
tshow = T.pack . show