Repository: cython/cython

Issue:
Functions with C++ return type are `extern "C"`
Thats a C++ compiler error.
All functions with C++ in their signatures should be `extern "C++"`.


Discussion:
The whole error only exists when the function is `public`.
I've never come across "extern C++", so I'm probably the wrong person to review this, but I'd be happy to see a pull request that contains a test showing what goes wrong here, and preferably even adds a fix.

To write a test, look at the `.srctree` files under `/tests/run/`, e.g. this one:
https://github.com/cython/cython/blob/master/tests/run/cpp_assignment_overload.srctree
They are basically a bunch of separate files, separated by marker lines.
In C++, `extern "C++"` is the default, but in this case, Cython overrides it with `extern "C"`.
I also encountered this problem and made a minimal working example.

### Source

mwe.cpp
```cpp
#include "mwe.h"

#include <vector>
// this must come _after_ vector
#include "mwe_pyx.h"

std::vector<int> do_stuff() {
	std::vector<int> ret;
	std::vector<int> o = foo();
	for (auto i : o) {
		ret.emplace_back(2*i);
	}
	return ret;
}
```
mwe.h
```cpp
#include <vector>

std::vector<int> do_stuff();
```
c_mwe.pxd
```
from libcpp.vector cimport vector

cdef extern from "mwe.h":
    vector[int] do_stuff()
```
mwe.pyx
```py
from c_mwe cimport do_stuff
from libcpp.vector cimport vector

cdef public vector[int] foo():
    cdef vector[int] ret = [1, 2, 3]
    return [1, 2, 3]

def func():
    cdef vector[int] a = do_stuff()
    for i in a:
        print(i)
```
meson.build
```meson
project('cython-test', 'cpp',
  version : '0.1',
  default_options : ['warning_level=3', 'cpp_std=c++14'])

cython = find_program('cython', version: '>=0.29.14')
cython_flags = ['-Wextra', '--cplus', '-3']

mwe_pyx = custom_target('mwe_pyx',
  output: ['mwe_pyx.cpp', 'mwe_pyx.h'],
  input: files('mwe.pyx'),
  depend_files: files('c_mwe.pxd'),
  command: [cython, '@INPUT@', '-o', '@OUTPUT0@'] + cython_flags,
)
mwe_pyx_header = mwe_pyx[1]
mwe_pyx_cpp = mwe_pyx[0]

py3_mod = import('python')
py3 = py3_mod.find_installation('python3')

mwe_mod = py3.extension_module('mwe',
  mwe_pyx_cpp,
  'mwe.cpp',
  dependencies: [py3.dependency()]
)
```

### Compiling

```
[1/4] /usr/bin/cython ../mwe.pyx -o mwe_pyx.cpp -Wextra --cplus -3
[2/4] clang++ -Imwe.cpython-310-x86_64-linux-gnu.so.p -I. -I.. -I/usr/include/python3.10 -fcolor-diagnostics -D_FILE_OFFSET_BITS=64 -Wall -Winvalid-pch -Wnon-virtual-dtor -Wextra -Wpedantic -std=c++14 -O0 -g -fPIC -MD -MQ mwe.cpython-310-x86_64-linux-gnu.so.p/mwe.cpp.o -MF mwe.cpython-310-x86_64-linux-gnu.so.p/mwe.cpp.o.d -o mwe.cpython-310-x86_64-linux-gnu.so.p/mwe.cpp.o -c ../mwe.cpp
In file included from ../mwe.cpp:5:
./mwe_pyx.h:22:34: warning: 'foo' has C-linkage specified, but returns incomplete type 'std::vector<int>' which could be incompatible with C [-Wreturn-type-c-linkage]
__PYX_EXTERN_C std::vector<int>  foo(void);
                                 ^
1 warning generated.
[3/4] clang++ -Imwe.cpython-310-x86_64-linux-gnu.so.p -I. -I.. -I/usr/include/python3.10 -fcolor-diagnostics -D_FILE_OFFSET_BITS=64 -Wall -Winvalid-pch -Wnon-virtual-dtor -Wextra -Wpedantic -std=c++14 -O0 -g -fPIC -MD -MQ mwe.cpython-310-x86_64-linux-gnu.so.p/meson-generated_.._mwe_pyx.cpp.o -MF mwe.cpython-310-x86_64-linux-gnu.so.p/meson-generated_.._mwe_pyx.cpp.o.d -o mwe.cpython-310-x86_64-linux-gnu.so.p/meson-generated_.._mwe_pyx.cpp.o -c mwe_pyx.cpp
mwe_pyx.cpp:1176:34: warning: 'foo' has C-linkage specified, but returns incomplete type 'std::vector<int>' which could be incompatible with C [-Wreturn-type-c-linkage]
__PYX_EXTERN_C std::vector<int>  foo(void); /*proto*/
                                 ^
1 warning generated.
[4/4] clang++  -o mwe.cpython-310-x86_64-linux-gnu.so mwe.cpython-310-x86_64-linux-gnu.so.p/meson-generated_.._mwe_pyx.cpp.o mwe.cpython-310-x86_64-linux-gnu.so.p/mwe.cpp.o -Wl,--as-needed -Wl,--allow-shlib-undefined -shared -fPIC
```

