zyzzler
zyzzler

Reputation: 115

How to use an object of a wrapped class as an argument in a wrapped function

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.

Upvotes: 3

Views: 184

Answers (1)

DavidW
DavidW

Reputation: 30941

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.

Upvotes: 1

Related Questions