packages feed

duckdb-simple-0.2.0.0: src/Database/DuckDB/Simple/Callback.hs

{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE ForeignFunctionInterface #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}

-- | Manage callback resources at the DuckDB ownership boundary.
module Database.DuckDB.Simple.Callback (
    withCallbackResources,
    transferCallbackState,
    runCallback,
    ignoreCallbackExceptions,
) where

import Control.Exception (SomeException, catch, displayException, finally, mask, mask_, onException, try)
import Data.IORef (modifyIORef', newIORef, readIORef)
import qualified Data.Text as Text
import qualified Data.Text.Foreign as TextForeign
import Database.DuckDB.FFI (DuckDBDeleteCallback)
import Foreign.C.String (CString, withCString)
import Foreign.Ptr (FunPtr, Ptr, freeHaskellFunPtr, nullPtr)
import Foreign.StablePtr (StablePtr, castPtrToStablePtr, castStablePtrToPtr, deRefStablePtr, freeStablePtr, newStablePtr)

{- | Acquire callbacks and transfer their cleanup to a DuckDB object.
The object must be non-null. Its destructor must run after the action.
-}
withCallbackResources ::
    ((forall a. IO (FunPtr a) -> IO (FunPtr a)) -> IO r) ->
    (Ptr () -> DuckDBDeleteCallback -> IO ()) ->
    (r -> IO b) ->
    IO b
withCallbackResources acquire attach action = mask \restore -> do
    cleanups <- newIORef []
    let cleanup = readIORef cleanups >>= sequence_
        allocate make = do
            ptr <- make
            modifyIORef' cleanups (freeHaskellFunPtr ptr :)
                `onException` freeHaskellFunPtr ptr
            pure ptr
    resources <- acquire allocate `onException` cleanup
    stable <- newStablePtr cleanup `onException` cleanup
    attach (castStablePtrToPtr stable) callbackResourcesDestructor
        `onException` (freeStablePtr stable >> cleanup)
    restore (action resources)

-- | Transfer one state value to a DuckDB callback state slot.
transferCallbackState :: (Ptr () -> DuckDBDeleteCallback -> IO ()) -> a -> IO ()
transferCallbackState attach state = mask_ do
    stable <- newStablePtr state
    attach (castStablePtrToPtr stable) callbackStateDestructor
        `onException` freeStablePtr stable

-- | Convert callback exceptions to DuckDB errors.
runCallback :: (CString -> IO ()) -> IO () -> IO ()
runCallback setError action = mask \restore -> do
    outcome <- try (restore action)
    case outcome of
        Right () -> pure ()
        Left (err :: SomeException) ->
            TextForeign.withCString (Text.pack (displayException err)) setError
                `catch` \(_ :: SomeException) ->
                    ignoreCallbackExceptions $
                        withCString "duckdb-simple: Haskell callback failed" setError

-- | Contain exceptions in callbacks which have no error channel.
ignoreCallbackExceptions :: IO () -> IO ()
ignoreCallbackExceptions action = mask \restore ->
    restore action `catch` \(_ :: SomeException) -> pure ()

-- | Release object-owned callbacks through a static C entry point.
releaseCallbackResources :: Ptr () -> IO ()
releaseCallbackResources raw =
    mask_
        $ ignoreCallbackExceptions
        $ if raw == nullPtr
            then pure ()
            else do
                let stable = castPtrToStablePtr raw :: StablePtr (IO ())
                (deRefStablePtr stable >>= id) `finally` freeStablePtr stable

-- | Release callback state independently of the registered function.
releaseCallbackState :: Ptr () -> IO ()
releaseCallbackState raw =
    mask_
        $ ignoreCallbackExceptions
        $ if raw == nullPtr then pure () else freeStablePtr (castPtrToStablePtr raw)

foreign export ccall "duckdb_simple_release_callback_resources"
    releaseCallbackResources :: Ptr () -> IO ()

foreign import ccall "&duckdb_simple_release_callback_resources"
    callbackResourcesDestructor :: DuckDBDeleteCallback

foreign export ccall "duckdb_simple_release_callback_state"
    releaseCallbackState :: Ptr () -> IO ()

foreign import ccall "&duckdb_simple_release_callback_state"
    callbackStateDestructor :: DuckDBDeleteCallback