3

I use cython to wrap C++ code and make it available in Python. The problem I'm facing is that I want to use a wrapped class as an argument in a function that I want to wrap as well. So from Python's point of view I want to create and modify an object of the wrapped class and use it as an argument for the wrapped function that I also want to call from Python. The code below will hopefully demonstrate this.

Below you can find a mininmal example in C++ which I want to wrap:

./cppCode/Settings/Settings.h

class Settings
{
public:
  Settings();
  void doSomething();
};

./cppCode/Helper/Helper.h

#include "../Settings/Settings.h"

void myFunction(Settings settings);

The functionality is not really important. Therefor I left out the .cpp files. Below is my approach in Cython so far:

./cythonCode/Settings/Settings.pxd

cdef extern from "../../cppCode/Settings/Settings.h":
  cdef cppclass Settings:
    Settings() except +
    void doSomething()

./cythonCode/Settings/Settings.pyx

# distutils: sources = ../../cppCode/Settings/Settings.cpp
# distutils: language = c++

from Settings cimport Settings

cdef class PySettings:
  cdef Settings c_settings

  def __cinit__(self):
    self.c_settings = Settings()

  def doSomething(self):
    self.c_settings.doSomething()

./cythonCode/Helper.pxd

from Settings.Settings cimport Settings

cdef extern from "../../cppCode/Helper/Helper.h":
  void myFunction(Settings settings)

./cythonCode/Helper.pyx

# distutils: sources = ../../cppCode/Helper/Helper.cpp
# distutils: language = c++

from Helper cimport myFunction

cdef PyMyFunction(PySettings settings):
  myFunction(settings)

run.py

import cythonCode.Settings.Settings as Settings
#import cythonCode.Helper as Helper

mySettings = Settings.PySettings()
mySettings.doSomething()

#Helper.myFunction(mySettings) # not working

I hope the structure of the project is clear. I would actually like to have the "Helper.pyx" and "Helper.pxd" in a folder "Helper" as well but then I don't know how to cimport Settings. If you can help me fix this it would also be highly appreciated. However, the main issue is to get Helper running at all such that I can use it in "run.py". The "setup.py" to build the cython modules simply looks like this:

from distutils.core import setup
from Cython.Build import cythonize
from setuptools.extension import Extension

extensions = [
  Extension("Helper", ["Helper.pyx"])
]

setup(ext_modules=cythonize(extensions))

I do the same separately in the Settings folder.

I'd highly appreciate your help in solving this issue!

EDIT: As mentioned in the comments, there are two mistakes:

1) It should be Helper.PyMyFunction(mySettings) in run.py.

2) It should be def instead of cdef in front of PyMyFunction because I definitely want to call this function from Python.

EDIT2: I played around with your inputs and found an inelegant solution which leads to another question. So below is the code that works for me:

Settings.pxd

cdef extern from "../../cppCode/Settings/Settings.h":
  cdef cppclass Settings:
    Settings() except +
    void doSomething()

cdef extern from "../../cppCode/Helper/Helper.h":
  void myFunction(Settings settings)

Settings.pyx

# distutils: sources = [../../cppCode/Settings/Settings.cpp, ../../cppCode/Helper/Helper.cpp]
# distutils: language = c++

from Settings cimport Settings, myFunction

cdef class PySettings:
  cdef Settings c_settings

  def __cinit__(self):
    self.c_settings = Settings()

  def doSomething(self):
    self.c_settings.doSomething()

def PyMyFunction(PySettings settings):
  myFunction(settings.c_settings)

When I cythonize Settings.pyx I can run the following Python code and everything works fine:

import Settings

mySettings = Settings.PySettings()
mySettings.doSomething()

Settings.PyMyFunction(mySettings)

What I find inelegant about it is the fact that both parts (Settings and myFunction) are included in the same file. I have no idea how to get this running when these two parts are in separate files.

EDIT3: In order to tackle the "two parts are in separate files" issue imagine the following code:

Settings.pxd

cdef extern from "../../cppCode/Settings/Settings.h":
  cdef cppclass Settings:
    Settings() except +
    void doSomething()

Settings.pyx

# distutils: sources = ../../cppCode/Settings/Settings.cpp
# distutils: language = c++

from Settings cimport Settings

cdef class PySettings:
  cdef Settings c_settings

  def __cinit__(self):
    self.c_settings = Settings()

  def doSomething(self):
    self.c_settings.doSomething()