### Fix

Currently, `__PYX_EXTERN_C` is defined as:
```cpp
#ifndef __PYX_EXTERN_C
  #ifdef __cplusplus
    #define __PYX_EXTERN_C extern "C"
  #else
    #define __PYX_EXTERN_C extern
  #endif
#endif
```

AFAIK, `extern` exports the symbol to the linker, while `extern "C"` additionally avoids name mangling and makes linking to C code possible.
I don't see why `extern "C"` should be necessary here at all since all code is compiled and linked with C++.
Therefore, I would just suggest:
```cpp
#ifndef __PYX_EXTERN_C
  #define __PYX_EXTERN_C extern
#endif
```
Applying this to the above MWE fixes the warning, while the module compiles, links, and runs as expected.





Resolving patch:
diff --git a/Cython/Compiler/ModuleNode.py b/Cython/Compiler/ModuleNode.py
--- a/Cython/Compiler/ModuleNode.py
+++ b/Cython/Compiler/ModuleNode.py
@@ -262,7 +262,7 @@ def h_entries(entries, api=0, pxd=0):
             api_guard = self.api_name(Naming.api_guard_prefix, env)
             h_code_start.putln("#ifndef %s" % api_guard)
             h_code_start.putln("")
-            self.generate_extern_c_macro_definition(h_code_start)
+            self.generate_extern_c_macro_definition(h_code_start, env.is_cpp())
             h_code_start.putln("")
             self.generate_dl_import_macro(h_code_start)
             if h_extension_types:
