Repository: cython/cython

Issue:
[ENH] Functions returning pointers does not propagate exception via NULL pointer by default
### Is your feature request related to a problem? Please describe.

In the cython3 we have changed the default logic to propagate exceptions by default. Consider following code:

```cython
from libc.stdlib cimport malloc

cdef void *safe_alloc(size_t size):
    mem =  malloc(size)
    if mem is NULL:
        raise MemoryError
    return mem

def main():
    cdef void *mem = NULL
    try:
        mem = safe_alloc(5)
    except MemoryError:
        print('Allocation of the memory failed.')
```

Calling `safe_alloc()` is by default causing calling `PyErr_Occurred()` after every call - see generated C code:
```c
__pyx_t_4 = __pyx_f_9exception_safe_alloc(5); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 12, __pyx_L3_error)
```
This can be dangerous when used together with `nogil` keyword - see notes in [1]



### Describe the solution you'd like.

To avoid unnecessary overhead we can propagate the exception via `NULL` value (same as `except? NULL`) when the function returns the pointer - the same is done when python object is returned - see notes in [1].


### Describe alternatives you've considered.

We can leave it as it is but I think it is unnecessary complication for the users.

### Additional context

[1] https://cython.readthedocs.io/en/latest/src/userguide/language_basics.html#error-return-values


Discussion:
This issue is simmilar to #5586

Resolving patch:
diff --git a/Cython/Compiler/PyrexTypes.py b/Cython/Compiler/PyrexTypes.py
--- a/Cython/Compiler/PyrexTypes.py
+++ b/Cython/Compiler/PyrexTypes.py
@@ -2812,6 +2812,7 @@ class CPtrType(CPointerBaseType):
 
     is_ptr = 1
     default_value = "0"
+    exception_value = "NULL"
 
     def __hash__(self):
         return hash(self.base_type) + 27  # arbitrarily chosen offset


Test patch:
diff --git a/tests/run/exceptionpropagation.pyx b/tests/run/exceptionpropagation.pyx
--- a/tests/run/exceptionpropagation.pyx
+++ b/tests/run/exceptionpropagation.pyx
@@ -79,3 +79,57 @@ def test_noexcept():
     >>> test_noexcept()
     """
     cdef_noexcept_function_that_raises()
+
+
+cdef int* cdef_ptr_func(int* input, int failure_mode):
+    # should have except NULL? by default
+    # failure mode is 0, 1, or 2
+    if failure_mode == 0:
+        return input  # don't fail
+    elif failure_mode == 1:
+        return NULL  # no exception
+    else:
+        raise RuntimeError("help!")
+
+ctypedef int* (*cdef_ptr_func_ptr)(int*, int) except? NULL
+
+def test_ptr_func(int failure_mode):
+    """
+    >>> test_ptr_func(0)
+    100
+    >>> test_ptr_func(1)
+    NULL
+    >>> test_ptr_func(2)
+    exception
+    """
+    # check that the signature is what we think it is
+    cdef cdef_ptr_func_ptr fptr = cdef_ptr_func
+    cdef int a = 100
+    try:
+        out = fptr(&a, failure_mode)
+        if out:
+            return out[0]
+        else:
+            print("NULL")
+    except RuntimeError:
+        print("exception")
+
+def test_ptr_func2(int failure_mode):
+    """
+    >>> test_ptr_func(0)
+    100
+    >>> test_ptr_func(1)
+    NULL
+    >>> test_ptr_func(2)
+    exception
+    """
+    # as above, but don't go through a function pointer
+    cdef int a = 100
+    try:
+        out = cdef_ptr_func(&a, failure_mode)
+        if out:
+            return out[0]
+        else:
+            print("NULL")
+    except RuntimeError:
+        print("exception")