{-# LANGUAGE Rank2Types #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE FlexibleContexts #-} {- | Library for converting types to and from binary, so that they can be written to and from files, stored compactly in memory, and so on. This is a preliminary version of the library, hence I have decided /not/ to optimise heavily, beyond putting in strictness annotations in where they seem appropriate. A good place to start optimising would probably be the separate "Bytes" libary. See also "BinaryInstances", which declares instances for the standard types (and one or two others), "BinaryUtils", which contains (mostly) material for declaring new instances, "BinaryExtras", which contains other miscellaneous utilities, and finally "BinaryAll" which just imports and reexports everything. -} module Util.Binary ( hWrite, -- :: HasBinary a IO => Handle -> a -> IO () hRead, -- :: HasBinary a IO => Handle -> IO a writeToBytes, -- :: HasBinary a StateBinArea => a -> IO (Bytes,Int) writeToBytes0, -- :: HasBinary a StateBinArea => Int -> a -> IO (Bytes,Int) readFromBytes, -- :: HasBinary a StateBinArea => (Bytes,Int) -> IO a HasBinary (..), WriteBinary (..), ReadBinary (..), {- Ways of constructing WriteBinary/ReadBinary instances (not usually required explicitly). -} toWriteBinaryHandle, -- :: Handle -> WriteBinary IO toReadBinaryHandle, -- :: Handle -> ReadBinary IO -- Functions required for writing directly to binary areas. BinArea, StateBinArea, -- = StateT BinArea IO -- writing a BinArea -- create mkEmptyBinArea, {- :: Int -> IO BinArea pass as argument to writeBin -} writeBinaryBinArea, {- :: WriteBinary StateBinArea close and get contents. -} closeBinArea, -- :: BinArea -> IO (Bytes,Int) -- reading a BinArea -- create mkBinArea, {- :: (Bytes,Int) -> BinArea pass to things which read. -} readBinaryBinArea, {- :: ReadBinary StateBinArea check that the BinArea is completely read. -} checkFullBinArea, -- :: BinArea -> IO () -- Functions for transforming WriteBinary/ReadBinary values. liftWriteBinary, -- :: (forall a . m a -> n a) -> WriteBinary m -> WriteBinary n liftReadBinary, -- :: (forall a . m a -> n a) -> ReadBinary m -> ReadBinary n ) where -- Standard imports import System.IO -- GHC imports import Control.Monad.State -- Our imports import Util.Bytes {- ---------------------------------------------------------------------- The general framework Type variable "m" is a monad; "a" is the thing to read or write. NB. Bytes values are currently not subject to the garbage-collector, and so need to be explicitly freed. The following rules for this should be observed. (1) For writeBytes, it is only guaranteed that the argument "Bytes" will be valid at the actual time of evaluation. (2) For readBytes, it is the caller's responsibility to free the returned area. ---------------------------------------------------------------------- -} -- | A consumer of binary data data WriteBinary m = WriteBinary { writeByte :: Byte -> m (), -- ^ write one byte writeBytes :: Bytes -> Int -> m () -- ^ write multiple bytes } -- | A source of binary data data ReadBinary m = ReadBinary { readByte :: m Byte, -- ^ read one byte readBytes :: Int -> m Bytes -- ^ read multiple bytes } class HasBinary a m where writeBin :: WriteBinary m -> a -> m () -- ^ Given a consumer of binary data, and an (a), write out the (a) readBin :: ReadBinary m -> m a -- ^ Given a source of binary data, provide an (a) {- ---------------------------------------------------------------------- Reading/Writing HasBinary instances to Handles. ---------------------------------------------------------------------- -} -- | Write an (a) to a 'Handle' hWrite :: HasBinary a IO => Handle -> a -> IO () hWrite handle = writeBin $ toWriteBinaryHandle handle -- | Read an (a) from a 'Handle' hRead :: HasBinary a IO => Handle -> IO a hRead handle = readBin (toReadBinaryHandle handle) toWriteBinaryHandle :: Handle -> WriteBinary IO toWriteBinaryHandle handle = WriteBinary { writeByte = hPutByte handle, writeBytes = hPutBytes handle } toReadBinaryHandle :: Handle -> ReadBinary IO toReadBinaryHandle handle = ReadBinary { readByte = hGetByte handle, readBytes = hGetBytes handle } {- ---------------------------------------------------------------------- Writing HasBinary instances to a memory area We do this by allocating an area, and then doubling its size as necessary. ---------------------------------------------------------------------- -} -- | Somewhere to where you write binary data in memory. data BinArea = BinArea { bytes :: ! Bytes, -- current storage area len :: ! Int, -- its length next :: ! Int -- where to write next bit of data. } -- | Write an (a) to memory. The 'Int' is the length of the area. writeToBytes :: HasBinary a StateBinArea => a -> IO (Bytes, Int) writeToBytes = writeToBytes0 1000 {- Be generous, since memory is cheap. Make it a bit less than a power of two, since some memory allocation algorithms (buddy algorithm) like this. -} {- | Write an (a) to memory. The integer argument is an initial guess at the number of bytes that will be needed. This should be greater than 0. If it is too small, there will be unnecessary reallocations; if too large, too much memory will be used. -} writeToBytes0 :: HasBinary a StateBinArea => Int -> a -> IO (Bytes, Int) -- The result is returned as a pair (data area,length) writeToBytes0 len0 a = do binArea0 <- mkEmptyBinArea len0 ((), binArea1) <- runStateT (writeBin writeBinaryBinArea a) binArea0 closeBinArea binArea1 -- | Create an empty 'BinArea', given the initial size. mkEmptyBinArea :: Int -> IO BinArea -- the argument gives the initial size to use (which had better be positive). mkEmptyBinArea l = do bs <- bytesMalloc l return BinArea { bytes = bs, len = l, next = 0 } -- | Return all the data currently in the 'BinArea' closeBinArea :: BinArea -> IO (Bytes, Int) closeBinArea binArea = do let bytes1 = bytes binArea l = next binArea bytes2 <- bytesReAlloc bytes1 l return (bytes2, l) -- | a state monad containing the BinArea. type StateBinArea = StateT BinArea IO -- | A 'BinArea' as somewhere to put binary data. writeBinaryBinArea :: WriteBinary StateBinArea writeBinaryBinArea = WriteBinary { writeByte = \ byte -> StateT $ \ binArea0 -> do let next0 = next binArea0 next1 = next0 + 1 binArea1 <- ensureBinArea binArea0 next1 putByteToBytes byte (bytes binArea1) next0 return ((), binArea1 {next = next1}) , writeBytes = \ bytes' l -> StateT $ \ binArea0 -> do let next0 = next binArea0 next1 = next0 + l binArea1 <- ensureBinArea binArea0 next1 putBytesToBytes bytes' 0 (bytes binArea1) next0 l return ((), binArea1 {next = next1}) } -- | ensure that the given BinArea can hold at least len bytes. ensureBinArea :: BinArea -> Int -> IO BinArea ensureBinArea binArea size = if size <= len binArea then return binArea else do let len1 = 2 * size bytes1 <- bytesReAlloc (bytes binArea) len1 return BinArea { bytes = bytes1, len = len1, next = next binArea } {- ---------------------------------------------------------------------- Reading Binary instances from a memory area We use BinArea's for this too. But this is simpler, because we don't have to worry about reallocing. ---------------------------------------------------------------------- -} {- | Read a value from binary data in memory. The 'Int' is the length, and there will be an error if this is either too small or too large. -} readFromBytes :: HasBinary a StateBinArea => (Bytes, Int) -> IO a readFromBytes bl = do let binArea0 = mkBinArea bl (a, binArea1) <- runStateT (readBin readBinaryBinArea) binArea0 checkFullBinArea binArea1 return a {- | Turn binary data in memory into a 'BinArea' (so that you can read from it). -} mkBinArea :: (Bytes, Int) -> BinArea mkBinArea (bytes', len') = BinArea { bytes = bytes', len = len', next = 0 } checkFullBinArea :: BinArea -> IO () checkFullBinArea binArea = unless (next binArea == len binArea) $ error "Binary.checkFullBinArea: mysterious extra bytes" -- | A BinArea as a source of binary data. readBinaryBinArea :: ReadBinary StateBinArea readBinaryBinArea = ReadBinary { readByte = StateT $ \ binArea0 -> do let next0 = next binArea0 next1 = next0 + 1 checkBinArea binArea0 next1 byte <- getByteFromBytes (bytes binArea0) next0 return (byte, binArea0 {next = next1}) , readBytes = \ l -> StateT $ \ binArea0 -> do let next0 = next binArea0 next1 = next0 + l checkBinArea binArea0 next1 bytes' <- bytesMalloc l putBytesToBytes (bytes binArea0) next0 bytes' 0 l return (bytes', binArea0 {next = next1}) } checkBinArea :: BinArea -> Int -> IO () -- check that the given BinArea can hold at least len bytes. checkBinArea binArea newNext = when (newNext > len binArea) $ error "Binary.checkBinArea - BinArea overflow on read" {- ---------------------------------------------------------------------- Lifting writeBinary and readBinary instances. ---------------------------------------------------------------------- -} -- | Transform the monad used by a 'WriteBinary' liftWriteBinary :: (forall a . m a -> n a) -> WriteBinary m -> WriteBinary n liftWriteBinary lft wb = let writeByte2 b = lft (writeByte wb b) writeBytes2 b i = lft (writeBytes wb b i) in WriteBinary {writeByte = writeByte2, writeBytes = writeBytes2} -- | Transform the monad used by a 'ReadBinary' liftReadBinary :: (forall a . m a -> n a) -> ReadBinary m -> ReadBinary n liftReadBinary lft rb = let readByte2 = lft (readByte rb) readBytes2 i = lft (readBytes rb i) in ReadBinary {readByte = readByte2, readBytes = readBytes2}