From 54b760cc220057dab7ec974e67ba85122a202b23 Mon Sep 17 00:00:00 2001 From: Alexey Khudyakov Date: Thu, 9 Jan 2025 12:41:12 +0300 Subject: [PATCH 1/9] Reorganize module layout --- inline-python.cabal | 1 + src/Python/Inline/Literal.hs | 2 +- src/Python/Internal/CAPI.hs | 27 +++++++++++++++++++++++++++ src/Python/Internal/Eval.hs | 14 ++------------ src/Python/Internal/Program.hs | 6 ++++++ 5 files changed, 37 insertions(+), 13 deletions(-) create mode 100644 src/Python/Internal/CAPI.hs diff --git a/inline-python.cabal b/inline-python.cabal index ad689f7..2215c25 100644 --- a/inline-python.cabal +++ b/inline-python.cabal @@ -71,6 +71,7 @@ Library Python.Inline Python.Types Other-modules: + Python.Internal.CAPI Python.Internal.Eval Python.Internal.EvalQQ Python.Internal.Program diff --git a/src/Python/Inline/Literal.hs b/src/Python/Inline/Literal.hs index c1acd8d..cda38db 100644 --- a/src/Python/Inline/Literal.hs +++ b/src/Python/Inline/Literal.hs @@ -32,7 +32,7 @@ import Language.C.Inline.Unsafe qualified as CU import Python.Types import Python.Internal.Types import Python.Internal.Eval - +import Python.Internal.CAPI import Python.Internal.Program ---------------------------------------------------------------- diff --git a/src/Python/Internal/CAPI.hs b/src/Python/Internal/CAPI.hs new file mode 100644 index 0000000..0e33739 --- /dev/null +++ b/src/Python/Internal/CAPI.hs @@ -0,0 +1,27 @@ +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE TemplateHaskell #-} +-- | +-- Thin wrappers over C API +module Python.Internal.CAPI + ( decref + , incref + ) where + +import Foreign.Ptr +import Language.C.Inline qualified as C +import Language.C.Inline.Unsafe qualified as CU + +import Python.Internal.Types + + +---------------------------------------------------------------- +C.context (C.baseCtx <> pyCtx) +C.include "" +---------------------------------------------------------------- + + +decref :: Ptr PyObject -> Py () +decref p = Py [CU.exp| void { Py_DECREF($(PyObject* p)) } |] + +incref :: Ptr PyObject -> Py () +incref p = Py [CU.exp| void { Py_INCREF($(PyObject* p)) } |] diff --git a/src/Python/Internal/Eval.hs b/src/Python/Internal/Eval.hs index 27cc242..c40457f 100644 --- a/src/Python/Internal/Eval.hs +++ b/src/Python/Internal/Eval.hs @@ -19,8 +19,6 @@ module Python.Internal.Eval -- * GC-related , newPyObject -- * C-API wrappers - , decref - , incref , takeOwnership , ensureGIL , dropGIL @@ -56,6 +54,8 @@ import Language.C.Inline.Unsafe qualified as CU import Python.Internal.Types import Python.Internal.Util import Python.Internal.Program +import Python.Internal.CAPI + ---------------------------------------------------------------- C.context (C.baseCtx <> pyCtx) @@ -535,12 +535,6 @@ gcDecref p = [CU.block| void { -- C-API wrappers ---------------------------------------------------------------- -decref :: Ptr PyObject -> Py () -decref p = Py [CU.exp| void { Py_DECREF($(PyObject* p)) } |] - -incref :: Ptr PyObject -> Py () -incref p = Py [CU.exp| void { Py_INCREF($(PyObject* p)) } |] - -- | Ensure that we hold GIL for duration of action ensureGIL :: Py a -> Py a ensureGIL action = do @@ -559,10 +553,6 @@ dropGIL action = do st <- Py [CU.exp| PyThreadState* { PyEval_SaveThread() } |] Py $ action `finally` [CU.exp| void { PyEval_RestoreThread($(PyThreadState *st)) } |] --- | Decrement reference counter at end of ContT block -takeOwnership :: Ptr PyObject -> Program r (Ptr PyObject) -takeOwnership p = ContT $ \c -> c p `finally` decref p - ---------------------------------------------------------------- -- Conversion of exceptions diff --git a/src/Python/Internal/Program.hs b/src/Python/Internal/Program.hs index 303eaee..9db9780 100644 --- a/src/Python/Internal/Program.hs +++ b/src/Python/Internal/Program.hs @@ -7,6 +7,7 @@ module Python.Internal.Program , checkNull , finallyProg , onExceptionProg + , takeOwnership -- * Allocators , withPyAlloca , withPyAllocaArray @@ -27,6 +28,7 @@ import Foreign.Storable import Python.Internal.Types import Python.Internal.Util +import Python.Internal.CAPI -- | Internally we usually wrap 'Py' into 'ContT' in order get early @@ -60,6 +62,10 @@ onExceptionProg -> Program r () onExceptionProg fini = ContT $ \c -> c () `onException` fini +-- | Decrement reference counter at end of ContT block +takeOwnership :: Ptr PyObject -> Program r (Ptr PyObject) +takeOwnership p = ContT $ \c -> c p `finally` decref p + ---------------------------------------------------------------- -- Allocation in context of `ContT _ Py` From 83bb1ebf390867bf9a6e52243d6e9a2eb125b854 Mon Sep 17 00:00:00 2001 From: Alexey Khudyakov Date: Thu, 9 Jan 2025 12:46:53 +0300 Subject: [PATCH 2/9] Move function --- src/Python/Internal/CAPI.hs | 5 +++++ src/Python/Internal/Eval.hs | 1 - src/Python/Internal/EvalQQ.hs | 4 +--- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Python/Internal/CAPI.hs b/src/Python/Internal/CAPI.hs index 0e33739..b1feb20 100644 --- a/src/Python/Internal/CAPI.hs +++ b/src/Python/Internal/CAPI.hs @@ -5,6 +5,8 @@ module Python.Internal.CAPI ( decref , incref + -- * Simple wrappers + , basicNewDict ) where import Foreign.Ptr @@ -25,3 +27,6 @@ decref p = Py [CU.exp| void { Py_DECREF($(PyObject* p)) } |] incref :: Ptr PyObject -> Py () incref p = Py [CU.exp| void { Py_INCREF($(PyObject* p)) } |] + +basicNewDict :: Py (Ptr PyObject) +basicNewDict = Py [CU.exp| PyObject* { PyDict_New() } |] diff --git a/src/Python/Internal/Eval.hs b/src/Python/Internal/Eval.hs index c40457f..7255f73 100644 --- a/src/Python/Internal/Eval.hs +++ b/src/Python/Internal/Eval.hs @@ -54,7 +54,6 @@ import Language.C.Inline.Unsafe qualified as CU import Python.Internal.Types import Python.Internal.Util import Python.Internal.Program -import Python.Internal.CAPI ---------------------------------------------------------------- diff --git a/src/Python/Internal/EvalQQ.hs b/src/Python/Internal/EvalQQ.hs index 7ac6fa9..2acce44 100644 --- a/src/Python/Internal/EvalQQ.hs +++ b/src/Python/Internal/EvalQQ.hs @@ -37,6 +37,7 @@ import Python.Types import Python.Internal.Types import Python.Internal.Program import Python.Internal.Eval +import Python.Internal.CAPI import Python.Inline.Literal @@ -151,9 +152,6 @@ basicBindInDict name a p_dict = evalContT $ do 0 -> pure () _ -> mustThrowPyError "basicBindInDict" -basicNewDict :: Py (Ptr PyObject) -basicNewDict = Py [CU.exp| PyObject* { PyDict_New() } |] - -- | Return dict of @__main__@ module basicMainDict :: Py (Ptr PyObject) basicMainDict = Py [CU.block| PyObject* { From 488b4bef25e66f415bc652c53e8ac39451ad570c Mon Sep 17 00:00:00 2001 From: Alexey Khudyakov Date: Thu, 9 Jan 2025 12:48:06 +0300 Subject: [PATCH 3/9] Fix leak in python --- src/Python/Internal/EvalQQ.hs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Python/Internal/EvalQQ.hs b/src/Python/Internal/EvalQQ.hs index 2acce44..2cf6048 100644 --- a/src/Python/Internal/EvalQQ.hs +++ b/src/Python/Internal/EvalQQ.hs @@ -168,7 +168,9 @@ getFunctionObject p_dict = do callFunctionObject :: Ptr PyObject -> Ptr PyObject -> Py (Ptr PyObject) callFunctionObject fun kwargs = Py [CU.block| PyObject* { PyObject* args = PyTuple_Pack(0); - return PyObject_Call($(PyObject *fun), args, $(PyObject *kwargs)); + PyObject* res = PyObject_Call($(PyObject *fun), args, $(PyObject *kwargs)); + Py_DECREF(args); + return res; } |] From a6ae41cbf6c7954e8eb4ca8f1792811eab995318 Mon Sep 17 00:00:00 2001 From: Alexey Khudyakov Date: Thu, 9 Jan 2025 12:52:42 +0300 Subject: [PATCH 4/9] Move another function --- src/Python/Internal/CAPI.hs | 13 +++++++++++++ src/Python/Internal/EvalQQ.hs | 10 +--------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/Python/Internal/CAPI.hs b/src/Python/Internal/CAPI.hs index b1feb20..0612ee7 100644 --- a/src/Python/Internal/CAPI.hs +++ b/src/Python/Internal/CAPI.hs @@ -7,6 +7,7 @@ module Python.Internal.CAPI , incref -- * Simple wrappers , basicNewDict + , basicCallKwdOnly ) where import Foreign.Ptr @@ -30,3 +31,15 @@ incref p = Py [CU.exp| void { Py_INCREF($(PyObject* p)) } |] basicNewDict :: Py (Ptr PyObject) basicNewDict = Py [CU.exp| PyObject* { PyDict_New() } |] + +-- | Call python function using only keyword arguments +basicCallKwdOnly + :: Ptr PyObject -- ^ Function object + -> Ptr PyObject -- ^ Keywords. Must be dictionary + -> Py (Ptr PyObject) +basicCallKwdOnly fun kwd = Py [CU.block| PyObject* { + PyObject* args = PyTuple_Pack(0); + PyObject* res = PyObject_Call($(PyObject *fun), args, $(PyObject *kwd)); + Py_DECREF(args); + return res; + } |] diff --git a/src/Python/Internal/EvalQQ.hs b/src/Python/Internal/EvalQQ.hs index 2cf6048..28a92b1 100644 --- a/src/Python/Internal/EvalQQ.hs +++ b/src/Python/Internal/EvalQQ.hs @@ -132,7 +132,7 @@ evaluatorPyf getSource = evalContT $ do NULL -> error "INTERNAL ERROR: _inline_python_ must be present" p -> pure p -- Call python function we just constructed - callFunctionObject p_fun p_kwargs >>= \case + basicCallKwdOnly p_fun p_kwargs >>= \case NULL -> mustThrowPyError "evaluatorPyf" p_res -> newPyObject p_res @@ -165,14 +165,6 @@ getFunctionObject :: Ptr PyObject -> Py (Ptr PyObject) getFunctionObject p_dict = do Py [CU.exp| PyObject* { PyDict_GetItemString($(PyObject *p_dict), "_inline_python_") } |] -callFunctionObject :: Ptr PyObject -> Ptr PyObject -> Py (Ptr PyObject) -callFunctionObject fun kwargs = Py [CU.block| PyObject* { - PyObject* args = PyTuple_Pack(0); - PyObject* res = PyObject_Call($(PyObject *fun), args, $(PyObject *kwargs)); - Py_DECREF(args); - return res; - } |] - ---------------------------------------------------------------- From da468909f7ee8ae39e5faec7746b04ba547f3475 Mon Sep 17 00:00:00 2001 From: Alexey Khudyakov Date: Thu, 9 Jan 2025 12:55:18 +0300 Subject: [PATCH 5/9] Cleanup --- src/Python/Internal/EvalQQ.hs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Python/Internal/EvalQQ.hs b/src/Python/Internal/EvalQQ.hs index 28a92b1..c28a9b9 100644 --- a/src/Python/Internal/EvalQQ.hs +++ b/src/Python/Internal/EvalQQ.hs @@ -3,9 +3,7 @@ -- | module Python.Internal.EvalQQ ( -- * Evaluators and QQ - pyExecExpr - , pyEvalExpr - , evaluatorPymain + evaluatorPymain , evaluatorPy_ , evaluatorPye , evaluatorPyf @@ -139,8 +137,8 @@ evaluatorPyf getSource = evalContT $ do basicBindInDict :: ToPy a => String -> a -> Ptr PyObject -> Py () basicBindInDict name a p_dict = evalContT $ do - (p_key) <- withPyCString name - p_obj <- takeOwnership =<< lift (basicToPy a) + p_key <- withPyCString name + p_obj <- takeOwnership =<< lift (basicToPy a) lift $ case p_obj of NULL -> mustThrowPyError "basicBindInDict" _ -> do From 4b0594dc9b9246f6ed9ca69661de6927e4f1d87c Mon Sep 17 00:00:00 2001 From: Alexey Khudyakov Date: Thu, 9 Jan 2025 13:24:29 +0300 Subject: [PATCH 6/9] Small refactoring --- src/Python/Internal/Eval.hs | 13 ++++++++++--- src/Python/Internal/EvalQQ.hs | 24 ++++++++++-------------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/Python/Internal/Eval.hs b/src/Python/Internal/Eval.hs index 7255f73..ee3ef7a 100644 --- a/src/Python/Internal/Eval.hs +++ b/src/Python/Internal/Eval.hs @@ -28,6 +28,7 @@ module Python.Internal.Eval , checkThrowPyError , mustThrowPyError , checkThrowBadPyType + , throwOnNULL -- * Debugging , debugPrintPy ) where @@ -621,12 +622,18 @@ checkThrowPyError = -- | Throw python error as haskell exception if it's raised. If it's -- not that internal error. Another exception will be raised -mustThrowPyError :: String -> Py a -mustThrowPyError msg = +mustThrowPyError :: Py a +mustThrowPyError = Py [CU.exp| PyObject* { PyErr_Occurred() } |] >>= \case - NULL -> error $ "mustThrowPyError: no python exception raised. " ++ msg + NULL -> error $ "mustThrowPyError: no python exception raised." _ -> throwM =<< convertPy2Haskell +-- | Calls mustThrowPyError if pointer is null or returns it unchanged +throwOnNULL :: Ptr PyObject -> Py (Ptr PyObject) +throwOnNULL = \case + NULL -> mustThrowPyError + p -> pure p + checkThrowBadPyType :: Py () checkThrowBadPyType = do r <- Py [CU.block| int { diff --git a/src/Python/Internal/EvalQQ.hs b/src/Python/Internal/EvalQQ.hs index c28a9b9..d6a5954 100644 --- a/src/Python/Internal/EvalQQ.hs +++ b/src/Python/Internal/EvalQQ.hs @@ -130,25 +130,21 @@ evaluatorPyf getSource = evalContT $ do NULL -> error "INTERNAL ERROR: _inline_python_ must be present" p -> pure p -- Call python function we just constructed - basicCallKwdOnly p_fun p_kwargs >>= \case - NULL -> mustThrowPyError "evaluatorPyf" - p_res -> newPyObject p_res + newPyObject =<< throwOnNULL =<< basicCallKwdOnly p_fun p_kwargs basicBindInDict :: ToPy a => String -> a -> Ptr PyObject -> Py () basicBindInDict name a p_dict = evalContT $ do p_key <- withPyCString name - p_obj <- takeOwnership =<< lift (basicToPy a) - lift $ case p_obj of - NULL -> mustThrowPyError "basicBindInDict" - _ -> do - r <- Py [C.block| int { - PyObject* p_obj = $(PyObject* p_obj); - return PyDict_SetItemString($(PyObject* p_dict), $(char* p_key), p_obj); - } |] - case r of - 0 -> pure () - _ -> mustThrowPyError "basicBindInDict" + p_obj <- takeOwnership =<< lift (throwOnNULL =<< basicToPy a) + lift $ do + r <- Py [C.block| int { + PyObject* p_obj = $(PyObject* p_obj); + return PyDict_SetItemString($(PyObject* p_dict), $(char* p_key), p_obj); + } |] + case r of + 0 -> pure () + _ -> mustThrowPyError -- | Return dict of @__main__@ module basicMainDict :: Py (Ptr PyObject) From dc72cfc83e860a54113ce185872d988fd10d5443 Mon Sep 17 00:00:00 2001 From: Alexey Khudyakov Date: Thu, 9 Jan 2025 21:10:57 +0300 Subject: [PATCH 7/9] Make liftIO clear exception masking --- src/Python/Internal/Types.hs | 27 ++++++++++++++++++--------- src/Python/Types.hs | 3 ++- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/Python/Internal/Types.hs b/src/Python/Internal/Types.hs index 108bbe5..553edf1 100644 --- a/src/Python/Internal/Types.hs +++ b/src/Python/Internal/Types.hs @@ -12,6 +12,7 @@ module Python.Internal.Types , PyThreadState , PyError(..) , Py(..) + , pyIO -- * inline-C , pyCtx -- * Patterns @@ -23,6 +24,7 @@ module Python.Internal.Types import Control.Monad.IO.Class import Control.Monad.Catch +import Control.Exception import Data.Coerce import Data.Int import Data.Map.Strict qualified as Map @@ -70,17 +72,24 @@ data PyError instance Exception PyError --- | Monad for code which is interacts directly with python --- interpreter. During its execution python global interpreter lock --- (GIL) is held, async exceptions are masked. It's also always --- executed on bound thread if RTS supports one. --- --- It's needed in order to distinguish between code that needs such --- guarantees and plain IO. +-- | Monad for code which is interacts with python interpreter. Only +-- one haskell thread can interact with python interpreter at a +-- time. Function that execute @Py@ make sure that this invariant is +-- held. Also note that all code in @Py@ monad is executed with +-- asynchronous exception masked, but 'liftIO' removes mask. newtype Py a = Py (IO a) - deriving newtype (Functor,Applicative,Monad,MonadIO,MonadFail, + -- See NOTE: [Python and threading] + deriving newtype (Functor,Applicative,Monad,MonadFail, MonadThrow,MonadCatch,MonadMask) --- See NOTE: [Python and threading] + +-- | Inject @IO@ into @Py@ monad without changing masking state +-- (unlike 'liftIO') +pyIO :: IO a -> Py a +pyIO = Py + +-- | Removes exception masking +instance MonadIO Py where + liftIO = Py . interruptible ---------------------------------------------------------------- diff --git a/src/Python/Types.hs b/src/Python/Types.hs index dc35fbb..f6cd4e2 100644 --- a/src/Python/Types.hs +++ b/src/Python/Types.hs @@ -4,7 +4,8 @@ module Python.Types ( -- * @Py@ monad Py , runPy - , PyObject(..) + , pyIO + , PyObject , unsafeWithPyObject , PyError(..) ) where From 8fa02edd1ef763999dc06a1cd182b13ffa8257d5 Mon Sep 17 00:00:00 2001 From: Alexey Khudyakov Date: Thu, 9 Jan 2025 21:57:08 +0300 Subject: [PATCH 8/9] Make program a newtype and make sure we handle masking consitently --- src/Python/Inline/Literal.hs | 100 ++++++++++++++++----------------- src/Python/Internal/Eval.hs | 12 ++-- src/Python/Internal/EvalQQ.hs | 42 +++++++------- src/Python/Internal/Program.hs | 38 +++++++++---- 4 files changed, 103 insertions(+), 89 deletions(-) diff --git a/src/Python/Inline/Literal.hs b/src/Python/Inline/Literal.hs index cda38db..38e001f 100644 --- a/src/Python/Inline/Literal.hs +++ b/src/Python/Inline/Literal.hs @@ -56,7 +56,7 @@ class ToPy a where basicToPy :: a -> Py (Ptr PyObject) -- | Old hack for handling of strings basicListToPy :: [a] -> Py (Ptr PyObject) - basicListToPy xs = evalContT $ do + basicListToPy xs = runProgram $ do let n = fromIntegral $ length xs :: CLLong p_list <- checkNull (Py [CU.exp| PyObject* { PyList_New($(long long n)) } |]) onExceptionProg $ decref p_list @@ -64,9 +64,9 @@ class ToPy a where loop i (a:as) = basicToPy a >>= \case NULL -> pure nullPtr p_a -> do - liftIO [CU.exp| void { PyList_SET_ITEM($(PyObject* p_list), $(long long i), $(PyObject* p_a)) } |] + pyIO [CU.exp| void { PyList_SET_ITEM($(PyObject* p_list), $(long long i), $(PyObject* p_a)) } |] loop (i+1) as - lift $ loop 0 xs + progPy $ loop 0 xs -- | Convert python object to haskell value. class FromPy a where @@ -250,9 +250,9 @@ instance ToPy Char where uint32_t cs[1] = { $(unsigned i) }; return PyUnicode_DecodeUTF32((char*)cs, 4, NULL, NULL); } |] - basicListToPy str = evalContT $ do + basicListToPy str = runProgram $ do p_str <- withPyWCString str - liftIO [CU.exp| PyObject* { PyUnicode_FromWideChar($(wchar_t *p_str), -1) } |] + progIO [CU.exp| PyObject* { PyUnicode_FromWideChar($(wchar_t *p_str), -1) } |] instance FromPy Char where @@ -289,84 +289,84 @@ instance FromPy Bool where instance (ToPy a, ToPy b) => ToPy (a,b) where - basicToPy (a,b) = evalContT $ do + basicToPy (a,b) = runProgram $ do p_a <- takeOwnership =<< checkNull (basicToPy a) p_b <- takeOwnership =<< checkNull (basicToPy b) - liftIO [CU.exp| PyObject* { PyTuple_Pack(2, $(PyObject* p_a), $(PyObject* p_b)) } |] + progIO [CU.exp| PyObject* { PyTuple_Pack(2, $(PyObject* p_a), $(PyObject* p_b)) } |] -- | Will accept any iterable instance (FromPy a, FromPy b) => FromPy (a,b) where - basicFromPy p_tup = evalContT $ do + basicFromPy p_tup = runProgram $ do -- Unpack 2-tuple. p_args <- withPyAllocaArray 2 - unpack_ok <- liftIO [CU.exp| int { + unpack_ok <- progIO [CU.exp| int { inline_py_unpack_iterable($(PyObject *p_tup), 2, $(PyObject **p_args)) }|] - lift $ do checkThrowPyError - when (unpack_ok /= 0) $ throwM BadPyType + progPy $ do checkThrowPyError + when (unpack_ok /= 0) $ throwM BadPyType -- Parse each element of tuple - p_a <- takeOwnership =<< liftIO (peekElemOff p_args 0) - p_b <- takeOwnership =<< liftIO (peekElemOff p_args 1) - lift $ do a <- basicFromPy p_a - b <- basicFromPy p_b - pure (a,b) + p_a <- takeOwnership =<< progIO (peekElemOff p_args 0) + p_b <- takeOwnership =<< progIO (peekElemOff p_args 1) + progPy $ do a <- basicFromPy p_a + b <- basicFromPy p_b + pure (a,b) instance (ToPy a, ToPy b, ToPy c) => ToPy (a,b,c) where - basicToPy (a,b,c) = evalContT $ do + basicToPy (a,b,c) = runProgram $ do p_a <- takeOwnership =<< checkNull (basicToPy a) p_b <- takeOwnership =<< checkNull (basicToPy b) p_c <- takeOwnership =<< checkNull (basicToPy c) - liftIO [CU.exp| PyObject* { + progIO [CU.exp| PyObject* { PyTuple_Pack(3, $(PyObject *p_a), $(PyObject *p_b), $(PyObject *p_c)) } |] -- | Will accept any iterable instance (FromPy a, FromPy b, FromPy c) => FromPy (a,b,c) where - basicFromPy p_tup = evalContT $ do + basicFromPy p_tup = runProgram $ do -- Unpack 3-tuple. p_args <- withPyAllocaArray 3 - unpack_ok <- liftIO [CU.exp| int { + unpack_ok <- progIO [CU.exp| int { inline_py_unpack_iterable($(PyObject *p_tup), 3, $(PyObject **p_args)) }|] - lift $ do checkThrowPyError - when (unpack_ok /= 0) $ throwM BadPyType + progPy $ do checkThrowPyError + when (unpack_ok /= 0) $ throwM BadPyType -- Parse each element of tuple - p_a <- takeOwnership =<< liftIO (peekElemOff p_args 0) - p_b <- takeOwnership =<< liftIO (peekElemOff p_args 1) - p_c <- takeOwnership =<< liftIO (peekElemOff p_args 2) - lift $ do a <- basicFromPy p_a - b <- basicFromPy p_b - c <- basicFromPy p_c - pure (a,b,c) + p_a <- takeOwnership =<< progIO (peekElemOff p_args 0) + p_b <- takeOwnership =<< progIO (peekElemOff p_args 1) + p_c <- takeOwnership =<< progIO (peekElemOff p_args 2) + progPy $ do a <- basicFromPy p_a + b <- basicFromPy p_b + c <- basicFromPy p_c + pure (a,b,c) instance (ToPy a, ToPy b, ToPy c, ToPy d) => ToPy (a,b,c,d) where - basicToPy (a,b,c,d) = evalContT $ do + basicToPy (a,b,c,d) = runProgram $ do p_a <- takeOwnership =<< checkNull (basicToPy a) p_b <- takeOwnership =<< checkNull (basicToPy b) p_c <- takeOwnership =<< checkNull (basicToPy c) p_d <- takeOwnership =<< checkNull (basicToPy d) - liftIO [CU.exp| PyObject* { + progIO [CU.exp| PyObject* { PyTuple_Pack(4, $(PyObject *p_a), $(PyObject *p_b), $(PyObject *p_c), $(PyObject *p_d)) } |] -- | Will accept any iterable instance (FromPy a, FromPy b, FromPy c, FromPy d) => FromPy (a,b,c,d) where - basicFromPy p_tup = evalContT $ do + basicFromPy p_tup = runProgram $ do -- Unpack 3-tuple. p_args <- withPyAllocaArray 4 - unpack_ok <- liftIO [CU.exp| int { + unpack_ok <- progIO [CU.exp| int { inline_py_unpack_iterable($(PyObject *p_tup), 4, $(PyObject **p_args)) }|] - lift $ do checkThrowPyError - when (unpack_ok /= 0) $ throwM BadPyType + progPy $ do checkThrowPyError + when (unpack_ok /= 0) $ throwM BadPyType -- Parse each element of tuple - p_a <- takeOwnership =<< liftIO (peekElemOff p_args 0) - p_b <- takeOwnership =<< liftIO (peekElemOff p_args 1) - p_c <- takeOwnership =<< liftIO (peekElemOff p_args 2) - p_d <- takeOwnership =<< liftIO (peekElemOff p_args 3) - lift $ do a <- basicFromPy p_a - b <- basicFromPy p_b - c <- basicFromPy p_c - d <- basicFromPy p_d - pure (a,b,c,d) + p_a <- takeOwnership =<< progIO (peekElemOff p_args 0) + p_b <- takeOwnership =<< progIO (peekElemOff p_args 1) + p_c <- takeOwnership =<< progIO (peekElemOff p_args 2) + p_d <- takeOwnership =<< progIO (peekElemOff p_args 3) + progPy $ do a <- basicFromPy p_a + b <- basicFromPy p_b + c <- basicFromPy p_c + d <- basicFromPy p_d + pure (a,b,c,d) instance (ToPy a) => ToPy [a] where basicToPy = basicListToPy @@ -436,7 +436,7 @@ instance (ToPy b) => ToPy (IO b) where basicToPy f = Py $ do -- f_ptr <- wrapCFunction $ \_ _ -> pyCallback $ do - lift $ basicToPy =<< dropGIL f + progPy $ basicToPy =<< dropGIL f -- [CU.exp| PyObject* { inline_py_callback_METH_NOARGS($(PyCFunction f_ptr)) } |] @@ -447,7 +447,7 @@ instance (FromPy a, Show a, ToPy b) => ToPy (a -> IO b) where -- f_ptr <- wrapCFunction $ \_ p_a -> pyCallback $ do a <- loadArg p_a 0 1 - lift $ basicToPy =<< dropGIL (f a) + progPy $ basicToPy =<< dropGIL (f a) -- [CU.exp| PyObject* { inline_py_callback_METH_O($(PyCFunction f_ptr)) } |] @@ -459,7 +459,7 @@ instance (FromPy a1, FromPy a2, ToPy b) => ToPy (a1 -> a2 -> IO b) where when (n /= 2) $ abortM $ raiseBadNArgs 2 n a1 <- loadArgFastcall p_arr 0 n a2 <- loadArgFastcall p_arr 1 n - lift $ basicToPy =<< dropGIL (f a1 a2) + progPy $ basicToPy =<< dropGIL (f a1 a2) -- [CU.exp| PyObject* { inline_py_callback_METH_FASTCALL($(PyCFunctionFast f_ptr)) } |] @@ -470,7 +470,7 @@ instance (FromPy a1, FromPy a2, ToPy b) => ToPy (a1 -> a2 -> IO b) where -- | Execute haskell callback function pyCallback :: Program (Ptr PyObject) (Ptr PyObject) -> IO (Ptr PyObject) -pyCallback io = callbackEnsurePyLock $ unPy $ ensureGIL $ evalContT io `catch` convertHaskell2Py +pyCallback io = callbackEnsurePyLock $ unPy $ ensureGIL $ runProgram io `catch` convertHaskell2Py -- | Load argument from python object for haskell evaluation loadArg @@ -479,7 +479,7 @@ loadArg -> Int -- ^ Argument number (0-based) -> Int64 -- ^ Total number of arguments -> Program (Ptr PyObject) a -loadArg p (fromIntegral -> i) (fromIntegral -> tot) = ContT $ \success -> do +loadArg p (fromIntegral -> i) (fromIntegral -> tot) = Program $ ContT $ \success -> do try (basicFromPy p) >>= \case Right a -> success a Left BadPyType -> oops @@ -501,7 +501,7 @@ loadArgFastcall -> Int64 -- ^ Total number of arguments -> Program (Ptr PyObject) a loadArgFastcall p_arr i tot = do - p <- liftIO $ peekElemOff p_arr i + p <- progIO $ peekElemOff p_arr i loadArg p i tot raiseBadNArgs :: CInt -> Int64 -> Py (Ptr PyObject) diff --git a/src/Python/Internal/Eval.hs b/src/Python/Internal/Eval.hs index ee3ef7a..b3d20ed 100644 --- a/src/Python/Internal/Eval.hs +++ b/src/Python/Internal/Eval.hs @@ -35,6 +35,7 @@ module Python.Internal.Eval import Control.Concurrent import Control.Concurrent.STM +import Control.Exception (interruptible) import Control.Monad import Control.Monad.Catch import Control.Monad.IO.Class @@ -551,7 +552,8 @@ dropGIL action = do -- PyGILState_STATE is defined as enum. Let hope it will stay -- this way. st <- Py [CU.exp| PyThreadState* { PyEval_SaveThread() } |] - Py $ action `finally` [CU.exp| void { PyEval_RestoreThread($(PyThreadState *st)) } |] + Py $ interruptible action + `finally` [CU.exp| void { PyEval_RestoreThread($(PyThreadState *st)) } |] ---------------------------------------------------------------- @@ -571,11 +573,11 @@ convertHaskell2Py err = Py $ do -- | Convert python exception to haskell exception. Should only be -- called if there's unhandled python exception. Clears exception. convertPy2Haskell :: Py PyError -convertPy2Haskell = evalContT $ do +convertPy2Haskell = runProgram $ do p_errors <- withPyAllocaArray @(Ptr PyObject) 3 p_len <- withPyAlloca @CLong -- Fetch error indicator - (p_type, p_value) <- liftIO $ do + (p_type, p_value) <- progIO $ do [CU.block| void { PyObject **p = $(PyObject** p_errors); PyErr_Fetch(p, p+1, p+2); @@ -586,7 +588,7 @@ convertPy2Haskell = evalContT $ do pure (p_type,p_value) -- Convert exception type and value to strings. let pythonStr p = do - p_str <- liftIO [CU.block| PyObject* { + p_str <- progIO [CU.block| PyObject* { PyObject *s = PyObject_Str($(PyObject *p)); if( PyErr_Occurred() ) { PyErr_Clear(); @@ -610,7 +612,7 @@ convertPy2Haskell = evalContT $ do case c_str of NULL -> pure "" _ -> peekCString c_str - liftIO $ PyError <$> toString s_type <*> toString s_value + progIO $ PyError <$> toString s_type <*> toString s_value -- | Throw python error as haskell exception if it's raised. diff --git a/src/Python/Internal/EvalQQ.hs b/src/Python/Internal/EvalQQ.hs index d6a5954..187ad09 100644 --- a/src/Python/Internal/EvalQQ.hs +++ b/src/Python/Internal/EvalQQ.hs @@ -13,8 +13,6 @@ module Python.Internal.EvalQQ ) where import Control.Monad.IO.Class -import Control.Monad.Trans.Class -import Control.Monad.Trans.Cont import Data.Bits import Data.Char import Data.List (intercalate) @@ -55,9 +53,9 @@ pyExecExpr -> Ptr PyObject -- ^ Locals -> String -- ^ Python source code -> Py () -pyExecExpr p_globals p_locals src = evalContT $ do +pyExecExpr p_globals p_locals src = runProgram $ do p_py <- withPyCString src - lift $ do + progPy $ do Py [C.block| void { PyObject* globals = $(PyObject* p_globals); PyObject* locals = $(PyObject* p_locals); @@ -79,9 +77,9 @@ pyEvalExpr -> Ptr PyObject -- ^ Locals -> String -- ^ Python source code -> Py PyObject -pyEvalExpr p_globals p_locals src = evalContT $ do +pyEvalExpr p_globals p_locals src = runProgram $ do p_py <- withPyCString src - lift $ do + progPy $ do p_res <- Py [C.block| PyObject* { PyObject* globals = $(PyObject* p_globals); PyObject* locals = $(PyObject* p_locals); @@ -106,23 +104,23 @@ evaluatorPymain getSource = do pyExecExpr p_main p_main src evaluatorPy_ :: (Ptr PyObject -> Py String) -> Py () -evaluatorPy_ getSource = evalContT $ do - p_globals <- lift basicMainDict - p_locals <- takeOwnership =<< lift basicNewDict - lift $ pyExecExpr p_globals p_locals =<< getSource p_locals +evaluatorPy_ getSource = runProgram $ do + p_globals <- progPy basicMainDict + p_locals <- takeOwnership =<< progPy basicNewDict + progPy $ pyExecExpr p_globals p_locals =<< getSource p_locals evaluatorPye :: (Ptr PyObject -> Py String) -> Py PyObject -evaluatorPye getSource = evalContT $ do - p_globals <- lift basicMainDict - p_locals <- takeOwnership =<< lift basicNewDict - lift $ pyEvalExpr p_globals p_locals =<< getSource p_locals +evaluatorPye getSource = runProgram $ do + p_globals <- progPy basicMainDict + p_locals <- takeOwnership =<< progPy basicNewDict + progPy $ pyEvalExpr p_globals p_locals =<< getSource p_locals evaluatorPyf :: (Ptr PyObject -> Py String) -> Py PyObject -evaluatorPyf getSource = evalContT $ do - p_globals <- lift basicMainDict - p_locals <- takeOwnership =<< lift basicNewDict - p_kwargs <- takeOwnership =<< lift basicNewDict - lift $ do +evaluatorPyf getSource = runProgram $ do + p_globals <- progPy basicMainDict + p_locals <- takeOwnership =<< progPy basicNewDict + p_kwargs <- takeOwnership =<< progPy basicNewDict + progPy $ do -- Create function in p_locals pyExecExpr p_globals p_locals =<< getSource p_kwargs -- Look up function @@ -134,10 +132,10 @@ evaluatorPyf getSource = evalContT $ do basicBindInDict :: ToPy a => String -> a -> Ptr PyObject -> Py () -basicBindInDict name a p_dict = evalContT $ do +basicBindInDict name a p_dict = runProgram $ do p_key <- withPyCString name - p_obj <- takeOwnership =<< lift (throwOnNULL =<< basicToPy a) - lift $ do + p_obj <- takeOwnership =<< progPy (throwOnNULL =<< basicToPy a) + progPy $ do r <- Py [C.block| int { PyObject* p_obj = $(PyObject* p_obj); return PyDict_SetItemString($(PyObject* p_dict), $(char* p_key), p_obj); diff --git a/src/Python/Internal/Program.hs b/src/Python/Internal/Program.hs index 9db9780..9499a33 100644 --- a/src/Python/Internal/Program.hs +++ b/src/Python/Internal/Program.hs @@ -1,6 +1,9 @@ -- | module Python.Internal.Program - ( Program + ( Program(..) + , runProgram + , progPy + , progIO -- * Control flow , abort , abortM @@ -17,6 +20,7 @@ module Python.Internal.Program ) where import Control.Monad.Trans.Cont +import Control.Monad.Trans.Class import Control.Monad.Catch import Data.Coerce import Foreign.Ptr @@ -31,22 +35,32 @@ import Python.Internal.Util import Python.Internal.CAPI --- | Internally we usually wrap 'Py' into 'ContT' in order get early --- exit and avoid building ladder of -type Program r a = ContT r Py a +-- | This monad wraps 'Py' into 'ContT' in order get early exit, +-- applying @finally@ while avoiding building huge ladders. +newtype Program r a = Program (ContT r Py a) + deriving newtype (Functor, Applicative, Monad) +runProgram :: Program a a -> Py a +runProgram (Program m) = evalContT m + +-- | Does not change masking state +progIO :: IO a -> Program r a +progIO = Program . lift . pyIO + +progPy :: Py a -> Program r a +progPy = Program . lift -- | Early exit from continuation monad. -abort :: Monad m => r -> ContT r m a -abort r = ContT $ \_ -> pure r +abort :: r -> Program r a +abort r = Program $ ContT $ \_ -> pure r -- | Early exit from continuation monad. -abortM :: Monad m => m r -> ContT r m a -abortM m = ContT $ \_ -> m +abortM :: Py r -> Program r a +abortM m = Program $ ContT $ \_ -> m -- | If result of computation is NULL return NULL immediately. checkNull :: Py (Ptr a) -> Program (Ptr a) (Ptr a) -checkNull action = ContT $ \cnt -> action >>= \case +checkNull action = Program $ ContT $ \cnt -> action >>= \case NULL -> pure nullPtr p -> cnt p @@ -54,17 +68,17 @@ checkNull action = ContT $ \cnt -> action >>= \case finallyProg :: Py b -- ^ Finalizer -> Program r () -finallyProg fini = ContT $ \c -> c () `finally` fini +finallyProg fini = Program $ ContT $ \c -> c () `finally` fini -- | Evaluate finalizer if exception is thrown. onExceptionProg :: Py b -- ^ Finalizer -> Program r () -onExceptionProg fini = ContT $ \c -> c () `onException` fini +onExceptionProg fini = Program $ ContT $ \c -> c () `onException` fini -- | Decrement reference counter at end of ContT block takeOwnership :: Ptr PyObject -> Program r (Ptr PyObject) -takeOwnership p = ContT $ \c -> c p `finally` decref p +takeOwnership p = Program $ ContT $ \c -> c p `finally` decref p ---------------------------------------------------------------- From 18221cf9c9eba289e7d859d3e869d599a1d94a0f Mon Sep 17 00:00:00 2001 From: Alexey Khudyakov Date: Thu, 9 Jan 2025 22:46:24 +0300 Subject: [PATCH 9/9] Add From,To intances for sets --- inline-python.cabal | 1 + src/Python/Inline/Literal.hs | 58 +++++++++++++++++++++++++++--------- src/Python/Internal/CAPI.hs | 13 ++++++++ test/TST/Roundtrip.hs | 2 ++ test/TST/ToPy.hs | 9 ++++++ 5 files changed, 69 insertions(+), 14 deletions(-) diff --git a/inline-python.cabal b/inline-python.cabal index 2215c25..970e0c7 100644 --- a/inline-python.cabal +++ b/inline-python.cabal @@ -89,6 +89,7 @@ library test , tasty-hunit >=0.10 , tasty-quickcheck >=0.10 , exceptions + , containers hs-source-dirs: test Exposed-modules: TST.Run diff --git a/src/Python/Inline/Literal.hs b/src/Python/Inline/Literal.hs index 38e001f..201b6bc 100644 --- a/src/Python/Inline/Literal.hs +++ b/src/Python/Inline/Literal.hs @@ -14,13 +14,12 @@ module Python.Inline.Literal import Control.Monad import Control.Monad.Catch -import Control.Monad.IO.Class -import Control.Monad.Trans.Class import Control.Monad.Trans.Cont import Data.Bits import Data.Char import Data.Int import Data.Word +import Data.Set qualified as Set import Foreign.Ptr import Foreign.C.Types import Foreign.Storable @@ -58,13 +57,13 @@ class ToPy a where basicListToPy :: [a] -> Py (Ptr PyObject) basicListToPy xs = runProgram $ do let n = fromIntegral $ length xs :: CLLong - p_list <- checkNull (Py [CU.exp| PyObject* { PyList_New($(long long n)) } |]) - onExceptionProg $ decref p_list - let loop !_ [] = pure p_list + p_list <- takeOwnership =<< checkNull (Py [CU.exp| PyObject* { PyList_New($(long long n)) } |]) + let loop !_ [] = p_list <$ incref p_list loop i (a:as) = basicToPy a >>= \case NULL -> pure nullPtr p_a -> do - pyIO [CU.exp| void { PyList_SET_ITEM($(PyObject* p_list), $(long long i), $(PyObject* p_a)) } |] + -- NOTE: PyList_SET_ITEM steals reference + Py [CU.exp| void { PyList_SET_ITEM($(PyObject* p_list), $(long long i), $(PyObject* p_a)) } |] loop (i+1) as progPy $ loop 0 xs @@ -383,14 +382,45 @@ instance (FromPy a) => FromPy [a] where } |] when (nullPtr == p_iter) $ throwM BadPyType -- - let loop f = do - p <- Py [C.exp| PyObject* { PyIter_Next($(PyObject* p_iter)) } |] - checkThrowPyError - case p of - NULL -> pure f - _ -> do a <- basicFromPy p `finally` decref p - loop (f . (a:)) - ($ []) <$> loop id + f <- foldPyIterable p_iter + (\f p -> do a <- basicFromPy p + pure (f . (a:))) + id + pure $ f [] + +instance (ToPy a, Ord a) => ToPy (Set.Set a) where + basicToPy set = runProgram $ do + p_set <- takeOwnership =<< checkNull basicNewSet + progPy $ do + let loop [] = p_set <$ incref p_set + loop (x:xs) = basicToPy x >>= \case + NULL -> pure NULL + p_a -> Py [C.exp| int { PySet_Add($(PyObject *p_set), $(PyObject *p_a)) }|] >>= \case + 0 -> decref p_a >> loop xs + _ -> mustThrowPyError + loop $ Set.toList set + +instance (FromPy a, Ord a) => FromPy (Set.Set a) where + basicFromPy p_set = basicGetIter p_set >>= \case + NULL -> do Py [C.exp| void { PyErr_Clear() } |] + throwM BadPyType + p_iter -> foldPyIterable p_iter + (\s p -> do a <- basicFromPy p + pure $! Set.insert a s) + Set.empty + +-- | Fold over iterable. Function takes ownership over iterator. +foldPyIterable + :: Ptr PyObject -- ^ Python iterator (not checked) + -> (a -> Ptr PyObject -> Py a) -- ^ Step function. It takes borrowed pointer. + -> a -- ^ Initial value + -> Py a +foldPyIterable p_iter step a0 + = loop a0 `finally` decref p_iter + where + loop a = basicIterNext p_iter >>= \case + NULL -> a <$ checkThrowPyError + p -> loop =<< (step a p `finally` decref p) ---------------------------------------------------------------- diff --git a/src/Python/Internal/CAPI.hs b/src/Python/Internal/CAPI.hs index 0612ee7..a3d6eeb 100644 --- a/src/Python/Internal/CAPI.hs +++ b/src/Python/Internal/CAPI.hs @@ -7,6 +7,9 @@ module Python.Internal.CAPI , incref -- * Simple wrappers , basicNewDict + , basicNewSet + , basicGetIter + , basicIterNext , basicCallKwdOnly ) where @@ -32,6 +35,16 @@ incref p = Py [CU.exp| void { Py_INCREF($(PyObject* p)) } |] basicNewDict :: Py (Ptr PyObject) basicNewDict = Py [CU.exp| PyObject* { PyDict_New() } |] +basicNewSet :: Py (Ptr PyObject) +basicNewSet = Py [CU.exp| PyObject* { PySet_New(NULL) } |] + +basicGetIter :: Ptr PyObject -> Py (Ptr PyObject) +basicGetIter p = Py [CU.exp| PyObject* { PyObject_GetIter( $(PyObject *p)) } |] + +basicIterNext :: Ptr PyObject -> Py (Ptr PyObject) +basicIterNext p = Py [C.exp| PyObject* { PyIter_Next($(PyObject* p)) } |] + + -- | Call python function using only keyword arguments basicCallKwdOnly :: Ptr PyObject -- ^ Function object diff --git a/test/TST/Roundtrip.hs b/test/TST/Roundtrip.hs index 596e1fe..c7cfd4f 100644 --- a/test/TST/Roundtrip.hs +++ b/test/TST/Roundtrip.hs @@ -5,6 +5,7 @@ module TST.Roundtrip (tests) where import Data.Int import Data.Word import Data.Typeable +import Data.Set (Set) import Foreign.C.Types import Test.Tasty @@ -51,6 +52,7 @@ tests = testGroup "Roundtrip" , testRoundtrip @(Int,Int,Int,Char) , testRoundtrip @[Int] , testRoundtrip @[[Int]] + , testRoundtrip @(Set Int) -- , testRoundtrip @String -- Trips on zero byte as it should ] , testGroup "OutOfRange" diff --git a/test/TST/ToPy.hs b/test/TST/ToPy.hs index aba6800..a712fb7 100644 --- a/test/TST/ToPy.hs +++ b/test/TST/ToPy.hs @@ -1,10 +1,13 @@ -- | module TST.ToPy (tests) where +import Data.Set qualified as Set import Test.Tasty import Test.Tasty.HUnit import Python.Inline import Python.Inline.QQ +import TST.Util + tests :: TestTree tests = testGroup "ToPy" @@ -30,4 +33,10 @@ tests = testGroup "ToPy" , testCase "list" $ runPy $ let x = [1 .. 5::Int] in [py_| assert x_hs == [1,2,3,4,5] |] + , testCase "set" $ runPy $ + let x = Set.fromList [1, 5, 3::Int] + in [py_| assert x_hs == {1,3,5} |] + , testCase "set unhashable" $ runPy $ + let x = Set.fromList [[1], [5], [3::Int]] + in throwsPy [py_| assert x_hs == {1,3,5} |] ]