Repository: cython/cython
Pull request: [Ticket 2940] fix mangling for .pxd cdef public functions
Fixes #2940 

This PR fixes a typo that caused `cdef public` functions from .pxd files to be erroneously mangled.

The unmangled name is chosen when the definition from the .py file is processed, but on the condition that the cname determined while processing the declaration from the .pxd file was the mangled one (instead of a user-defined one).

It also adds a test for the ticket.

Problem statement:
Compiling a .py file with an augmenting .pxd leads to a mangled function name for cdef public function.
I would like to use the .py, with augmenting .pxd method for my Cython project so I can debug and use an IDE while I develop. For some reason this method leads to mangled exported function names. The definition of the function in the .pxd is ```cdef public int count_cwrap(int limit)``` and the .h file produced has this definition ```__PYX_EXTERN_C int __pyx_f_19pythagorean_triples_count_cwrap(int);``` If I convert the file to .pyx and add the cython definition, the name is not mangled anymore. I tried it and this occurs in both Linux and Windows.  I am attaching my source code to reproduce the issue.

pythagorean_triples.py:
```Python
# cython: language_level=3
import time
import cython
import numpy as np
 
def count(limit):
    if limit < 3:
       raise ValueError('Invalid limit value')
    arr = np.zeros(10)
    result : cython.int = 0
    a : cython.int
    b : cython.int
    c : cython.int
    for a in range(1, limit + 1):
        for b in range(a + 1, limit + 1):
            for c in range(b + 1, limit + 1):
                if c * c > a * a + b * b:
                    break
 
                if c * c == (a * a + b * b):
                    result += 1
    return result
	
def count_cwrap(limit):
    return count(limit)
 
if __name__ == '__main__':
    start = time.time()
    result = count(1000)
    duration = time.time() - start
    print(result, duration)
```
pythagorean_triples.pxd:
```Cython
# cython: language_level=3
cdef public int count_cwrap(int limit)
```
setup.py:
```Python
from distutils.core import setup
from distutils.extension import Extension
from Cython.Build import cythonize
 
setup(
    ext_modules = cythonize(Extension('pythagorean_triples', sources = ['pythagorean_triples.py']))
)
```


Is this a bug or am I doing something wrong perhaps in the setup.py?

I built with ```python setup_py.py build_ext --inplace``` in windows or with
```python3 setup_py.py build_ext --inplace``` in Linux

Thanks

Hints from issue discussion:
Thanks for the report. Could be that the `private` marker is simply not copied when adapting the Python function signature from the `.pxd` file in `AlignFunctionDefinitions` syntax tree transformation (`ParseTreeTransforms.py`). Probably easy to add there. PR welcome.
[Here](https://github.com/cython/cython/blob/master/tests/build/module_api.srctree) is an example of a test file that you could borrow from. It's just a bunch of files stuffed into one that the test runner extracts.
Just ran into this on `3.0.0a10` and I don't know how to fix it. Has anyone been able to workaround this?

It seems `AlignFunctionDefinitions.visit_DefNode` is receiving a `pxd_def` with its name mangled despite having `visibility == 'public'`. https://github.com/cython/cython/blob/ddaaa7b8bfe9885b7bed432cd0a5ab8191d112cd/Cython/Compiler/ParseTreeTransforms.py#L2598

My gut feeling is that's probably incorrect, but I don't know the codebase well enough to say for certain.

I can see the `pxd_def` is generated in `ModuleScope.declare_cfunction` where it looks like the `(visibility == 'public' and defining)` condition is allowing it to be mangled.
https://github.com/cython/cython/blob/ddaaa7b8bfe9885b7bed432cd0a5ab8191d112cd/Cython/Compiler/Symtab.py#L1552


Also, is it possible to specify the visibility when using the `@cython.cfunc` decorator? Something like how `@cython.declare(visibility='public')` does?

Commit message: Add to the list of skipped test because of a PyPy2 issue

Patch to review:
diff --git a/Cython/Compiler/Symtab.py b/Cython/Compiler/Symtab.py
index 92afd877954..4d834d89682 100644
--- a/Cython/Compiler/Symtab.py
+++ b/Cython/Compiler/Symtab.py
@@ -1617,7 +1617,7 @@ def declare_cfunction(self, name, type, pos,
         entry = self.lookup_here(name)
         if entry and entry.defined_in_pxd:
             if entry.visibility != "private":
-                mangled_cname = self.mangle(Naming.var_prefix, name)
+                mangled_cname = self.mangle(Naming.func_prefix, name)
                 if entry.cname == mangled_cname:
                     cname = name
                     entry.cname = cname
diff --git a/tests/compile/pxd_mangling_names.srctree b/tests/compile/pxd_mangling_names.srctree
new file mode 100644
index 00000000000..3797fc0f938
--- /dev/null
+++ b/tests/compile/pxd_mangling_names.srctree
@@ -0,0 +1,46 @@
+# mode: compile
+# ticket: 2940
+
+PYTHON setup.py build_ext --inplace
+PYTHON -c "import a; a.test()"
+
+######## setup.py ########
+
+from Cython.Build import cythonize
+from Cython.Distutils.extension import Extension
+from distutils.core import setup
+
+setup(
+    ext_modules=cythonize([Extension("a", ["a.py", "b.c"])]),
+)
+
+######## a.pxd ########
+
+cdef public int foo()
+
+cdef extern from "b.h":
+    cpdef int bar()
+
+######## a.py ########
+
+def foo():
+    return 42
+
+def test():
+    assert bar() == 42
+
+######## b.h ########
+
+#ifndef B_H
+#define B_H
+
+int bar();
+
+#endif
+
+######## b.c ########
+
+#include "a.h"
+
+int bar() { return foo(); }
+
diff --git a/tests/pypy2_bugs.txt b/tests/pypy2_bugs.txt
index 200f0dcf3b2..c15681dd05d 100644
--- a/tests/pypy2_bugs.txt
+++ b/tests/pypy2_bugs.txt
@@ -16,8 +16,9 @@ run.partial_circular_import
 # https://foss.heptapod.net/pypy/pypy/issues/3185
 run.language_level
 run.pure_pxd
+run.pxd_mangling_names
 
-# Silly error with doctest matching slightly different string outputs rather than 
+# Silly error with doctest matching slightly different string outputs rather than
 # an actual bug but one I can't easily resolve
 run.with_gil
 


Code review comments:
diff_hunk: @@ -16,8 +16,9 @@ run.partial_circular_import
 # https://foss.heptapod.net/pypy/pypy/issues/3185
 run.language_level
 run.pure_pxd
+run.pxd_mangling_names
path: tests/pypy2_bugs.txt
text: @user1:
```suggestion
compile.pxd_mangling_names
```

@author:
Damned, looks like I failed a copy/paste! 😭