From 48b5ee701c96368cadf243cb2e14100e88067478 Mon Sep 17 00:00:00 2001 From: Alexey Khudyakov Date: Thu, 3 Sep 2026 16:57:26 +0300 Subject: [PATCH 1/9] Convert AsyncCancelled python exception to PyAsyncCancelled --- src/Python/Internal/Eval.hs | 44 ++++++++++++++++--------------------- test/TST/Module.hs | 14 +++++++++++- 2 files changed, 32 insertions(+), 26 deletions(-) diff --git a/src/Python/Internal/Eval.hs b/src/Python/Internal/Eval.hs index a4dded7..0ceb194 100644 --- a/src/Python/Internal/Eval.hs +++ b/src/Python/Internal/Eval.hs @@ -777,38 +777,32 @@ convertHaskell2Py err = Py $ do -- | Convert python exception to haskell exception. Should only be -- called if there's unhandled python exception. Clears exception. -convertPy2Haskell :: Py PyException +convertPy2Haskell :: Py SomeException convertPy2Haskell = runProgram $ do p_errors <- withPyAllocaArray @(Ptr PyObject) 3 - -- Fetch error indicator - (p_type, p_value) <- progIO $ do - [CU.block| void { - PyObject **p = $(PyObject** p_errors); - PyErr_Fetch(p, p+1, p+2); - }|] - p_type <- peekElemOff p_errors 0 - -- NOTE: When we set exception using PyThreadState_SetAsyncExc - -- this field remains NULL on python<=3.11. In this case we - -- assume it's our AsyncCancelled: - p_value <- peekElemOff p_errors 1 >>= \case - NULL -> [CU.block| PyObject* { - PyObject *err_class = inline_py_AsyncCancelled(); - PyObject *tuple = PyTuple_New(0); - PyObject *err = PyObject_Call(err_class, tuple, NULL); - Py_DECREF(tuple); - return err; - } |] - p -> pure p - -- Traceback is not used ATM - pure (p_type,p_value) + -- Fetch error information + progIO [CU.block| void { + PyObject **p = $(PyObject** p_errors); + PyErr_Fetch(p, p+1, p+2); + }|] + -- Fetch exception type. Convert exceptions of proper + p_type <- progIO $ peekElemOff p_errors 0 + ty_async_cancelled <- progIO $ [CU.exp| PyObject* { inline_py_AsyncCancelled() } |] + when (p_type == ty_async_cancelled) $ + abort $ SomeException PyAsyncCancelled -- Convert exception type and value to strings. + -- + -- NOTE: When we set exception using PyThreadState_SetAsyncExc this + -- field remains NULL on python<=3.11. But that should cause + -- no problem since we handle this case directly above. progPy $ do + p_value <- Py $ peekElemOff p_errors 1 s_type <- pyobjectStrAsHask p_type s_value <- pyobjectStrAsHask p_value incref p_value exc <- newPyObject p_value let bad_str = "__str__ call failed" - pure $ PyException + pure $ SomeException $ PyError $ PyException { ty = fromMaybe bad_str s_type , str = fromMaybe bad_str s_value , exception = exc @@ -819,7 +813,7 @@ checkThrowPyError :: Py () checkThrowPyError = Py [CU.exp| PyObject* { PyErr_Occurred() } |] >>= \case NULL -> pure () - _ -> throwM . PyError =<< convertPy2Haskell + _ -> throwM =<< convertPy2Haskell -- | Throw python error as haskell exception if it's raised. If it's -- not that internal error. Another exception will be raised @@ -827,7 +821,7 @@ mustThrowPyError :: Py a mustThrowPyError = Py [CU.exp| PyObject* { PyErr_Occurred() } |] >>= \case NULL -> error $ "mustThrowPyError: no python exception raised." - _ -> throwM . PyError =<< convertPy2Haskell + _ -> throwM =<< convertPy2Haskell -- | Calls mustThrowPyError if pointer is null or returns it unchanged throwOnNULL :: Ptr PyObject -> Py (Ptr PyObject) diff --git a/test/TST/Module.hs b/test/TST/Module.hs index 4f5b208..8f6674f 100644 --- a/test/TST/Module.hs +++ b/test/TST/Module.hs @@ -1,11 +1,13 @@ -- | module TST.Module where +import Control.Exception +import Data.Typeable import Test.Tasty import Test.Tasty.HUnit import Python.Inline import Python.Inline.QQ - +import Python.Inline.Async tests :: TestTree tests = testGroup "Builtin module" @@ -26,4 +28,14 @@ tests = testGroup "Builtin module" assert ty is inline_python.AsyncCancelled assert isinstance(err, inline_python.AsyncCancelled) |] + , testCase "AsyncCancelled is converted to PyAsyncCancelled" $ do + r :: Either SomeException () <- try $ runPy [py_| + import inline_python + raise inline_python.AsyncCancelled() + |] + case r of + Right () -> error "No exception" + Left (SomeException e) + | Just PyAsyncCancelled <- cast e -> pure () + | otherwise -> throwIO e ] From 2c7ee3cddb469a5b7612212c9aa48955e2351140 Mon Sep 17 00:00:00 2001 From: Alexey Khudyakov Date: Thu, 3 Sep 2026 23:17:02 +0300 Subject: [PATCH 2/9] We own reference returned by PyErr_Fetch No need to increment counter further --- src/Python/Internal/Eval.hs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Python/Internal/Eval.hs b/src/Python/Internal/Eval.hs index 0ceb194..26e5393 100644 --- a/src/Python/Internal/Eval.hs +++ b/src/Python/Internal/Eval.hs @@ -799,7 +799,6 @@ convertPy2Haskell = runProgram $ do p_value <- Py $ peekElemOff p_errors 1 s_type <- pyobjectStrAsHask p_type s_value <- pyobjectStrAsHask p_value - incref p_value exc <- newPyObject p_value let bad_str = "__str__ call failed" pure $ SomeException $ PyError $ PyException From ce523a4f7f174e6eba5c263c0d22c414dd7a3cd8 Mon Sep 17 00:00:00 2001 From: Alexey Khudyakov Date: Thu, 3 Sep 2026 23:30:55 +0300 Subject: [PATCH 3/9] Add xdecref wrapper --- src/Python/Internal/CAPI.hs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Python/Internal/CAPI.hs b/src/Python/Internal/CAPI.hs index 273400b..0f8c412 100644 --- a/src/Python/Internal/CAPI.hs +++ b/src/Python/Internal/CAPI.hs @@ -4,6 +4,7 @@ -- Thin wrappers over C API module Python.Internal.CAPI ( decref + , xdecref , incref -- * Simple wrappers , basicNewDict @@ -29,6 +30,9 @@ C.include "" decref :: Ptr PyObject -> Py () decref p = Py [C.exp| void { Py_DECREF($(PyObject* p)) } |] +xdecref :: Ptr PyObject -> Py () +xdecref p = Py [C.exp| void { Py_XDECREF($(PyObject* p)) } |] + incref :: Ptr PyObject -> Py () incref p = Py [CU.exp| void { Py_INCREF($(PyObject* p)) } |] From eab615d62c6b7bdb7875254942fa4f82ad7de615 Mon Sep 17 00:00:00 2001 From: Alexey Khudyakov Date: Thu, 3 Sep 2026 23:31:09 +0300 Subject: [PATCH 4/9] Add another combinatro for Program --- src/Python/Internal/Program.hs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Python/Internal/Program.hs b/src/Python/Internal/Program.hs index 7c713a0..397468f 100644 --- a/src/Python/Internal/Program.hs +++ b/src/Python/Internal/Program.hs @@ -6,6 +6,7 @@ module Python.Internal.Program , runProgram , progPy , progIO + , progPyBracket , progIOBracket -- * Control flow , abort @@ -67,6 +68,9 @@ progPy = Program . lift progIOBracket :: ((a -> IO r) -> IO r) -> Program r a progIOBracket = coerce +progPyBracket :: ((a -> Py r) -> Py r) -> Program r a +progPyBracket = coerce + -- | Early exit from continuation monad. abort :: r -> Program r a abort r = Program $ ContT $ \_ -> pure r From c7d7ba65ea8fc7f221b439da4b71156cd03744f5 Mon Sep 17 00:00:00 2001 From: Alexey Khudyakov Date: Thu, 3 Sep 2026 23:38:10 +0300 Subject: [PATCH 5/9] Add wrapper for rethrowing haskell exceptions It sometimes work. So I think general idea is sound --- cbits/python.c | 71 +++++++++++++++++++++++++++++++++++++ include/inline-python.h | 12 ++++++- src/Python/Internal/Eval.hs | 40 +++++++++++++-------- test/TST/Module.hs | 19 ++++++++++ 4 files changed, 126 insertions(+), 16 deletions(-) diff --git a/cbits/python.c b/cbits/python.c index 5530be7..080fac0 100644 --- a/cbits/python.c +++ b/cbits/python.c @@ -254,6 +254,71 @@ PyObject* inline_py_AsyncCancelled() { return AsyncCancelled; } + +typedef struct { + PyBaseExceptionObject obj; + void *exception_stableptr; +} HaskellError; + +static void haskell_error_dealloc(PyObject *op) { + HaskellError *self = (HaskellError*) op; + // FIXME: I should free stable ptr here + printf("haskell_error_dealloc\n"); + Py_TYPE(self)->tp_free(self); +} + +static PyTypeObject HaskellError_Type = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "inline_python.HaskellError", + .tp_basicsize = sizeof(HaskellError), + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_DISALLOW_INSTANTIATION, + .tp_doc = PyDoc_STR("Wrapper for a haskell exception object"), + .tp_dealloc = haskell_error_dealloc, +}; + + +PyObject* inline_py_HaskellError() { + static int initialized = 0; + if( 0 == initialized ) { + if( PyType_Ready(&HaskellError_Type) != 0) { + // It should success always and we don't have any reasonable + // way of handing error. + exit(1); + } + initialized = 1; + } + return (PyObject*)&HaskellError_Type; +} + +PyObject* inline_py_HaskellError_create(void* exc_ptr) { + PyTypeObject *base = HaskellError_Type.tp_base; + PyObject *args = PyTuple_New(0); + // Call __new__ + PyObject *obj = base->tp_new(&HaskellError_Type, args, NULL); + if( !obj ) { + goto err; + } + // Call __init__ + if( 0 != base->tp_init(obj, args, NULL) ) { + goto err; + } + // Set custom fields + HaskellError* h_err = (HaskellError*)obj; + h_err->exception_stableptr = exc_ptr; + Py_DECREF(args); + return obj; +err: + Py_DECREF(args); + return NULL; +} + +void* inline_py_HaskellError_get_stableptr(PyObject* err) { + HaskellError *h_err = (HaskellError*) err; + return h_err->exception_stableptr; +} + + + static PyMethodDef inline_python_methods[] = { {NULL, NULL, 0, NULL} }; @@ -269,6 +334,12 @@ static int inline_python_module_exec(PyObject *m) { if (PyModule_AddObjectRef(m, "AsyncCancelled", inline_py_AsyncCancelled()) < 0) { return -1; } + // + HaskellError_Type.tp_base = (PyTypeObject*)PyExc_Exception; + HaskellError_Type.tp_new = NULL; + if (PyModule_AddObjectRef(m, "HaskellError", inline_py_HaskellError()) < 0) { + return -1; + } return 0; } diff --git a/include/inline-python.h b/include/inline-python.h index b3ec015..5a0819b 100644 --- a/include/inline-python.h +++ b/include/inline-python.h @@ -107,5 +107,15 @@ void* inline_py_get_state(void); PyMODINIT_FUNC PyInit_inline_python(void); -// Obtain class for async exception +// Obtain type for async exception. PyObject* inline_py_AsyncCancelled(); + +// Obtain type for wrapper for haskell exceptions. +PyObject* inline_py_HaskellError(); + +// Create python object wrapping haskell exception +PyObject* inline_py_HaskellError_create(void* exc_ptr); + +// Get StablePtr held by exception. Python object must be of type +// HaskellError +void* inline_py_HaskellError_get_stableptr(PyObject* err); diff --git a/src/Python/Internal/Eval.hs b/src/Python/Internal/Eval.hs index 26e5393..ddcfb67 100644 --- a/src/Python/Internal/Eval.hs +++ b/src/Python/Internal/Eval.hs @@ -68,7 +68,6 @@ import Foreign.Ptr import Foreign.ForeignPtr import Foreign.StablePtr import Foreign.C.Types -import Foreign.C.String import Foreign.Marshal.Array import Foreign.Storable import System.Environment @@ -769,11 +768,14 @@ getPyThreadID = PyThreadId <$> [CU.exp| uint64_t { PyThread_get_thread_ident() } -- NULL. convertHaskell2Py :: SomeException -> Py (Ptr PyObject) convertHaskell2Py err = Py $ do - withCString ("Haskell exception: "++show err) $ \p_err -> do - [C.block| PyObject* { - PyErr_SetString(PyExc_RuntimeError, $(char *p_err)); - return NULL; - } |] + s_ptr <- newStablePtr err + let ptr = castStablePtrToPtr s_ptr + [C.block| PyObject* { + PyObject* exc = inline_py_HaskellError_create($(void* ptr)); + PyErr_SetObject(inline_py_HaskellError(), exc); + Py_DECREF(exc); + return NULL; + } |] -- | Convert python exception to haskell exception. Should only be -- called if there's unhandled python exception. Clears exception. @@ -785,20 +787,28 @@ convertPy2Haskell = runProgram $ do PyObject **p = $(PyObject** p_errors); PyErr_Fetch(p, p+1, p+2); }|] - -- Fetch exception type. Convert exceptions of proper - p_type <- progIO $ peekElemOff p_errors 0 - ty_async_cancelled <- progIO $ [CU.exp| PyObject* { inline_py_AsyncCancelled() } |] - when (p_type == ty_async_cancelled) $ - abort $ SomeException PyAsyncCancelled - -- Convert exception type and value to strings. + -- Fetch exception type. -- -- NOTE: When we set exception using PyThreadState_SetAsyncExc this - -- field remains NULL on python<=3.11. But that should cause - -- no problem since we handle this case directly above. + -- field remains NULL on python<=3.11. Thus we must use xdecref + p_type <- progPyBracket $ (Py $ peekElemOff p_errors 0) `bracket` decref + p_value <- progPyBracket $ (Py $ peekElemOff p_errors 1) `bracket` xdecref + _trace <- progPyBracket $ (Py $ peekElemOff p_errors 2) `bracket` xdecref + -- Should we convert to PyAsyncCancelled? + ty_async_cancelled <- progIO [CU.exp| PyObject* { inline_py_AsyncCancelled() } |] + when (p_type == ty_async_cancelled) $ do + abort $ SomeException PyAsyncCancelled + -- Should we convert to haskell exception? + ty_hask_err <- progIO [CU.exp| PyObject* { inline_py_HaskellError() } |] + when (p_type == ty_hask_err) $ do + s_ptr <- progIO [CU.exp| void* { inline_py_HaskellError_get_stableptr($(PyObject* p_value)) } |] + err <- progIO $ deRefStablePtr $ castPtrToStablePtr s_ptr + abort err + -- Convert any other python exception progPy $ do - p_value <- Py $ peekElemOff p_errors 1 s_type <- pyobjectStrAsHask p_type s_value <- pyobjectStrAsHask p_value + incref p_value exc <- newPyObject p_value let bad_str = "__str__ call failed" pure $ SomeException $ PyError $ PyException diff --git a/test/TST/Module.hs b/test/TST/Module.hs index 8f6674f..fd3c7a0 100644 --- a/test/TST/Module.hs +++ b/test/TST/Module.hs @@ -38,4 +38,23 @@ tests = testGroup "Builtin module" Left (SomeException e) | Just PyAsyncCancelled <- cast e -> pure () | otherwise -> throwIO e + , testCase "Haskell exception are converted 1" $ do + let foo :: IO Int + foo = return $! 1 `div` 0 + runPy [py_| + try: + foo_hs() + except Exception as e: + pass + del e + #print(e) + #print(type(e)) + |] + , testCase "Haskell exception are converted 2" $ do + let foo :: IO Int + foo = return $! 1 `div` 0 + let handler DivideByZero = pure () + handler e = throwIO e + runPy [py_| foo_hs() |] `catch` handler + ] From 9c93e8c2ed3d1605e45722a806ae91db64658adc Mon Sep 17 00:00:00 2001 From: Alexey Khudyakov Date: Fri, 4 Sep 2026 00:03:34 +0300 Subject: [PATCH 6/9] We simply need to make sure that module inline_python is imported --- src/Python/Internal/Eval.hs | 6 +++++- test/TST/Callbacks.hs | 5 +++-- test/TST/Module.hs | 5 ++++- test/TST/Util.hs | 7 +++++++ 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/Python/Internal/Eval.hs b/src/Python/Internal/Eval.hs index ddcfb67..8e676bf 100644 --- a/src/Python/Internal/Eval.hs +++ b/src/Python/Internal/Eval.hs @@ -444,8 +444,12 @@ doInitializePythonIO = do PyErr_Clear(); } } - // Initialize internals + // Initialize internals. We also need to import module with our internals inline_py_initialize(); + PyObject *inline_python = PyImport_ImportModule("inline_python"); + if( PyErr_Occurred() ) { + PyErr_Clear(); + } // Release GIL so other threads may take it PyEval_SaveThread(); return 0; diff --git a/test/TST/Callbacks.hs b/test/TST/Callbacks.hs index 04488e3..def121f 100644 --- a/test/TST/Callbacks.hs +++ b/test/TST/Callbacks.hs @@ -1,6 +1,7 @@ -- | module TST.Callbacks (tests) where +import Control.Exception import Test.Tasty import Test.Tasty.HUnit import Python.Inline @@ -68,11 +69,11 @@ tests = testGroup "Callbacks" , testCase "Haskell exception in callback(arity=1)" $ runPy $ do let foo :: Int -> IO Int foo y = pure $ 10 `div` y - throwsPy [py_| foo_hs(0) |] + throwsException DivideByZero [py_| foo_hs(0) |] , testCase "Haskell exception in callback(arity=2)" $ runPy $ do let foo :: Int -> Int -> IO Int foo x y = pure $ x `div` y - throwsPy [py_| foo_hs(1, 0) |] + throwsException DivideByZero [py_| foo_hs(1, 0) |] ---------------------------------------- , testCase "Call python in callback (arity=1)" $ runPy $ do let foo :: Int -> IO Int diff --git a/test/TST/Module.hs b/test/TST/Module.hs index fd3c7a0..d04fe52 100644 --- a/test/TST/Module.hs +++ b/test/TST/Module.hs @@ -12,10 +12,13 @@ import Python.Inline.Async tests :: TestTree tests = testGroup "Builtin module" [ testCase "Module exists" $ runPy [py_| import inline_python |] - , testCase "AsyncCancelled" $ runPy [py_| + , testCase "Exceptions" $ runPy [py_| import inline_python assert issubclass(inline_python.AsyncCancelled, BaseException) assert not issubclass(inline_python.AsyncCancelled, Exception) + + assert issubclass(inline_python.HaskellError, Exception), "HaskellError is Exception" + assert issubclass(inline_python.HaskellError, BaseException), "HaskellError is BaseException" |] -- We want to check that inline_python types are stable under -- reload using importlib. diff --git a/test/TST/Util.hs b/test/TST/Util.hs index c4eb82e..ef5e154 100644 --- a/test/TST/Util.hs +++ b/test/TST/Util.hs @@ -3,6 +3,7 @@ module TST.Util where import Control.Monad.IO.Class import Control.Monad.Catch +import Data.Typeable import Test.Tasty.HUnit import Python.Inline @@ -11,6 +12,12 @@ throwsPy :: Py () -> Py () throwsPy io = (io >> liftIO (assertFailure "Evaluation should raise python exception")) `catch` (\(_::PyError) -> pure ()) +throwsException :: (Exception e, Eq e) => e -> Py () -> Py () +throwsException e0 io = (io >> liftIO (assertFailure "Evaluation should raise python exception")) + `catch` (\(SomeException e) -> case cast e of Just e' | e' == e0 -> pure () + Nothing -> throwM e + ) + throwsPyIO :: IO () -> IO () throwsPyIO io = (io >> assertFailure "Evaluation should raise python exception") `catch` (\(_::PyError) -> pure ()) From 0ee35e18472a9fb5e232a5f005d5d92d78be9965 Mon Sep 17 00:00:00 2001 From: Alexey Khudyakov Date: Fri, 4 Sep 2026 00:13:51 +0300 Subject: [PATCH 7/9] Free StablePtr from C side As GHC user guide suggests in very roundabout way in 6.17.2.8. Freeing many stable pointers efficiently. --- cbits/python.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cbits/python.c b/cbits/python.c index 080fac0..3e38387 100644 --- a/cbits/python.c +++ b/cbits/python.c @@ -262,8 +262,7 @@ typedef struct { static void haskell_error_dealloc(PyObject *op) { HaskellError *self = (HaskellError*) op; - // FIXME: I should free stable ptr here - printf("haskell_error_dealloc\n"); + hs_free_stable_ptr(self->exception_stableptr); Py_TYPE(self)->tp_free(self); } From 7da19341ff1a66474a98786680bcb92e76ff78f7 Mon Sep 17 00:00:00 2001 From: Alexey Khudyakov Date: Fri, 4 Sep 2026 11:30:10 +0300 Subject: [PATCH 8/9] Start writing changelog while I still rememeber what's going on --- ChangeLog.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/ChangeLog.md b/ChangeLog.md index 9b6603e..b645921 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,3 +1,13 @@ +0.3.0.0 [XXXX.XX.XX] +-------------------- +* Support for builds with `python3-config` +* Support for async +* `inline_python` module is now available. +* Now haskell exception from haskell callback in converted to + `inline_python.HaskellError` and is rethrown if it's not catched by python. +* Memory leak is fixed. Python exception object were never freed when exception + propagated to haskell side. + 0.2.1.0 [2026.01.13] ---------------- * `From/ToPy` instance for `Integer`&`Natural` added. @@ -21,7 +31,7 @@ 0.1.1 [2025.02.13] ------------------ * Number of deadlocks in `runPyInMain` fixed: - - It no longer deadlocks is exception is thrown + - It no longer deadlocks if exception is thrown - Nested calls no longer deadlock. - Calling it from python callback. * `ToPy` instance added for `Py b`, `a -> Py b`, `a1 -> a2 -> Py b` From e50017542cb57681cba47c34ebd2736a07917edb Mon Sep 17 00:00:00 2001 From: Alexey Khudyakov Date: Fri, 4 Sep 2026 12:07:52 +0300 Subject: [PATCH 9/9] Add proper __repr__ for HaskellError --- cbits/python.c | 15 ++++++++++++++- include/inline-python.h | 8 ++++++++ inline-python.cabal | 1 + src/Python/Internal/Eval.hs | 35 ++++++++++++++++++++++++++++++++--- test/TST/Callbacks.hs | 10 ++++++++++ test/TST/Module.hs | 19 ------------------- test/TST/Util.hs | 2 +- 7 files changed, 66 insertions(+), 24 deletions(-) diff --git a/cbits/python.c b/cbits/python.c index 3e38387..4972f13 100644 --- a/cbits/python.c +++ b/cbits/python.c @@ -246,6 +246,9 @@ void inline_py_initialize(void) { // ================================================================ // inline_python module +PyObject* (*inline_py_haskell_error_repr)(void*); +PyObject* (*inline_py_haskell_error_tyrepr)(void*); + PyObject* inline_py_AsyncCancelled() { static PyObject* AsyncCancelled = 0; if( AsyncCancelled == 0 ) { @@ -254,7 +257,6 @@ PyObject* inline_py_AsyncCancelled() { return AsyncCancelled; } - typedef struct { PyBaseExceptionObject obj; void *exception_stableptr; @@ -266,6 +268,16 @@ static void haskell_error_dealloc(PyObject *op) { Py_TYPE(self)->tp_free(self); } +static PyObject* haskell_error_repr(PyObject *op) { + HaskellError *self = (HaskellError*) op; + PyObject* exc_repr = inline_py_haskell_error_repr(self->exception_stableptr); + PyObject* exc_ty = inline_py_haskell_error_tyrepr(self->exception_stableptr); + PyObject* repr = PyUnicode_FromFormat("", exc_ty, exc_repr); + Py_DECREF(exc_repr); + Py_DECREF(exc_ty); + return repr; +} + static PyTypeObject HaskellError_Type = { PyVarObject_HEAD_INIT(NULL, 0) .tp_name = "inline_python.HaskellError", @@ -273,6 +285,7 @@ static PyTypeObject HaskellError_Type = { .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_DISALLOW_INSTANTIATION, .tp_doc = PyDoc_STR("Wrapper for a haskell exception object"), .tp_dealloc = haskell_error_dealloc, + .tp_repr = haskell_error_repr }; diff --git a/include/inline-python.h b/include/inline-python.h index 5a0819b..72bf70a 100644 --- a/include/inline-python.h +++ b/include/inline-python.h @@ -105,6 +105,14 @@ void* inline_py_get_state(void); // inline_python module // ================================================================ +// Function pointers to be set from haskell side + +// String representation of an exception +extern PyObject* (*inline_py_haskell_error_repr)(void*); +// String representation of exception type +extern PyObject* (*inline_py_haskell_error_tyrepr)(void*); + + PyMODINIT_FUNC PyInit_inline_python(void); // Obtain type for async exception. diff --git a/inline-python.cabal b/inline-python.cabal index f1949db..7c9385d 100644 --- a/inline-python.cabal +++ b/inline-python.cabal @@ -69,6 +69,7 @@ Library import: language Build-Depends: base >=4.15 && <5 , primitive >=0.6.2 + , deepseq >=1.4 , vector >=0.13.2 , containers >=0.5 , process diff --git a/src/Python/Internal/Eval.hs b/src/Python/Internal/Eval.hs index 8e676bf..2201153 100644 --- a/src/Python/Internal/Eval.hs +++ b/src/Python/Internal/Eval.hs @@ -55,7 +55,8 @@ module Python.Internal.Eval import Control.Concurrent import Control.Concurrent.STM -import Control.Exception (interruptible) +import Control.Exception (interruptible,evaluate) +import Control.DeepSeq import Control.Monad import Control.Monad.Catch import Control.Monad.IO.Class @@ -63,6 +64,7 @@ import Control.Monad.Trans.Cont import Data.Maybe import Data.Function import Data.ByteString.Unsafe qualified as BS +import Data.Typeable import Foreign.Concurrent qualified as GHC import Foreign.Ptr import Foreign.ForeignPtr @@ -399,13 +401,16 @@ doInitializePythonIO = do argv0 <- getProgName argv <- getArgs let n_argv = fromIntegral $ length argv + 1 - -- FIXME: For some reason sys.argv is initialized incorrectly. No - -- easy way to debug. Will do for now + hask_err_repr <- wrapReprFromStablePtr haskellErrorRepr + hask_err_tyrepr <- wrapReprFromStablePtr haskellErrorTyRepr r <- evalContT $ do p_argv0 <- ContT $ withWCString argv0 p_argv <- traverse (ContT . withWCString) argv ptr_argv <- ContT $ withArray (p_argv0 : p_argv) liftIO [C.block| int { + // Set global constants + inline_py_haskell_error_repr = $(PyObject* (*hask_err_repr)(void*)); + inline_py_haskell_error_tyrepr = $(PyObject* (*hask_err_tyrepr)(void*)); // Now fill config PyStatus status; PyConfig cfg; @@ -460,6 +465,30 @@ doInitializePythonIO = do } |] return $! r == 0 +haskellErrorRepr :: Ptr () -> IO (Ptr PyObject) +haskellErrorRepr ptr = unsafeRunPy $ runProgram $ do + SomeException err <- progIO $ deRefStablePtr $ castPtrToStablePtr ptr + -- We need to make sure that we evaluated string so that we won't + -- leak exceptions + repr <- progIO $ evaluate $ force $ show err + p_str <- withPyWCString repr + progIO [CU.exp| PyObject* { PyUnicode_FromWideChar($(wchar_t *p_str), -1) } |] + +haskellErrorTyRepr :: Ptr () -> IO (Ptr PyObject) +haskellErrorTyRepr ptr = unsafeRunPy $ runProgram $ do + SomeException err <- progIO $ deRefStablePtr $ castPtrToStablePtr ptr + -- We need to make sure that we evaluated string so that we won't + -- leak exceptions + repr <- progIO $ evaluate $ force $ show $ typeOf err + p_str <- withPyWCString repr + progIO [CU.exp| PyObject* { PyUnicode_FromWideChar($(wchar_t *p_str), -1) } |] + +type FunWrapper a = a -> IO (FunPtr a) + +foreign import ccall "wrapper" wrapReprFromStablePtr + :: FunWrapper (Ptr () -> IO (Ptr PyObject)) + + ---------------------------------------------------------------- -- Running Py monad diff --git a/test/TST/Callbacks.hs b/test/TST/Callbacks.hs index def121f..aadfdaf 100644 --- a/test/TST/Callbacks.hs +++ b/test/TST/Callbacks.hs @@ -74,6 +74,16 @@ tests = testGroup "Callbacks" let foo :: Int -> Int -> IO Int foo x y = pure $ x `div` y throwsException DivideByZero [py_| foo_hs(1, 0) |] + , testCase "Haskell exception has correct type" $ runPy $ do + let foo :: Int -> IO Int + foo y = pure $ 10 `div` y + [py_| + import inline_python + try: + foo_hs(0) + except inline_python.HaskellError as e: + assert repr(e) == "", repr(e) + |] ---------------------------------------- , testCase "Call python in callback (arity=1)" $ runPy $ do let foo :: Int -> IO Int diff --git a/test/TST/Module.hs b/test/TST/Module.hs index d04fe52..ecf6c03 100644 --- a/test/TST/Module.hs +++ b/test/TST/Module.hs @@ -41,23 +41,4 @@ tests = testGroup "Builtin module" Left (SomeException e) | Just PyAsyncCancelled <- cast e -> pure () | otherwise -> throwIO e - , testCase "Haskell exception are converted 1" $ do - let foo :: IO Int - foo = return $! 1 `div` 0 - runPy [py_| - try: - foo_hs() - except Exception as e: - pass - del e - #print(e) - #print(type(e)) - |] - , testCase "Haskell exception are converted 2" $ do - let foo :: IO Int - foo = return $! 1 `div` 0 - let handler DivideByZero = pure () - handler e = throwIO e - runPy [py_| foo_hs() |] `catch` handler - ] diff --git a/test/TST/Util.hs b/test/TST/Util.hs index ef5e154..5037ea7 100644 --- a/test/TST/Util.hs +++ b/test/TST/Util.hs @@ -15,7 +15,7 @@ throwsPy io = (io >> liftIO (assertFailure "Evaluation should raise python excep throwsException :: (Exception e, Eq e) => e -> Py () -> Py () throwsException e0 io = (io >> liftIO (assertFailure "Evaluation should raise python exception")) `catch` (\(SomeException e) -> case cast e of Just e' | e' == e0 -> pure () - Nothing -> throwM e + _ -> throwM e ) throwsPyIO :: IO () -> IO ()