threepenny-gui-0.8.2.2: src/Foreign/RemotePtr.hs
{-# LANGUAGE RecordWildCards, CPP, ExistentialQuantification #-}
{-# LANGUAGE MagicHash, UnboxedTuples #-}
module Foreign.RemotePtr (
-- * Synopsis
-- | Toolbox for managing remote objects in Haskell.
-- * RemotePtr
RemotePtr,
withRemotePtr, addFinalizer, destroy, addReachable, clearReachable,
unprotectedGetCoupon,
-- * Coupons and Vendors
Coupon, newCoupon,
Vendor, newVendor, lookup,
newRemotePtr,
) where
import Prelude hiding (lookup)
import Control.Monad
import Control.Concurrent
import qualified Data.Text as T
import qualified Data.Map as Map
import Data.Functor
import Data.IORef
import System.IO.Unsafe (unsafePerformIO)
import System.Mem.Weak hiding (addFinalizer)
import qualified System.Mem.Weak as Weak
import qualified GHC.Base as GHC
import qualified GHC.Weak as GHC
import qualified GHC.IORef as GHC
import qualified GHC.STRef as GHC
#if CABAL
#if MIN_VERSION_base(4,6,0)
#else
atomicModifyIORef' = atomicModifyIORef
#endif
#endif
mkWeakIORefValue :: IORef a -> value -> IO () -> IO (Weak value)
#if CABAL
#if MIN_VERSION_base(4,9,0)
mkWeakIORefValue r@(GHC.IORef (GHC.STRef r#)) v (GHC.IO f) = GHC.IO $ \s ->
case GHC.mkWeak# r# v f s of (# s1, w #) -> (# s1, GHC.Weak w #)
#else
mkWeakIORefValue r@(GHC.IORef (GHC.STRef r#)) v f = GHC.IO $ \s ->
case GHC.mkWeak# r# v f s of (# s1, w #) -> (# s1, GHC.Weak w #)
#endif
#else
mkWeakIORefValue r@(GHC.IORef (GHC.STRef r#)) v (GHC.IO f) = GHC.IO $ \s ->
case GHC.mkWeak# r# v f s of (# s1, w #) -> (# s1, GHC.Weak w #)
#endif
type Map = Map.Map
{-----------------------------------------------------------------------------
Types
------------------------------------------------------------------------------}
-- | A 'Coupon' is a unique identifier.
--
-- It is a string of alphanumeric ASCII characters and it is intended to
-- be sent to or received from a remote program.
--
-- The data structure 'Vendor' associates 'Coupon's to 'RemotPtr' objects.
type Coupon = T.Text
-- | A 'RemotePtr' is a pointer to a foreign object.
--
-- Like a 'ForeignPtr', it refers to an object managed by an environment
-- external to the Haskell runtime.
-- Likewise, you can assign finalizers to a 'RemotePtr'. The finalizers
-- will be run when the Haskell runtime garbage collects this value.
-- They can perform some cleanup operations, like freeing memory.
--
-- Unlike a 'ForeignPtr', the object referenced by a 'RemotePtr' is not
-- necessarily a block of RAM. Instead, it can refer to things like an object
-- managed by a remote program.
type RemotePtr a = IORef (RemoteData a)
data RemoteData a = RemoteData
{ self :: Weak (RemotePtr a)
, coupon :: Coupon
, value :: a
, children :: IORef [SomeWeak]
}
-- Existentially quantified weak pointer. We only care about its finalizer.
data SomeWeak = forall a. SomeWeak (Weak a)
-- | A 'Vendor' is a bijective mapping from 'Coupon' to 'RemotePtr'.
--
-- Every 'Coupon' has at most one 'RemotePtr' associated to it.
-- A single 'RemotePtr' will always be associated with the same 'Coupon'.
data Vendor a = Vendor
{ coupons :: MVar (Map Coupon (Weak (RemotePtr a)))
, counter :: MVar [Integer]
}
{-----------------------------------------------------------------------------
Vendor and Coupons
------------------------------------------------------------------------------}
-- | Create a new 'Vendor' for trading 'Coupon's and 'RemotePtr's.
newVendor :: IO (Vendor a)
newVendor = do
counter <- newMVar [0..]
coupons <- newMVar Map.empty
return $ Vendor {..}
-- | Take a 'Coupon' to a 'Vendor' and maybe you'll get a 'RemotePtr' for it.
lookup :: Coupon -> Vendor a -> IO (Maybe (RemotePtr a))
lookup coupon Vendor{..} = do
w <- Map.lookup coupon <$> readMVar coupons
maybe (return Nothing) deRefWeak w
-- | Create a new 'Coupon'.
--
-- WARNING: This coupon is only unique relative to this 'Vendor'.
-- There is no guarantee that this 'Coupon' is globally unique,
-- certainly not on a remote machine.
newCoupon :: Vendor a -> IO Coupon
newCoupon Vendor{..} =
T.pack . show <$> modifyMVar counter (\(n:ns) -> return (ns,n))
-- | Create a new 'RemotePtr' from a 'Coupon' and register it with a 'Vendor'.
newRemotePtr :: Coupon -> a -> Vendor a -> IO (RemotePtr a)
newRemotePtr coupon value Vendor{..} = do
children <- newIORef []
let self = undefined
ptr <- newIORef RemoteData{..}
let finalize = modifyMVar coupons $ \m -> return (Map.delete coupon m, ())
w <- mkWeakIORef ptr finalize
modifyMVar coupons $ \m -> return (Map.insert coupon w m, ())
atomicModifyIORef' ptr $ \itemdata -> (itemdata { self = w }, ())
return ptr
{-----------------------------------------------------------------------------
RemotePtr
------------------------------------------------------------------------------}
-- | Access the data of the 'RemotePtr'.
--
-- While the action is being performed, it is ensured that the 'RemotePtr'
-- will not be garbage collected
-- and its 'Coupon' can be successfully redeemed at the 'Vendor'.
withRemotePtr :: RemotePtr a -> (Coupon -> a -> IO b) -> IO b
withRemotePtr ptr f = do
RemoteData{..} <- readIORef ptr
b <- f coupon value
touch ptr
return b
where
-- make sure that the pointer is alive at this point in the code
touch ptr = void $ readIORef ptr
-- | Unprotected access the 'Coupon' of a 'RemotePtr'.
--
-- Note: There is no guarantee that the 'RemotePtr' is alive
-- after this operation and that the 'Coupon' can be redeemed at a 'Vendor'.
-- Most of the time, you should use 'withRemotePtr' instead.
--
-- Note: In particular, if you use this with @unsafePerformIO@,
-- the risk is high that you only refer to the 'RemotePtr' argument via
-- the result just obtained, and the pointer will be garbage collected.
unprotectedGetCoupon :: RemotePtr a -> IO Coupon
unprotectedGetCoupon ptr = coupon <$> readIORef ptr
-- | Add a finalizer that is run when the 'RemotePtr' is garbage collected.
--
-- The associated coupon cannot be redeemed anymore while the finalizer runs.
addFinalizer :: RemotePtr a -> IO () -> IO ()
addFinalizer ptr = void . mkWeakIORef ptr
-- | FIXME: Is this finalizer really run when 'destroy' is called?
-- | Destroy a 'RemotePtr' and run all finalizers for it.
-- 'Coupon's for this pointer can no longer be redeemed.
destroy :: RemotePtr a -> IO ()
destroy ptr = finalize =<< self <$> readIORef ptr
-- | When dealing with several foreign objects,
-- it is useful to model dependencies between them.
--
-- After this operation, the second 'RemotePtr' will be reachable
-- whenever the first one is reachable.
-- For instance, you should call this function when the second foreign object
-- is actually a subobject of the first one.
--
-- Note: It is possible to model dependencies in the @parent@ data,
-- but the 'addReachable' method is preferrable,
-- as it allows all child object to be garbage collected at once.
addReachable :: RemotePtr a -> RemotePtr b -> IO ()
addReachable parent child = do
w <- mkWeakIORefValue parent child $ return ()
ref <- children <$> readIORef parent
atomicModifyIORef' ref $ \ws -> (SomeWeak w:ws, ())
-- | Clear all dependencies.
--
-- Reachability of this 'RemotePtr' no longer implies reachability
-- of other items, as formerly implied by calls to 'addReachable'.
clearReachable :: RemotePtr a -> IO ()
clearReachable parent = do
ref <- children <$> readIORef parent
xs <- atomicModifyIORef' ref $ \xs -> ([], xs)
sequence_ [finalize x | SomeWeak x <- xs]