capnp-0.18.0.0: lib/Capnp/Rpc/Server.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
module Capnp.Rpc.Server
( CallHandler,
MethodHandler,
UntypedMethodHandler,
CallInfo (..),
ServerOps (..),
Export (..),
exportToServerOps,
findMethod,
SomeServer (..),
runServer,
castHandler,
-- * Helpers for writing method handlers
handleParsed,
handleRaw,
methodUnimplemented,
toUntypedMethodHandler,
-- * Internals; exposed only for use by generated code.
MethodHandlerTree (..),
)
where
import qualified Capnp.Basics as B
import qualified Capnp.Classes as C
import Capnp.Convert (parsedToRaw)
import Capnp.Message (Mutability (..))
import qualified Capnp.Repr as R
-- import Capnp.Repr.Methods (Client (..))
import Capnp.Rpc.Errors
( eFailed,
eMethodUnimplemented,
wrapException,
)
import Capnp.Rpc.Promise
( Fulfiller,
breakPromise,
fulfill,
)
import Capnp.TraversalLimit (defaultLimit, evalLimitT)
import qualified Capnp.Untyped as U
import Control.Concurrent.STM (atomically)
import Control.Exception.Safe (withException)
import Data.Function ((&))
import Data.Functor.Contravariant (contramap)
import Data.Kind (Constraint, Type)
import qualified Data.Map.Strict as M
import Data.Maybe (fromMaybe)
import Data.Proxy (Proxy (..))
import Data.Typeable (Typeable)
import qualified Data.Vector as V
import Data.Word
import GHC.Prim (coerce)
import Internal.BuildPure (createPure)
import qualified Internal.TCloseQ as TCloseQ
-- | A 'CallInfo' contains information about a method call.
data CallInfo = CallInfo
{ -- | The id of the interface whose method is being called.
interfaceId :: !Word64,
-- | The method id of the method being called.
methodId :: !Word16,
-- | The arguments to the method call.
arguments :: Maybe (U.Ptr 'Const),
-- | A 'Fulfiller' which accepts the method's return value.
response :: Fulfiller (Maybe (U.Ptr 'Const))
}
-- | The operations necessary to receive and handle method calls, i.e.
-- to implement an object.
data ServerOps = ServerOps
{ -- | Handle a method call; takes the interface and method id and returns
-- a handler for the specific method.
handleCall :: Word64 -> Word16 -> UntypedMethodHandler,
-- | Handle shutting-down the receiver; this is called when the last
-- reference to the capability is dropped.
handleStop :: IO (),
-- | used to unwrap the server when reflecting on a local client.
handleCast :: forall a. Typeable a => Maybe a
}
-- | A handler for arbitrary RPC calls. Maps (interfaceId, methodId) pairs to
-- 'UntypedMethodHandler's.
type CallHandler = M.Map Word64 (V.Vector UntypedMethodHandler)
-- | Type alias for a handler for a particular rpc method.
type MethodHandler p r =
R.Raw p 'Const ->
Fulfiller (R.Raw r 'Const) ->
IO ()
castHandler ::
forall p q r s.
(R.ReprFor p ~ R.ReprFor q, R.ReprFor r ~ R.ReprFor s) =>
MethodHandler p r ->
MethodHandler q s
castHandler = coerce
-- | Type alias for a handler for an untyped RPC method.
type UntypedMethodHandler = MethodHandler (Maybe B.AnyPointer) (Maybe B.AnyPointer)
-- | Base class for things that can act as capnproto servers.
class SomeServer a where
-- | Called when the last live reference to a server is dropped.
shutdown :: a -> IO ()
shutdown _ = pure ()
-- | Try to extract a value of a given type. The default implementation
-- always fails (returns 'Nothing'). If an instance chooses to implement
-- this, it will be possible to use "reflection" on clients that point
-- at local servers to dynamically unwrap the server value. A typical
-- implementation will just call Typeable's @cast@ method, but this
-- needn't be the case -- a server may wish to allow local peers to
-- unwrap some value that is not exactly the data the server has access
-- to.
unwrap :: Typeable b => a -> Maybe b
unwrap _ = Nothing
-- | Generated interface types have instances of 'Export', which allows a server
-- for that interface to be exported as a 'Client'.
class (R.IsCap i, C.HasTypeId i) => Export i where
-- | The constraint needed for a server to implement an interface;
-- if @'Server' i s@ is satisfied, @s@ is a server for interface @i@.
-- The code generator generates a type class for each interface, and
-- this will aways be an alias for that type class.
type Server i :: Type -> Constraint
-- | Convert the server to a 'MethodHandlerTree' populated with appropriate
-- 'MethodHandler's for the interface. This is really only exported for use
-- by generated code; users of the library will generally prefer to use
-- 'export'.
methodHandlerTree :: Server i s => Proxy i -> s -> MethodHandlerTree
-- NB: the proxy helps disambiguate types; for some reason TypeApplications
-- doesn't seem to be enough in the face of a type alias of kind 'Constraint'.
-- the inconsistency is a bit ugly, but this method isn't intended to called
-- by users directly, only by generated code and our helper in this module,
-- so it's less of a big deal.
-- | Lazily computed tree of the method handlers exposed by an interface. Only
-- of interest to generated code.
data MethodHandlerTree = MethodHandlerTree
{ -- | type id for the primary interface
mhtId :: Word64,
-- | method handlers for methods of the primary interface.
mhtHandlers :: [UntypedMethodHandler],
-- | Trees for parent interfaces. In the case of diamond dependencies,
-- there may be duplicates, which are eliminated by 'mhtToCallHandler'.
mhtParents :: [MethodHandlerTree]
}
mhtToCallHandler :: MethodHandlerTree -> CallHandler
mhtToCallHandler = go M.empty . pure
where
go accum [] = accum
go accum (t : ts)
| mhtId t `M.member` accum = go accum ts -- dedup diamond dependencies
| otherwise =
go (M.insert (mhtId t) (V.fromList (mhtHandlers t)) accum) (mhtParents t ++ ts)
-- | Look up a particlar 'MethodHandler' in the 'CallHandler'.
findMethod :: Word64 -> Word16 -> CallHandler -> Maybe UntypedMethodHandler
findMethod interfaceId methodId handler = do
iface <- M.lookup interfaceId handler
iface V.!? fromIntegral methodId
-- | Convert a typed method handler to an untyped one. Mostly intended for
-- use by generated code.
toUntypedMethodHandler ::
forall p r.
(R.IsStruct p, R.IsStruct r) =>
MethodHandler p r ->
UntypedMethodHandler
toUntypedMethodHandler h =
\case
R.Raw (Just (U.PtrStruct param)) -> \ret ->
h
(R.Raw param)
( contramap
(\(R.Raw s) -> R.Raw $ Just $ U.PtrStruct s)
ret
)
_ ->
\ret -> breakPromise ret (eFailed "Parameter was not a struct")
someServerToServerOps :: SomeServer a => a -> CallHandler -> ServerOps
someServerToServerOps srv callHandler =
ServerOps
{ handleStop = shutdown srv,
handleCast = unwrap srv,
handleCall = \interfaceId methodId ->
findMethod interfaceId methodId callHandler
& fromMaybe methodUnimplemented
}
exportToServerOps :: forall i s. (Export i, Server i s, SomeServer s) => Proxy i -> s -> ServerOps
exportToServerOps proxy srv =
mhtToCallHandler (methodHandlerTree proxy srv)
& someServerToServerOps srv
-- Helpers for writing method handlers
-- | Handle a method, working with the parsed form of parameters and
-- results.
handleParsed ::
( C.Parse p pp,
R.IsStruct p,
C.Parse r pr,
R.IsStruct r
) =>
(pp -> IO pr) ->
MethodHandler p r
handleParsed handler param = propagateExceptions $ \f -> do
p <- evalLimitT defaultLimit $ C.parse param
r <- handler p
-- TODO: Figure out how to add an instance of Thaw for
-- Raw so we can skip the (un)wrapping here.
struct <- createPure maxBound $ R.fromRaw <$> parsedToRaw r
fulfill f (R.Raw struct)
-- | Handle a method, working with the raw (unparsed) form of
-- parameters and results.
handleRaw ::
(R.IsStruct p, R.IsStruct r) =>
(R.Raw p 'Const -> IO (R.Raw r 'Const)) ->
MethodHandler p r
handleRaw handler param = propagateExceptions $ \f ->
handler param >>= fulfill f
-- Helper for handle*; breaks the promise if the handler throws.
propagateExceptions :: (Fulfiller a -> IO b) -> Fulfiller a -> IO b
propagateExceptions h f =
h f `withException` (breakPromise f . wrapException False)
-- | 'MethodHandler' that always throws unimplemented.
methodUnimplemented :: MethodHandler p r
methodUnimplemented _ f = breakPromise f eMethodUnimplemented
-- | Handle incoming messages for a given object.
--
-- Accepts a queue of messages to handle, and 'ServerOps' used to handle them.
-- returns when it receives a 'Stop' message.
runServer :: TCloseQ.Q CallInfo -> ServerOps -> IO ()
runServer q ops = go
where
go =
atomically (TCloseQ.read q) >>= \case
Nothing ->
pure ()
Just CallInfo {interfaceId, methodId, arguments, response} ->
do
handleCall ops interfaceId methodId (R.Raw arguments) (coerce response)
go