Repository: cython/cython

Issue:
[BUG] Function pointers in extern structure are not noexcept by default 
### Describe the bug

Numpy declares following structure [1]:
```
cdef extern from "numpy/random/bitgen.h":
    struct bitgen:
        void *state
        uint64_t (*next_uint64)(void *st) nogil
        uint32_t (*next_uint32)(void *st) nogil
        double (*next_double)(void *st) nogil
        uint64_t (*next_raw)(void *st) nogil


    ctypedef bitgen bitgen_t
```

All function pointers are declared as `except *` in Cython3.


[1] https://github.com/numpy/numpy/blob/bba99dbd606d173a12c7de1867e2337de17c60e9/numpy/random/__init__.pxd#L4-L12

### Code to reproduce the behaviour:

```cython
from numpy.random cimport bitgen
from libc.stdint cimport uint64_t

cdef uint64_t cb(void *st) nogil:
    return 5

cdef bitgen bg = bitgen(NULL, &cb, NULL, NULL, NULL)

bg.next_uint64(NULL)
```

### Expected behaviour

All function pointers in structure are `noexcept` by default when placed in extern block.

### OS

MacOS

### Python version

3.9.16

### Cython version

master

### Additional context

There is possible performance degradation caused by this bug due following excerpt from documentation:
> There’s a known performance pitfall when combining nogil and except * @cython.exceptval(check=True). In this case Cython must always briefly re-acquire the GIL after a function call to check if an exception has been raised.

> **Note** This bug is affecting migration of scipy to cython3 - see: https://github.com/scipy/scipy/pull/18242/files#r1156507014

### Example 1:
```cython
cdef uint64_t cb(void *st) noexcept nogil:
    return 5

cdef bitgen bg = bitgen(NULL, &cb, NULL, NULL, NULL)

bg.next_uint64(NULL)
```
* Cython 0.29.X - compilation success, code: `(void)(__pyx_v_2np_bg.next_uint64(NULL));`
* master - compilation success, code: `__pyx_v_2np_bg.next_uint64(NULL); if (unlikely(PyErr_Occurred())) __PYX_ERR(1, 9, __pyx_L1_error)`
> **Warning** it seems that cython3 here ignores `noexcept` and generates code as for `except *`

### Example 2:
```cython
cdef uint64_t cb(void *st) nogil:
    return 5

cdef bitgen bg = bitgen(NULL, &cb, NULL, NULL, NULL)

bg.next_uint64(NULL)
```
* Cython 0.29.X - compilation success, code: `(void)(__pyx_v_2np_bg.next_uint64(NULL));`
* master - compilation success, code: `__pyx_v_2np_bg.next_uint64(NULL); if (unlikely(PyErr_Occurred())) __PYX_ERR(1, 9, __pyx_L1_error)`

### Example 3:
```cython
cdef uint64_t cb(void *st) except * nogil:
    return 5

cdef bitgen bg = bitgen(NULL, &cb, NULL, NULL, NULL)

bg.next_uint64(NULL)
```
* Cython 0.29.X - compilation error
* master - compilation success, code: `__pyx_v_2np_bg.next_uint64(NULL); if (unlikely(PyErr_Occurred())) __PYX_ERR(1, 9, __pyx_L1_error)`




Discussion:
CC @mattip @rgommers @tacaswell 
Agreed that this contradicts the decision in https://github.com/cython/cython/issues/4280#issuecomment-1100588580

Resolving patch:
diff --git a/Cython/Compiler/Nodes.py b/Cython/Compiler/Nodes.py
--- a/Cython/Compiler/Nodes.py
+++ b/Cython/Compiler/Nodes.py
@@ -1557,7 +1557,7 @@ def declare(self, env, scope=None):
     def analyse_declarations(self, env):
         scope = None
         if self.attributes is not None:
-            scope = StructOrUnionScope(self.name)
+            scope = StructOrUnionScope(self.name, self.visibility)
         self.declare(env, scope)
         if self.attributes is not None:
             if self.in_pxd and not env.in_cinclude:
diff --git a/Cython/Compiler/Parsing.py b/Cython/Compiler/Parsing.py
--- a/Cython/Compiler/Parsing.py
+++ b/Cython/Compiler/Parsing.py
@@ -3394,7 +3394,7 @@ def p_c_struct_or_union_definition(s, pos, ctx):
         else:
             s.expect('NEWLINE')
             s.expect_indent()