Helper.pxd

from Settings cimport Settings

cdef extern from "../../cppCode/Helper/Helper.h":
  void myFunction(Settings settings)

Helper.pyx

# distutils: sources = ../../cppCode/Helper/Helper.cpp
# distutils: language = c++

from Helper cimport myFunction

def PyMyFunction(PySettings settings):
  myFunction(settings.c_settings)

It is the same code as in EDIT2 but split up in two files. There is only one additional line in Helper.pxd which is "from Settings cimport Settings". However, in Helper.pyx I get this error:

def PyMyFunction(PySettings settings):
                ^
---------------------------------------
Helper.pyx:6:17: 'PySettings' is not a type identifier

I tried "from Settings cimport PySettings" but this doesn't work. The same error keeps occuring. All the files are in the same directory.

zyzzler
  • 105
  • 7
  • Possible duplicate of [Cython example with more than one class](https://stackoverflow.com/questions/52262669/cython-example-with-more-than-one-class) – ead Sep 23 '19 at 12:44
  • @ead I'm not convinced that's the right duplicate (or if it is it isn't hugely clear to me). The problem here is 1) `PyMyFunction` is `cdef` rather than `def`, so it can't be called from Python; 2) OP calls `myFunction` (without the `Py`), which I suspect is just a typo. I'm pretty sure there is a duplicate for point 1 though... – DavidW Sep 23 '19 at 13:49
  • Possible duplicate of [Importing cython function: AttributeError: 'module' object has no attribute 'fun'](https://stackoverflow.com/questions/30228821/importing-cython-function-attributeerror-module-object-has-no-attribute-fun) – DavidW Sep 23 '19 at 13:53
  • Given your edit: what issues are you having? I can see a few small things in your code (you don't build the settings module, maybe; no need to `cimport` a pxd of the same name; in helper.pxd you probably want to import `PySettings` too), but this looks basically right, and I'd think the error messages should be clear enough that you can fix them. – DavidW Sep 23 '19 at 15:11
  • I made a second edit. It basically is a whole different question now. I should probably just open a new question unless this is easy to fix. – zyzzler Sep 23 '19 at 15:52
  • About "the two parts are in separate files" - this is most likely a problem with Cython being able to find the pxds when they're in different directories (it's kind of hard to tell though). This kind of thing is much easier to answer if you tell us the error you're getting. It looks like you're doing basically the right thing though – DavidW Sep 23 '19 at 16:02
  • I made an EDIT3 to clearify my current state with the error I'm seeing. – zyzzler Sep 23 '19 at 16:37

1 Answers1

1

Clearly this question is just a range of small issues with something that was pretty close to working:

  1. The main initial issue was that functions [should be def or cpdef, but not cdef to be callable from Python (Importing cython function: AttributeError: 'module' object has no attribute 'fun'). It's worth remembering that Cython compiles/accelerates all functions and the only advantage of cdef or cpdef is that they can be called slightly faster from Cython. Therefore you should declare functions cdef by default.

  2. There were a few cases where you were using the C++ typename Settings instead of the Cython class PySettings. I assume this was largely a typo.

  3. There's no need to do cimport Settings within Settings.pyx and cimport Helper within Helper.pyx - Cython autoatically does the equivalent of from filename cimport * where a .pxd file with a matching filename exists.

  4. Your final problem is in separating your classes out into multiple files - you're unable to cimport PySettings into Helper.pyx. What you should remember is that "cimport" looks at the .pxd file - treating it a bit like a C/C++ header. If you want to cimport PySettings then (a declaration of) PySettings must be in the Settings.pxd

    cdef class PySettings:
       cdef Settings c_settings
       # signatures for any cdef functions also go here
    

    In Settings.pyx you do

    cdef class PySettings:
        # don't duplicate "cdef Settings c_settings"
        # do put all the def functions here
        def __cinit__(self):
           self.c_settings = Settings()
    
        def doSomething(self):
           self.c_settings.doSomething()
    

    from this Helper.pyx will know of PySettings and the fact that it has a c_settings attribute.

DavidW
  • 29,336
  • 6
  • 55
  • 86
  • Thanks a lot for all your useful input! It basically solved this issue. However, there is still a problem with the imports in the python code. I opend a different question for that: https://stackoverflow.com/questions/58083432/how-to-import-depending-cython-modules-from-parent-folder-in-python – zyzzler Sep 24 '19 at 15:19