Repository: cython/cython

Issue:
[BUG] Access violation using cython.ufunc with cimport numpy
### Describe the bug

The following code using `@cython.ufunc`, a slight variation of the documentation, crashes reproducibly on import on Windows with Python 3.11/3.12, Cython 3.0.9/3.1.0a0, numpy 1.26.4, Visual Studio 2022, and `define_macros=[("NPY_NO_DEPRECATED_API", "NPY_1_7_API_VERSION")]`.
```cython
# ufunc_crash.pyx
cimport cython

cimport numpy  # no crash without this line

@cython.ufunc
cdef double add_one(double x):
    return x+1
```
```
> py -X dev -c"import ufunc_crash"
Windows fatal exception: access violation

Current thread 0x000011b8 (most recent call first):
  File "<frozen importlib._bootstrap>", line 241 in _call_with_frames_removed
  File "<frozen importlib._bootstrap_external>", line 1241 in exec_module
  File "<frozen importlib._bootstrap>", line 690 in _load_unlocked
  File "<frozen importlib._bootstrap>", line 1147 in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 1176 in _find_and_load
  File "<string>", line 1 in <module>
```
```
[0x0]   ufunc_crash_cp311_win_amd64!__pyx_pymod_exec_ufunc_crash+0x5fb   0x3acbfef1e0   0x7ffab1670a88   
[0x1]   python311!PyModule_ExecDef+0x90   0x3acbfef290   0x7ffab16709a1   
[0x2]   python311!exec_builtin_or_dynamic+0x45   0x3acbfef2c0   0x7ffab175ab18   
[0x3]   python311!_imp_exec_dynamic_impl+0xa   0x3acbfef2f0   0x7ffab1619c0c   
[0x4]   python311!_imp_exec_dynamic+0x10   0x3acbfef2f0   0x7ffab1619c0c   
[0x5]   python311!cfunction_vectorcall_O+0x21c   0x3acbfef320   0x7ffab1660773   
[0x6]   python311!_PyVectorcall_Call+0x19   0x3acbfef380   0x7ffab1660440   
[0x7]   python311!_PyObject_Call+0x5b   0x3acbfef380   0x7ffab1660440   
[0x8]   python311!PyObject_Call+0xc   0x3acbfef3e0   0x7ffab15eac94   
[0x9]   python311!do_call_core+0x1bc   0x3acbfef3e0   0x7ffab15eac94   
[0xa]   python311!_PyEval_EvalFrameDefault+0x5384   0x3acbfef440   0x7ffab160f344   
[0xb]   python311!_PyEval_EvalFrame+0x1c   0x3acbfef650   0x7ffab16a37c3   
[0xc]   python311!_PyEval_Vector+0x30f   0x3acbfef650   0x7ffab16a37c3   
[0xd]   python311!_PyFunction_Vectorcall+0x336   0x3acbfef650   0x7ffab16a37c3   
[0xe]   python311!_PyObject_VectorcallTstate+0x3a4   0x3acbfef650   0x7ffab16a37c3   
[0xf]   python311!object_vacall+0x9f   0x3acbfef6e0   0x7ffab16a36fe   
[0x10]   python311!PyObject_CallMethodObjArgs+0x5e   0x3acbfef770   0x7ffab1646c11   
[0x11]   python311!import_find_and_load+0xe9   0x3acbfef7c0   0x7ffab162596a   
[0x12]   python311!PyImport_ImportModuleLevelObject+0x52a   0x3acbfef830   0x7ffab1648c53   
[0x13]   python311!import_name+0x9f   0x3acbfef8e0   0x7ffab15ecc62   
[0x14]   python311!_PyEval_EvalFrameDefault+0x7352   0x3acbfef980   0x7ffab16456bf   
[0x15]   python311!_PyEval_EvalFrame+0x1e   0x3acbfefb90   0x7ffab16470bf   
[0x16]   python311!_PyEval_Vector+0x77   0x3acbfefb90   0x7ffab16470bf   
[0x17]   python311!PyEval_EvalCode+0x97   0x3acbfefbd0   0x7ffab16d03c6   
[0x18]   python311!run_eval_code_obj+0x52   0x3acbfefc50   0x7ffab16d0342   
[0x19]   python311!run_mod+0x72   0x3acbfefc80   0x7ffab16cf8eb   
[0x1a]   python311!PyRun_StringFlags+0x7b   0x3acbfefcc0   0x7ffab1695599   
[0x1b]   python311!PyRun_SimpleStringFlags+0x41   0x3acbfefd10   0x7ffab169566c   
[0x1c]   python311!pymain_run_command+0xa0   0x3acbfefd50   0x7ffab169597c   
[0x1d]   python311!pymain_run_python+0x124   0x3acbfefd80   0x7ffab1695829   
[0x1e]   python311!Py_RunMain+0x15   0x3acbfefdf0   0x7ffab1696019   
[0x1f]   python311!Py_Main+0x25   0x3acbfefe20   0x7ff6322b1230   
[0x20]   python!invoke_main+0x22   0x3acbfefe70   0x7ffb3533257d   
[0x21]   python!__scrt_common_main_seh+0x10c   0x3acbfefe70   0x7ffb3533257d   
[0x22]   KERNEL32!BaseThreadInitThunk+0x1d   0x3acbfefeb0   0x7ffb36f8aa58   
[0x23]   ntdll!RtlUserThreadStart+0x28   0x3acbfefee0   0x0   
```