@@ -804,7 +804,7 @@ def generate_module_preamble(self, env, options, cimported_modules, metadata, co
         code.putln("    { __PYX_MARK_ERR_POS(f_index, lineno) goto Ln_error; }")
 
         code.putln("")
-        self.generate_extern_c_macro_definition(code)
+        self.generate_extern_c_macro_definition(code, env.is_cpp())
         code.putln("")
 
         code.putln("#define %s" % self.api_name(Naming.h_guard_prefix, env))
@@ -876,14 +876,17 @@ def generate_module_preamble(self, env, options, cimported_modules, metadata, co
         if has_np_pythran(env):
             env.use_utility_code(UtilityCode.load_cached("PythranConversion", "CppSupport.cpp"))
 
-    def generate_extern_c_macro_definition(self, code):
+    def generate_extern_c_macro_definition(self, code, is_cpp):
         name = Naming.extern_c_macro
         code.putln("#ifndef %s" % name)
-        code.putln("  #ifdef __cplusplus")
-        code.putln('    #define %s extern "C"' % name)
-        code.putln("  #else")
-        code.putln("    #define %s extern" % name)
-        code.putln("  #endif")
+        if is_cpp:
+            code.putln('    #define %s extern "C++"' % name)
+        else:
+            code.putln("  #ifdef __cplusplus")
+            code.putln('    #define %s extern "C"' % name)
+            code.putln("  #else")
+            code.putln("    #define %s extern" % name)
+            code.putln("  #endif")
         code.putln("#endif")
 
     def generate_dl_import_macro(self, code):


Test patch:
diff --git a/tests/compile/excvalcheck.h b/tests/compile/excvalcheck.h
--- a/tests/compile/excvalcheck.h
+++ b/tests/compile/excvalcheck.h
@@ -1,12 +1,6 @@
-#ifdef __cplusplus
-extern "C" {
-#endif
 extern DL_EXPORT(int) spam(void);
 extern DL_EXPORT(void) grail(void);
 extern DL_EXPORT(char *)tomato(void);
-#ifdef __cplusplus
-}
-#endif
 
 int spam(void) {return 0;}
 void grail(void) {return;}
diff --git a/tests/compile/nogil.h b/tests/compile/nogil.h
--- a/tests/compile/nogil.h
+++ b/tests/compile/nogil.h
@@ -1,25 +1,13 @@
-#ifdef __cplusplus
-extern "C" {
-#endif
 extern DL_EXPORT(void) e1(void);
 extern DL_EXPORT(int*) e2(void);
-#ifdef __cplusplus
-}
-#endif
 
 void e1(void) {return;}
 int* e2(void) {return 0;}
 
 
 
-#ifdef __cplusplus
-extern "C" {
-#endif
 extern DL_EXPORT(PyObject *) g(PyObject*);
 extern DL_EXPORT(void) g2(PyObject*);
-#ifdef __cplusplus
-}
-#endif
 
 PyObject *g(PyObject* o) {if (o) {}; return 0;}
 void g2(PyObject* o) {if (o) {}; return;}
diff --git a/tests/run/cpp_extern.srctree b/tests/run/cpp_extern.srctree
new file mode 100644
--- /dev/null
+++ b/tests/run/cpp_extern.srctree
@@ -0,0 +1,151 @@
+# mode: run
+# tag: cpp
+# ticket: 1839
+
+"""
+PYTHON setup.py build_ext --inplace
+PYTHON -c "from foo import test; test()"
+PYTHON -c "from bar import test; test()"
+PYTHON -c "from baz import test; test()"
+"""
+
+######## setup.py ########
+
+from Cython.Build import cythonize
+from Cython.Distutils.extension import Extension
+from distutils.core import setup
+
+foo = Extension(
+    "foo",
+    ["foo.pyx", "foo1.cpp", "foo2.cpp"],
+)
+bar = Extension(
+    "bar",
+    ["bar.pyx", "bar1.c", "bar2.cpp"],
+)
+baz = Extension(
+    "baz",
+    ["baz.pyx", "baz1.c", "baz2.cpp"],
+    define_macros = [("__PYX_EXTERN_C", 'extern "C"')],
+)
+
+setup(
+    ext_modules=cythonize([foo, bar, baz]),
+)
+
+######## foo.pyx ########
+
+# distutils: language = c++
+
+from libcpp cimport vector
+
+cdef public vector.vector[int] get_vector():
+    return [1,2,3]
+
+cdef extern from "foo_header.h":
+    cdef size_t size_vector1()
+    cdef size_t size_vector2()
+
+def test():
+    assert size_vector1() == 3
+    assert size_vector2() == 3
+
+######## foo_header.h ########
+
+size_t size_vector1();
+size_t size_vector2();
+
+######## foo1.cpp ########
+
+#include <vector>
+#include "foo.h"
+
+size_t size_vector1() {
+    return get_vector().size();
+}
+
+######## foo2.cpp ########
+
+#include <vector>
+extern "C" {
+// #include within `extern "C"` is legal.
+// We want to make sure here that Cython C++ functions are flagged as `extern "C++"`.
+// Otherwise they would be interpreted with C-linkage if the header is include within a `extern "C"` block.
+#include "foo.h"
+}
+
+size_t size_vector2() {
+    return get_vector().size();
+}
+
+######## bar.pyx ########
+
+cdef public char get_char():
+    return 42
+
+cdef extern from "bar_header.h":
+    cdef int get_int1()
+    cdef int get_int2()
+
+def test():
+    assert get_int1() == 42
+    assert get_int2() == 42
+
+######## bar_header.h ########
+
+int get_int1();
+int get_int2();
+
+######## bar1.c ########
+
+#include "bar.h"
+
+int get_int1() { return (int)get_char(); }
+
+######## bar2.cpp ########
+
+extern "C" {
+#include "bar.h"
+}
+
+extern "C" int get_int2() { return (int)get_char(); }
+
+######## baz.pyx ########
+
+# distutils: language = c++
+
+cdef public char get_char():
+    return 42
+
+cdef extern from "baz_header.h":
+    cdef int get_int1()
+    cdef int get_int2()
+
+def test():
+    assert get_int1() == 42
+    assert get_int2() == 42
+
+######## baz_header.h ########
+
+#ifdef __cplusplus
+  #define BAZ_EXTERN_C extern "C"
+#else
+  #define BAZ_EXTERN_C
+#endif
+
+BAZ_EXTERN_C int get_int1();
+int get_int2();
+
+######## baz1.c ########
+
+#undef __PYX_EXTERN_C
+#define __PYX_EXTERN_C
+#include "baz.h"
+
+int get_int1() { return (int)get_char(); }
+
+######## baz2.cpp ########
+
+#include "baz.h"
+
+int get_int2() { return (int)get_char(); }