capnp-0.18.0.0: lib/Capnp/Rpc/Membrane.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RecordWildCards #-}
-- | Module: Capnp.Rpc.Membrane
-- Descritpion: Helpers for working with membranes.
--
-- Membranes are common in object-capability design. Think of it like a
-- proxy on steroids: a membrane inserts itself in front of another capability,
-- and can intercept and modify method calls. Unlike a simple proxy though,
-- the membrane will also be applied to any objects returned by method calls,
-- or passed in arguments, transitively, so it can sit in front of entire
-- object graphs.
module Capnp.Rpc.Membrane
( enclose,
exclude,
Policy,
Action (..),
Direction (..),
Call (..),
)
where
import qualified Capnp.Message as M
import Capnp.Mutability (Mutability (..))
import qualified Capnp.Repr as R
import Capnp.Rpc.Promise (breakOrFulfill, newCallback)
import qualified Capnp.Rpc.Server as Server
import qualified Capnp.Rpc.Untyped as URpc
import qualified Capnp.Untyped as U
import Control.Concurrent.STM
import Control.Monad (void)
import Control.Monad.STM.Class
import Data.Functor.Contravariant (contramap)
import Data.Typeable (Typeable, cast)
import Data.Word
import Supervisors (Supervisor)
-- | An action indicates what to do with an incoming method call.
data Action
= -- | Handle the method using the provided method handler, instead of
-- letting it through the membrane. Arguments and return values will not
-- be wrapped/unwraped, so be careful when delegating to objects inside
-- the membrane.
Handle Server.UntypedMethodHandler
| -- | Forward the method call on to its original destination, wrapping
-- and unwrapping arguments & return values as normal.
Forward
-- | A Direction indicates which direction a method call is traveling:
-- into or out of the membrane.
data Direction = In | Out
deriving (Show, Read, Eq)
flipDir :: Direction -> Direction
flipDir In = Out
flipDir Out = In
-- | Alias for direction; somtimes it is convienent to think about capabilities
-- from the standpoint of which side they is _on_, rather than where it is going
-- as with methods.
type Side = Direction
-- | A 'Call' represents a method call that is crossing the membrane.
data Call = Call
{ -- | Which direction is the call going? if this is 'In', the call was made
-- by something outside the membrane to something inside it. If it is 'Out',
-- something inside the membrane is making a call to something outside the
-- membrane.
direction :: Direction,
-- | The interface id of the method being called.
interfaceId :: Word64,
-- | The ordinal of the method being called.
methodId :: Word16,
-- | The target of the method call.
target :: URpc.Client
}
-- | A 'Policy' decides what to do when a call crosses the membrane.
type Policy = Call -> STM Action
-- | @'enclose' sup cap policy@ wraps @cap@ in a membrane whose behavior is
-- goverend by @policy@.
enclose :: (URpc.IsClient c, MonadSTM m) => Supervisor -> c -> Policy -> m c
enclose = newMembrane In
-- | 'exclude' is like 'enclose', except that the capability is treated as
-- being *outside* of a membrane that wraps the rest of the world.
exclude :: (URpc.IsClient c, MonadSTM m) => Supervisor -> c -> Policy -> m c
exclude = newMembrane Out
newMembrane :: (URpc.IsClient c, MonadSTM m) => Direction -> Supervisor -> c -> Policy -> m c
newMembrane dir sup toWrap policy = liftSTM $ do
identity <- newTVar ()
let mem = Membrane {policy, identity}
URpc.fromClient <$> pass dir sup mem (URpc.toClient toWrap)
data MembraneWrapped = MembraneWrapped
{ client :: URpc.Client,
membrane :: Membrane,
side :: Direction
}
deriving (Typeable)
data Membrane = Membrane
{ policy :: Policy,
-- | an object with identity, for comparison purposes:
identity :: TVar ()
}
instance Eq Membrane where
x == y = identity x == identity y
wrapHandler :: Side -> Supervisor -> Membrane -> Server.UntypedMethodHandler -> Server.UntypedMethodHandler
wrapHandler receiverSide sup mem handler = \(R.Raw arguments) response -> do
(args, resp) <- atomically $ do
args' <- passPtr receiverSide sup mem arguments
resp' <- newCallback $ \result ->
traverse (passPtr (flipDir receiverSide) sup mem) result
>>= breakOrFulfill (contramap R.Raw response)
pure (args', resp')
handler (R.Raw args) (contramap R.fromRaw resp)
passPtr :: MonadSTM m => Direction -> Supervisor -> Membrane -> Maybe (U.Ptr 'Const) -> m (Maybe (U.Ptr 'Const))
passPtr dir sup mem = liftSTM . traverse (U.tMsg $ passMessage dir sup mem)
passMessage :: MonadSTM m => Direction -> Supervisor -> Membrane -> M.Message 'Const -> m (M.Message 'Const)
passMessage dir sup mem msg = liftSTM $ do
caps' <- traverse (pass dir sup mem) (M.getCapTable msg)
pure $ M.withCapTable caps' msg
pass :: MonadSTM m => Direction -> Supervisor -> Membrane -> URpc.Client -> m URpc.Client
pass dir sup mem inClient = liftSTM $
case URpc.unwrapServer inClient :: Maybe MembraneWrapped of
Just mw | onSide dir mw mem -> pure $ client mw
_ ->
URpc.export
sup
Server.ServerOps
{ Server.handleCast =
cast $
MembraneWrapped
{ client = inClient,
membrane = mem,
side = dir
},
-- Once we're gc'd, the downstream client will be as well
-- and then the relevant shutdown logic will still be called.
-- This introduces latency unfortuantely, but nothing is broken.
Server.handleStop = pure (),
Server.handleCall = \interfaceId methodId arguments response ->
do
action <- atomically $ policy mem Call {interfaceId, methodId, direction = dir, target = inClient}
case action of
Handle h -> h arguments response
Forward ->
( wrapHandler dir sup mem $ \(R.Raw arguments) resp ->
let response = contramap R.Raw resp
in void $ URpc.call Server.CallInfo {..} inClient
)
arguments
response
}
onSide :: Direction -> MembraneWrapped -> Membrane -> Bool
onSide dir mw mem =
membrane mw == mem && dir == side mw