mirror of
https://github.com/zap-proto/haskell.git
synced 2026-10-30 15:05:00 +00:00
266 lines
9.0 KiB
Haskell
266 lines
9.0 KiB
Haskell
{-# LANGUAGE DataKinds #-}
|
|
{-# LANGUAGE DuplicateRecordFields #-}
|
|
{-# LANGUAGE FlexibleContexts #-}
|
|
{-# LANGUAGE LambdaCase #-}
|
|
{-# LANGUAGE NamedFieldPuns #-}
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
{-# LANGUAGE RankNTypes #-}
|
|
{-# LANGUAGE ScopedTypeVariables #-}
|
|
{-# LANGUAGE TypeApplications #-}
|
|
{-# LANGUAGE TypeFamilies #-}
|
|
|
|
module Zap.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 Zap.Basics as B
|
|
import qualified Zap.Classes as C
|
|
import Zap.Convert (parsedToRaw)
|
|
import Zap.Message (Mutability (..))
|
|
import qualified Zap.Repr as R
|
|
-- import Zap.Repr.Methods (Client (..))
|
|
import Zap.Rpc.Errors
|
|
( eFailed,
|
|
eMethodUnimplemented,
|
|
wrapException,
|
|
)
|
|
import Zap.Rpc.Promise
|
|
( Fulfiller,
|
|
breakPromise,
|
|
fulfill,
|
|
)
|
|
import Zap.TraversalLimit (defaultLimit, evalLimitT)
|
|
import qualified Zap.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 zap 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
|