haskus-binary-1.6: src/lib/Haskus/Memory/Buffer.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE MagicHash #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE UnboxedTuples #-}
{-# LANGUAGE UnboxedSums #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE UnliftedFFITypes #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE PatternSynonyms #-}
-- | A buffer in memory
module Haskus.Memory.Buffer where
import Haskus.Number.Word
import Haskus.Number.Int
import Haskus.Binary.Storable
import Haskus.Memory.Utils (memcpy#)
import Haskus.Utils.Monad
import Data.IORef
import System.IO.Unsafe
#if MIN_VERSION_GLASGOW_HASKELL (9,0,0,0)
import Unsafe.Coerce (unsafeCoerce#)
#endif
import GHC.STRef
import GHC.IORef
import GHC.Prim
import GHC.Base
import GHC.Exts (toList, IsList(..), Ptr (..))
-- $setup
-- >>> :set -XDataKinds
-- >>> :set -XTypeApplications
-- >>> :set -XFlexibleContexts
-- >>> :set -XTypeFamilies
-- >>> :set -XScopedTypeVariables
-- >>> import Haskus.Binary.Bits
-- There are different kinds of buffers:
-- 1. in managed heap: small and unpinned
-- 2. in managed heap: pinned
-- 3. out of the managed heap
--
-- GHC maintains a distinction between immutable and mutable in heap buffers
-- (respectively ByteArray# and MutableByteArray#) but they are represented by
-- the same heap objects and we can freely convert from one to the other.
--
-- Buffers and managed heap may be pinned (i.e. may have a fixed address). GHC
-- automatically pins large buffers. Buffers out of the managed heap are
-- represented by their address, hence the latter mustn't/can't change and they
-- behave as pinned buffers.
--
-- It is common to want to attach finalizers to buffers (e.g. to reclaim memory
-- for buffer out of the managed heap). We can't directly attach them to the
-- Addr# or to the ByteArray#. We must also be very careful to avoid attaching
-- them to the box (e.g. to "Ptr" in "Ptr Addr#") because GHC freely removes
-- boxes to produce faster code. The best option is to attach the finalizers to
-- an IORef which contains the finalizers themselves!
--
-- For performance, we often want buffer references to be unboxed. Hence the use
-- of unboxed sums/tuples.
type InBuffer# s = MutableByteArray# s
type ExtBuffer# = (# Addr#, Word# #)
type Finalizers# s = (# (# #) | MutVar# s [IO ()] #) -- Finalizers are optional
type Buffer# s = (# (# InBuffer# s | ExtBuffer# #), Finalizers# s #)
data STBuffer s = Buffer (Buffer# s)
type Buffer = STBuffer RealWorld
{-# COMPLETE InBuffer, OutBuffer #-}
pattern OutBuffer :: Addr# -> Word# -> Finalizers# s -> STBuffer s
pattern OutBuffer addr sz fin = Buffer (# (# | (# addr, sz #) #), fin #)
pattern InBuffer :: MutableByteArray# s -> Finalizers# s -> STBuffer s
pattern InBuffer ba fin = Buffer (# (# ba | #), fin #)
{-# COMPLETE NoFinalizers, Finalizers #-}
pattern NoFinalizers :: Finalizers# s
pattern NoFinalizers = (# (# #) | #)
pattern Finalizers :: MutVar# s [IO ()] -> Finalizers# s
pattern Finalizers fin = (# | fin #)
-----------------------------------------------------------------
-- Allocation
-----------------------------------------------------------------
-- | Allocate a buffer (unpinned if small)
--
-- >>> b <- newBuffer 1024
--
newBuffer :: Word -> IO Buffer
newBuffer (W# sz) = IO \s0 ->
let !(# s1,ba #) = newByteArray# (word2Int# sz) s0
in (# s1, InBuffer ba NoFinalizers #)
-- | Allocate a buffer (pinned)
newPinnedBuffer :: Word -> IO Buffer
newPinnedBuffer (W# sz) = IO \s0 ->
let !(# s1,ba #) = newPinnedByteArray# (word2Int# sz) s0
in (# s1, InBuffer ba NoFinalizers #)
-- | Allocate an aligned buffer (pinned)
newAlignedPinnedBuffer :: Word -> Word -> IO Buffer
newAlignedPinnedBuffer (W# sz) (W# al) = IO \s0 ->
let !(# s1,ba #) = newAlignedPinnedByteArray# (word2Int# sz) (word2Int# al) s0
in (# s1, InBuffer ba NoFinalizers #)
-- | Attach an external buffer
attachExternalBuffer :: Addr# -> Word# -> Buffer
attachExternalBuffer addr sz = OutBuffer addr sz NoFinalizers
-- | Attach an external buffer
attachExternalBufferPtr :: Ptr a -> Word# -> Buffer
attachExternalBufferPtr (Ptr addr) sz = attachExternalBuffer addr sz
-- | Attach an external buffer with finalizers
attachFinalizedBuffer :: Addr# -> Word# -> IO Buffer
attachFinalizedBuffer addr sz = IO \s ->
let !(# s', fin #) = newMutVar# [] s
in (# s', OutBuffer addr sz (Finalizers fin) #)
-----------------------------------------------------------------
-- Finalizers
-----------------------------------------------------------------
getFinalizers :: STBuffer s -> Finalizers# s
getFinalizers = \case
InBuffer _ fin -> fin
OutBuffer _ _ fin -> fin
-- | Add a finalizer.
--
-- The latest added finalizers are executed first. Finalizers are not guaranteed
-- to run (e.g. if the program exits before the buffer is collected).
--
addFinalizer :: Buffer -> IO () -> IO ()
addFinalizer b f = do
let !fin = getFinalizers b
case fin of
Finalizers rfs -> do
mBox <- atomicModifyIORef (IORef (STRef rfs)) $ \finalizers -> case finalizers of
[] -> let box = [f] in (box , Just box)
fs -> (f:fs, Nothing)
-- add the weak reference to the first cons cell of the finalizers list,
-- that's the only boxed thing we have.
case mBox of
Nothing -> return ()
Just box -> IO \s ->
case mkWeak# box b (unIO $ runFinalizers fin) s of
(# s1, _wk #) -> (# s1, () #)
NoFinalizers -> error "insertFinalizer: can't insert finalizer (NoFinalizers)"
-- | Internal function used to execute finalizers
runFinalizers :: Finalizers# RealWorld -> IO ()
runFinalizers = \case
NoFinalizers -> return ()
Finalizers fin -> do
-- atomically remove finalizers to avoid double execution
fs <- atomicModifyIORef (IORef (STRef fin)) $ \fs -> ([], fs)
sequence_ fs
-- | Indicate if a buffer is pinned
bufferIsPinned :: STBuffer s -> Bool
bufferIsPinned = \case
OutBuffer {} -> True
InBuffer ba _ -> isTrue# (isMutableByteArrayPinned# ba)
-- | Touch a buffer to keep it alive
bufferTouch :: Buffer -> IO ()
bufferTouch b = IO \s -> case getFinalizers b of
NoFinalizers -> (# s, () #)
Finalizers fin -> case touch# fin s of
s' -> (# s', () #)
withBuffer :: Buffer -> IO a -> IO a
withBuffer b f = do
r <- f
bufferTouch b
pure r
-- | Get buffer size
bufferSize :: Buffer -> IO Word
bufferSize = \case
OutBuffer _addr sz _fin -> pure (W# sz)
InBuffer ba _fin -> IO \s -> case getSizeofMutableByteArray# ba s of
(# s', i #) -> (# s', W# (int2Word# i) #)
-- | Buffer address (careful with unpinned buffers!)
bufferAddr# :: Buffer -> Addr#
bufferAddr# = \case
OutBuffer addr _ _ -> addr
InBuffer ba _ -> byteArrayContents# (unsafeCoerce# ba)
-- | Get contents as a list of bytes
bufferToList :: Buffer -> IO [Word8]
bufferToList b = withBuffer b case b of
OutBuffer addr sz _fin -> peekArray (W# sz) (Ptr addr)
InBuffer {}
| bufferIsPinned b -> do
sz <- bufferSize b
peekArray sz (Ptr (bufferAddr# b))
InBuffer {} -> do
sz <- bufferSize b
let
go i xs = do
x <- bufferReadWord8 b i
if i == 0
then pure (x:xs)
else go (i-1) (x:xs)
go (sz-1) []
-- | Read a Word8, offset in bytes
--
-- We don't check that the offset is valid
bufferReadWord8 :: Buffer -> Word -> IO Word8
bufferReadWord8 b (W# off) = withBuffer b case b of
InBuffer ba _fin -> IO \s -> case readWord8Array# ba (word2Int# off) s of
(# s2 , r #) -> (# s2 , W8# r #)
OutBuffer addr _sz _fin -> IO \s -> case readWord8OffAddr# addr (word2Int# off) s of
(# s2 , r #) -> (# s2 , W8# r #)
-- | Read a Word16, offset in bytes
--
-- We don't check that the offset is valid
--
-- >>> let b = [0x12,0x34,0x56,0x78] :: Buffer
-- >>> x <- bufferReadWord16IO b 0
-- >>> (x == 0x1234) || (x == 0x3412)
-- True
--
bufferReadWord16 :: Buffer -> Word -> IO Word16
bufferReadWord16 b (W# off) = withBuffer b case b of
InBuffer ba _fin -> IO \s -> case readWord8ArrayAsWord16# ba (word2Int# off) s of
(# s2 , r #) -> (# s2 , W16# r #)
OutBuffer addr _sz _fin -> IO \s -> case readWord16OffAddr# (addr `plusAddr#` word2Int# off) 0# s of
(# s2 , r #) -> (# s2 , W16# r #)
-- | Read a Word32, offset in bytes
--
-- We don't check that the offset is valid
--
-- >>> let b = [0x12,0x34,0x56,0x78] :: Buffer
-- >>> x <- bufferReadWord32IO b 0
-- >>> (x == 0x12345678) || (x == 0x78563412)
-- True
--
bufferReadWord32 :: Buffer -> Word -> IO Word32
bufferReadWord32 b (W# off) = withBuffer b case b of
InBuffer ba _fin -> IO \s -> case readWord8ArrayAsWord32# ba (word2Int# off) s of
(# s2 , r #) -> (# s2 , W32# r #)
OutBuffer addr _sz _fin -> IO \s -> case readWord32OffAddr# (addr `plusAddr#` word2Int# off) 0# s of
(# s2 , r #) -> (# s2 , W32# r #)
-- | Read a Word64, offset in bytes
--
-- We don't check that the offset is valid
--
-- >>> let b = [0x12,0x34,0x56,0x78,0x9A,0xBC,0xDE,0xF0] :: Buffer
-- >>> x <- bufferReadWord64IO b 0
-- >>> (x == 0x123456789ABCDEF0) || (x == 0xF0DEBC9A78563412)
-- True
--
bufferReadWord64 :: Buffer -> Word -> IO Word64
bufferReadWord64 b (W# off) = withBuffer b case b of
InBuffer ba _fin -> IO \s -> case readWord8ArrayAsWord64# ba (word2Int# off) s of
(# s2 , r #) -> (# s2 , W64# r #)
OutBuffer addr _sz _fin -> IO \s -> case readWord64OffAddr# (addr `plusAddr#` word2Int# off) 0# s of
(# s2 , r #) -> (# s2 , W64# r #)
-- | Do something with a buffer address
--
-- Note: don't write into immutable buffers as it would break referential
-- consistency
withBufferAddr# :: Buffer -> (Addr# -> IO a) -> IO a
withBufferAddr# b f = withBuffer b (f (bufferAddr# b))
-- | Write a Word8, offset in bytes
--
-- We don't check that the offset is valid
--
-- >>> b <- newBuffer 10
-- >>> bufferWriteWord8IO b 1 123
-- >>> bufferReadWord8IO b 1
-- 123
--
bufferWriteWord8 :: Buffer -> Word -> Word8 -> IO ()
bufferWriteWord8 b (W# off) (W8# v) = withBuffer b case b of
InBuffer ba _ -> IO \s -> case writeWord8Array# ba (word2Int# off) v s of s2 -> (# s2 , () #)
OutBuffer addr _ _ -> IO \s -> case writeWord8OffAddr# addr (word2Int# off) v s of s2 -> (# s2 , () #)
-- | Write a Word16, offset in bytes
--
-- We don't check that the offset is valid
--
-- >>> b <- newBuffer 10
-- >>> let v = 1234 :: Word16
-- >>> bufferWriteWord16IO b 1 v
-- >>> bufferReadWord16IO b 1
-- 1234
--
-- >>> (x :: Word16) <- fromIntegral <$> bufferReadWord8IO b 1
-- >>> (y :: Word16) <- fromIntegral <$> bufferReadWord8IO b 2
-- >>> (((x `shiftL` 8) .|. y) == v) || (((y `shiftL` 8) .|. x) == v)
-- True
--
bufferWriteWord16 :: Buffer -> Word -> Word16 -> IO ()
bufferWriteWord16 b (W# off) (W16# v) = withBuffer b case b of
InBuffer ba _ -> IO \s -> case writeWord8ArrayAsWord16# ba (word2Int# off) v s of s2 -> (# s2 , () #)
OutBuffer addr _ _ -> IO \s -> case writeWord16OffAddr# (addr `plusAddr#` word2Int# off) 0# v s of s2 -> (# s2 , () #)
-- | Write a Word32, offset in bytes
--
-- We don't check that the offset is valid
--
-- >>> b <- newBuffer 10
-- >>> let v = 1234 :: Word32
-- >>> bufferWriteWord32IO b 1 v
-- >>> bufferReadWord32IO b 1
-- 1234
--
bufferWriteWord32 :: Buffer -> Word -> Word32 -> IO ()
bufferWriteWord32 b (W# off) (W32# v) = withBuffer b case b of
InBuffer ba _ -> IO \s -> case writeWord8ArrayAsWord32# ba (word2Int# off) v s of s2 -> (# s2 , () #)
OutBuffer addr _ _ -> IO \s -> case writeWord32OffAddr# (addr `plusAddr#` word2Int# off) 0# v s of s2 -> (# s2 , () #)
-- | Write a Word64, offset in bytes
--
-- We don't check that the offset is valid
--
-- >>> b <- newBuffer 10
-- >>> let v = 1234 :: Word64
-- >>> bufferWriteWord64IO b 1 v
-- >>> bufferReadWord64IO b 1
-- 1234
--
bufferWriteWord64 :: Buffer -> Word -> Word64 -> IO ()
bufferWriteWord64 b (W# off) (W64# v) = withBuffer b case b of
InBuffer ba _ -> IO \s -> case writeWord8ArrayAsWord64# ba (word2Int# off) v s of s2 -> (# s2 , () #)
OutBuffer addr _ _ -> IO \s -> case writeWord64OffAddr# (addr `plusAddr#` word2Int# off) 0# v s of s2 -> (# s2 , () #)
-- | Support for OverloadedLists
--
-- >>> :set -XOverloadedLists
-- >>> let b = [25,26,27,28] :: Buffer
--
instance IsList Buffer where
type Item Buffer = Word8
toList b = unsafePerformIO (bufferToList b)
fromList xs = unsafePerformIO do
let sz = fromIntegral (length xs)
b <- newBuffer sz
forM_ ([0..] `zip` xs) \(i,x) -> do
bufferWriteWord8 b i x
pure b
fromListN sz xs = unsafePerformIO do
b <- newBuffer (fromIntegral sz)
forM_ ([0..] `zip` xs) \(i,x) -> do
bufferWriteWord8 b i x
pure b
-- | Copy a buffer into another from/to the given offsets
--
-- We don't check buffer limits.
--
-- >>> let b = [0,1,2,3,4,5,6,7,8] :: Buffer
-- >>> b2 <- newBuffer 8
-- >>> bufferCopy b 4 b2 0 4
-- >>> bufferCopy b 0 b2 4 4
-- >>> forM [0..7] (bufferReadWord8 b2)
-- [4,5,6,7,0,1,2,3]
--
bufferCopy
:: Buffer -- ^ Source buffer
-> Word -- ^ Offset in source buffer
-> Buffer -- ^ Target buffer
-> Word -- ^ Offset in target buffer
-> Word -- ^ Number of Word8 to copy
-> IO ()
bufferCopy src (W# soff) dst (W# doff) (W# cnt) = withBuffer src $ withBuffer dst case (src,dst) of
(InBuffer sba _, InBuffer dba _) -> IO \s ->
case copyMutableByteArray# sba (word2Int# soff) dba (word2Int# doff) (word2Int# cnt) s of
s2 -> (# s2, () #)
(InBuffer sba _, OutBuffer addr _ _) -> IO \s ->
case copyMutableByteArrayToAddr# sba (word2Int# soff) (addr `plusAddr#` word2Int# doff) (word2Int# cnt) s of
s2 -> (# s2, () #)
(OutBuffer addr _ _, InBuffer dba _) -> IO \s ->
case copyAddrToByteArray# (addr `plusAddr#` word2Int# soff) dba (word2Int# doff) (word2Int# cnt) s of
s2 -> (# s2, () #)
(OutBuffer addr1 _ _, OutBuffer addr2 _ _) ->
memcpy# (addr1 `plusAddr#` word2Int# soff) (addr2 `plusAddr#` word2Int# doff) (word2Int# cnt)