capnp-0.17.0.0: lib/Capnp/Basics.hs
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE EmptyDataDeriving #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
-- |
-- Module: Capnp.Basics
-- Description: Handling of "basic" capnp datatypes.
--
-- This module contains phantom types for built-in Cap'n Proto
-- types, analogous to the phantom types generated for structs
-- by the code generator. It also defines applicable type class
-- instances.
module Capnp.Basics where
-- XXX: I(zenhack) don't know how to supply an explicit
-- export list here, since we have instances of data families
-- and I don't know what to call the instances to get all of the
-- constructors.
import qualified Capnp.Classes as C
import qualified Capnp.Errors as E
import qualified Capnp.Message as M
import qualified Capnp.Repr as R
import qualified Capnp.Untyped as U
import Control.Monad (when)
import Control.Monad.Catch (MonadThrow, throwM)
import qualified Data.ByteString as BS
import Data.Default (Default (..))
import Data.Foldable (foldl', for_)
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import qualified Data.Vector as V
import Data.Word
import GHC.Generics (Generic)
import GHC.Prim (coerce)
-- | The Cap'n Proto @Text@ type.
data Text
-- | The Cap'n Proto @Data@ type.
data Data
-- | A Cap'n Proto @AnyPointer@, i.e. an arbitrary pointer with unknown schema.
data AnyPointer
-- | A Cap'n Proto @List@ with unknown element type.
data AnyList
-- | A Cap'n Proto struct of unknown type.
data AnyStruct
-- | A Cap'n Proto capability with unknown interfaces.
data Capability
type instance R.ReprFor Data = R.ReprFor (R.List Word8)
type instance R.ReprFor Text = R.ReprFor (R.List Word8)
type instance R.ReprFor AnyPointer = 'R.Ptr 'Nothing
type instance R.ReprFor (Maybe AnyPointer) = 'R.Ptr 'Nothing
type instance R.ReprFor AnyList = 'R.Ptr ('Just ('R.List 'Nothing))
type instance R.ReprFor AnyStruct = 'R.Ptr ('Just 'R.Struct)
type instance R.ReprFor Capability = 'R.Ptr ('Just 'R.Cap)
data instance C.Parsed AnyPointer
= PtrStruct (C.Parsed AnyStruct)
| PtrList (C.Parsed AnyList)
| PtrCap M.Client
deriving (Show, Eq, Generic)
instance C.Parse (Maybe AnyPointer) (Maybe (C.Parsed AnyPointer)) where
parse (R.Raw ptr) = case ptr of
Nothing -> pure Nothing
Just _ -> Just <$> C.parse (R.Raw ptr :: R.Raw AnyPointer 'M.Const)
encode msg value =
R.Raw <$> case value of
Nothing -> pure Nothing
Just v -> coerce <$> C.encode msg v
instance C.Parse AnyPointer (C.Parsed AnyPointer) where
parse (R.Raw ptr) = case ptr of
Just (U.PtrCap cap) -> PtrCap <$> C.parse (R.Raw cap)
Just (U.PtrList list) -> PtrList <$> C.parse (R.Raw list)
Just (U.PtrStruct struct) -> PtrStruct <$> C.parse (R.Raw struct)
Nothing ->
throwM $ E.SchemaViolationError "Non-nullable AnyPointer was null"
encode msg value =
R.Raw <$> case value of
PtrCap cap -> Just . U.PtrCap . R.fromRaw <$> C.encode msg cap
PtrList list -> Just . U.PtrList . R.fromRaw <$> C.encode msg list
PtrStruct struct -> Just . U.PtrStruct . R.fromRaw <$> C.encode msg struct
instance C.AllocateList AnyPointer where
type ListAllocHint AnyPointer = Int
instance C.EstimateListAlloc AnyPointer (C.Parsed AnyPointer)
instance C.AllocateList (Maybe AnyPointer) where
type ListAllocHint (Maybe AnyPointer) = Int
instance C.EstimateListAlloc (Maybe AnyPointer) (Maybe (C.Parsed AnyPointer))
data instance C.Parsed AnyStruct = Struct
{ structData :: V.Vector Word64,
structPtrs :: V.Vector (Maybe (C.Parsed AnyPointer))
}
deriving (Show, Generic)
instance Eq (C.Parsed AnyStruct) where
-- We define equality specially (rather than just deriving), such that
-- slices are padded out with the default values of their elements.
(Struct dl pl) == (Struct dr pr) = sectionEq dl dr && sectionEq pl pr
where
sectionEq :: (Eq a, Default a) => V.Vector a -> V.Vector a -> Bool
sectionEq l r = go 0
where
go i
| i >= length = True
| otherwise = indexDef i l == indexDef i r && go (i + 1)
length = max (V.length l) (V.length r)
indexDef i vec
| i < V.length vec = vec V.! i
| otherwise = def
instance C.Parse AnyStruct (C.Parsed AnyStruct) where
parse (R.Raw s) =
Struct
<$> V.generateM
(fromIntegral $ U.structWordCount s)
(`U.getData` s)
<*> V.generateM
(fromIntegral $ U.structPtrCount s)
(\i -> U.getPtr i s >>= C.parse . R.Raw)
instance C.AllocateList AnyStruct where
type ListAllocHint AnyStruct = (Int, R.AllocHint 'R.Struct)
instance C.EstimateListAlloc AnyStruct (C.Parsed AnyStruct) where
estimateListAlloc structs =
let len = V.length structs
!nWords = foldl' max 0 $ map (V.length . structData) $ V.toList structs
!nPtrs = foldl' max 0 $ map (V.length . structPtrs) $ V.toList structs
in (len, (fromIntegral nWords, fromIntegral nPtrs))
instance C.EstimateAlloc AnyStruct (C.Parsed AnyStruct) where
estimateAlloc s =
( fromIntegral $ V.length $ structData s,
fromIntegral $ V.length $ structPtrs s
)
instance C.Marshal AnyStruct (C.Parsed AnyStruct) where
marshalInto (R.Raw raw) s = do
V.iforM_ (structData s) $ \i value -> do
U.setData value i raw
V.iforM_ (structPtrs s) $ \i value -> do
R.Raw ptr <- C.encode (U.message @U.Struct raw) value
U.setPtr ptr i raw
-- TODO(cleanup): It would be nice if we could reuse Capnp.Repr.Parsed.Parsed
-- here, but that would cause a circular import dependency.
type ParsedList a = V.Vector a
data instance C.Parsed AnyList
= ListPtr (ParsedList (Maybe (C.Parsed AnyPointer)))
| ListStruct (ParsedList (C.Parsed AnyStruct))
| List0 (ParsedList ())
| List1 (ParsedList Bool)
| List8 (ParsedList Word8)
| List16 (ParsedList Word16)
| List32 (ParsedList Word32)
| List64 (ParsedList Word64)
deriving (Show, Eq, Generic)
instance C.Parse AnyList (C.Parsed AnyList) where
parse (R.Raw list) = case list of
U.List0 l -> List0 <$> C.parse (R.Raw l)
U.List1 l -> List1 <$> C.parse (R.Raw l)
U.List8 l -> List8 <$> C.parse (R.Raw l)
U.List16 l -> List16 <$> C.parse (R.Raw l)
U.List32 l -> List32 <$> C.parse (R.Raw l)
U.List64 l -> List64 <$> C.parse (R.Raw l)
U.ListPtr l -> ListPtr <$> C.parse (R.Raw l)
U.ListStruct l -> ListStruct <$> C.parse (R.Raw l)
encode msg list =
R.Raw <$> case list of
List0 l -> U.List0 . R.fromRaw <$> C.encode msg l
List1 l -> U.List1 . R.fromRaw <$> C.encode msg l
List8 l -> U.List8 . R.fromRaw <$> C.encode msg l
List16 l -> U.List16 . R.fromRaw <$> C.encode msg l
List32 l -> U.List32 . R.fromRaw <$> C.encode msg l
List64 l -> U.List64 . R.fromRaw <$> C.encode msg l
ListPtr l -> U.ListPtr . R.fromRaw <$> C.encode msg l
ListStruct l -> U.ListStruct . R.fromRaw <$> C.encode msg l
instance C.Parse Capability M.Client where
parse (R.Raw cap) = U.getClient cap
encode msg client = R.Raw <$> U.appendCap msg client
instance C.Allocate Text where
type AllocHint Text = Int
new len msg = R.Raw <$> U.allocList8 msg (len + 1)
instance C.AllocateList Text where
type ListAllocHint Text = Int
instance C.EstimateListAlloc Text T.Text
instance C.Parse Text T.Text where
parse (R.Raw list) =
let len = U.length list
in if len == 0
then -- We are somewhat lenient here; technically this is invalid, as there is
-- no null terminator (see logic below, which is dead code because of
-- this check. But to avoid this we really need to expose nullability
-- in the API, so for now we just fudge it.
pure ""
else
( do
when (len == 0) $
throwM $
E.SchemaViolationError
"Text is not NUL-terminated (list of bytes has length 0)"
lastByte <- U.index (len - 1) list
when (lastByte /= 0) $
throwM $
E.SchemaViolationError $
"Text is not NUL-terminated (last byte is " ++ show lastByte ++ ")"
bytes <- BS.take (len - 1) <$> U.rawBytes list
case TE.decodeUtf8' bytes of
Left e -> throwM $ E.InvalidUtf8Error e
Right v -> pure v
)
encode msg value = do
let bytes = TE.encodeUtf8 value
raw@(R.Raw untyped) <- C.new @Text (BS.length bytes) msg
C.marshalInto @Data (R.Raw untyped) bytes
pure raw
-- Instances for Data
instance C.Parse Data BS.ByteString where
parse = U.rawBytes . R.fromRaw
instance C.Allocate Data where
type AllocHint Data = Int
new len msg = R.Raw <$> U.allocList8 msg len
instance C.EstimateAlloc Data BS.ByteString where
estimateAlloc = BS.length
instance C.AllocateList Data where
type ListAllocHint Data = Int
instance C.EstimateListAlloc Data BS.ByteString
instance C.Marshal Data BS.ByteString where
marshalInto (R.Raw list) bytes =
for_ [0 .. BS.length bytes - 1] $ \i ->
U.setIndex (BS.index bytes i) i list
-- Instances for AnyStruct
instance C.Allocate AnyStruct where
type AllocHint AnyStruct = (Word16, Word16)
new (nWords, nPtrs) msg = R.Raw <$> U.allocStruct msg nWords nPtrs
-- | Return the underlying buffer containing the text. This does not include the
-- null terminator.
textBuffer :: MonadThrow m => R.Raw Text mut -> m (R.Raw Data mut)
textBuffer (R.Raw list) = R.Raw <$> U.take (U.length list - 1) list
-- | Convert a 'Text' to a 'BS.ByteString', comprising the raw bytes of the text
-- (not counting the NUL terminator).
textBytes :: U.ReadCtx m 'M.Const => R.Raw Text 'M.Const -> m BS.ByteString
textBytes text = do
R.Raw raw <- textBuffer text
U.rawBytes raw