The issue might be with when the numpy C API is initialized. For example, the following code does not crash (calling `numpy.import_array()` should not be necessary according to the documentation):
```cython
cimport numpy
numpy.import_array()

cimport cython

@cython.ufunc
cdef double add_one(double x):
    return x+1
```

However, that is no solution since this does crash on Cython 3.0.9 (seems to be fixed in 3.1.0a0 ?):
```cython
cimport numpy
numpy.import_array()

cimport cython
from cython.parallel import parallel

@cython.ufunc
cdef double add_one(double x):
    return x+1
```

I have been using the numpy C API with Cython for many years. This seems to be specific to `cython.ufunc`.

### Code to reproduce the behaviour:

```cython
cimport cython

cimport numpy

@cython.ufunc
cdef double add_one(double x):
    return x+1
```

### Expected behaviour

No crash.

### OS

Windows 11

### Python version

3.11, 3.12

### Cython version

3.0.9, 3.1.0a0

### Additional context

_No response_


Discussion:
Adding these lines at the top of the .pyx before any other code seems to work reliable for me:
```cython
cimport numpy
numpy.import_array()
numpy.import_ufunc()
```
So the issue seems to be: using `cython.ufunc` is supposed to add this utility code https://github.com/cython/cython/blob/6aeecdec5a0aa851e941eb7e0186c9613faaeb12/Cython/Utility/NumpyImportArray.c#L22 but it doesn't seem to be happening for some reason

Resolving patch:
diff --git a/Cython/Compiler/Code.py b/Cython/Compiler/Code.py
--- a/Cython/Compiler/Code.py
+++ b/Cython/Compiler/Code.py
@@ -447,7 +447,7 @@ def format_code(self, code_string, replace_empty_lines=re.compile(r'\n\n+').sub)
             code_string = replace_empty_lines('\n', code_string.strip()) + '\n\n'
         return code_string
 
-    def __str__(self):
+    def __repr__(self):
         return "<%s(%s)>" % (type(self).__name__, self.name)
 
     def get_tree(self, **kwargs):
@@ -501,9 +501,11 @@ def __eq__(self, other):
         if self_type is not other_type and not (isinstance(other, self_type) or isinstance(self, other_type)):
             return False
 
+        self_init = getattr(self, 'init', None)
+        other_init = getattr(other, 'init', None)
         self_proto = getattr(self, 'proto', None)
         other_proto = getattr(other, 'proto', None)
-        return (self_proto, self.impl) == (other_proto, other.impl)
+        return (self_init, self_proto, self.impl) == (other_init, other_proto, other.impl)
 
     def none_or_sub(self, s, context):
         """


Test patch:
diff --git a/Cython/Compiler/Tests/TestUtilityLoad.py b/Cython/Compiler/Tests/TestUtilityLoad.py
--- a/Cython/Compiler/Tests/TestUtilityLoad.py
+++ b/Cython/Compiler/Tests/TestUtilityLoad.py
@@ -92,3 +92,21 @@ class TestCythonUtilityLoader(TestTempitaUtilityLoader):
 
     test_load = TestUtilityLoader.test_load
     test_load_tempita = TestTempitaUtilityLoader.test_load
+
+
+class TestUtilityCode(unittest.TestCase):
+    def test_equality(self):
+        c1 = Code.UtilityCode.load("NumpyImportUFunc", "NumpyImportArray.c")
+        c2 = Code.UtilityCode.load("NumpyImportArray", "NumpyImportArray.c")
+        c3 = Code.UtilityCode.load("pyunicode_strlen", "StringTools.c")
+        c4 = Code.UtilityCode.load("pyunicode_from_unicode", "StringTools.c")
+        c5 = Code.UtilityCode.load("IncludeStringH", "StringTools.c")
+        c6 = Code.UtilityCode.load("IncludeCppStringH", "StringTools.c")
+
+        codes = [c1, c2, c3, c4, c5, c6]
+        for m in range(len(codes)):
+            for n in range(len(codes)):
+                if n == m:
+                    self.assertEqual(codes[m], codes[n])
+                else:
+                    self.assertNotEqual(codes[m], codes[n])