-            body_ctx = Ctx()
+            body_ctx = Ctx(visibility=ctx.visibility)
             while s.sy != 'DEDENT':
                 if s.sy != 'pass':
                     attributes.append(
diff --git a/Cython/Compiler/Symtab.py b/Cython/Compiler/Symtab.py
--- a/Cython/Compiler/Symtab.py
+++ b/Cython/Compiler/Symtab.py
@@ -2094,9 +2094,11 @@ def lookup_assignment_expression_target(self, name):
 
 class StructOrUnionScope(Scope):
     #  Namespace of a C struct or union.
+    #  visibility     string   Visibility of a C struct or union
 
-    def __init__(self, name="?"):
+    def __init__(self, name="?", visibility='private'):
         Scope.__init__(self, name, None, None)
+        self.visibility = visibility
 
     def declare_var(self, name, type, pos,
                     cname=None, visibility='private',
@@ -2123,8 +2125,8 @@ def declare_var(self, name, type, pos,
         elif type.needs_refcounting:
             if not allow_refcounted:
                 error(pos, "C struct/union member cannot be reference-counted type '%s'" % type)
-        if visibility != 'private':
-            error(pos, "C struct/union member cannot be declared %s" % visibility)
+        if visibility != self.visibility:
+            error(pos, "C struct/union visibility %s does not match the member visibility %s" % (self.visibility, visibility))
         return entry
 
     def declare_cfunction(self, name, type, pos,


Test patch:
diff --git a/tests/errors/cfuncptr.pyx b/tests/errors/cfuncptr.pyx
--- a/tests/errors/cfuncptr.pyx
+++ b/tests/errors/cfuncptr.pyx
@@ -18,6 +18,10 @@ cdef extern from *:
     # define this as extern since Cython converts internal "except*" to "except -1"
     cdef int exceptstar(int bad) except *
 
+    struct mystruct:
+        int (*func_ptr)(int param) nogil
+        void (*func_ptr_void)(int param) nogil
+
 def fail_exceptstar(bad):
     cdef int (*fptr_a)(int) noexcept
     cdef int (*fptr_b)(int) except -1
@@ -26,11 +30,25 @@ def fail_exceptstar(bad):
     fptr_b = exceptstar
     fptr_c = exceptstar
 
+cdef int cb(int param) nogil:
+    return param
+
+cdef void cb_void(int param) except * nogil:
+    return
+
+def fail_struct_pointer():
+    cdef mystruct ms = mystruct(&cb, &cb_void)
+
+
 _ERRORS = """
 13:13: Cannot assign type 'int (int) except? -2' to 'int (*)(int) except -2'
 14:13: Cannot assign type 'int (int) except? -2' to 'int (*)(int) except -1'
 15:13: Cannot assign type 'int (int) except? -2' to 'int (*)(int) except? -1'
-25:13: Cannot assign type 'int (int) except *' to 'int (*)(int) noexcept'
-26:13: Cannot assign type 'int (int) except *' to 'int (*)(int) except -1'
-27:13: Cannot assign type 'int (int) except *' to 'int (*)(int) except? -1'
+29:13: Cannot assign type 'int (int) except *' to 'int (*)(int) noexcept'
+30:13: Cannot assign type 'int (int) except *' to 'int (*)(int) except -1'
+31:13: Cannot assign type 'int (int) except *' to 'int (*)(int) except? -1'
+40:32: Cannot assign type 'int (*)(int) except? -1 nogil' to 'int (*)(int) noexcept nogil'
+40:32: Cannot assign type 'int (*)(int) except? -1 nogil' to 'int (*)(int) noexcept nogil'
+40:37: Cannot assign type 'void (*)(int) except * nogil' to 'void (*)(int) noexcept nogil'
+40:37: Cannot assign type 'void (*)(int) except * nogil' to 'void (*)(int) noexcept nogil'
 """
diff --git a/tests/run/extern_impl_excvalue.srctree b/tests/run/extern_impl_excvalue.srctree
--- a/tests/run/extern_impl_excvalue.srctree
+++ b/tests/run/extern_impl_excvalue.srctree
@@ -1,6 +1,7 @@
 PYTHON setup.py build_ext --inplace
 PYTHON -c "import foo"
 PYTHON -c "import a"
+PYTHON -c "import b"
 
 ######## setup.py ########
 
@@ -15,6 +16,10 @@ setup(
 
 cdef int bar() except *
 
+cdef extern from "bar_impl.c":
+    struct mystruct:
+        int (*func_ptr)(int param) nogil
+
 ######## foo.pyx ########
 
 cdef extern from "bar_impl.c":
@@ -24,9 +29,24 @@ cdef extern from "bar_impl.c":
 
 static int bar() { return -1; }
 
+typedef struct mystruct {
+  int (*func_ptr)(int param);
+} mystruct_t;
+
 ######## a.pyx ########
 
 cimport cython
 from foo cimport bar
 
 assert bar() == -1
+
+
+######## b.pyx ########
+
+from foo cimport mystruct
+
+cdef int cb(int param) noexcept nogil:
+    return param
+
+cdef mystruct ms = mystruct(&cb)
+assert ms.func_ptr(5) == 5