0

I am working on interfacing an existing C/C++ library to Python using Cython, which I have never used before. It's been going pretty well, except for the following scenario, which I describe using a mock-up example:

Suppose I have a class 'Person' with one member variable, an int named 'id', and another class 'Group' with one member variable, a Person named 'leader'. The classes Person and Group also have member functions for getting and setting their respective member variables, as shown in the code below. What I want to do is this: Given a group with a leader, I want to be able to access the id of the group's leader (both read and modify). For example, I would expect the following Python code

from pyperson import PyPerson
from pygroup import PyGroup

nancy = PyPerson(7) # Create new person with id=7
my_group = PyGroup(nancy) # Create new group with leader=nancy
my.group.get_leader().set_id(12)
print(nancy.get_id())

to output the number 12.

I am aware that this issue has been described on this site before (see links at the bottom of the post), but due to a lack of C++ competence, I have been unable to successfully transfer the answers given there to my dummy example.

The code I have written is as follows:

person.h

#ifndef PERSON_H
#define PERSON_H

class Person {
    private:
        int my_id;
    public:
        Person();
        Person(int id);
        int getId();
        void setId(int id);
};
#endif

person.cpp

#include "person.h"

// Default constructor
Person::Person() {}

// Overloaded constructor
Person::Person(int id) {
    this->setId(id);
}

// Get and set id
int Person::getId() {
    return this->my_id;
}
void Person::setId(int id) {
    this->my_id = id;
}

cperson.pxd

cdef extern from "person.cpp":
    pass

cdef extern from "person.h":
    cdef cppclass Person:
        Person() except +
        Person(int) except +
        int getId();
        void setId(int);

pyperson.pyx

# distutils: language = c++

from cperson cimport Person

cdef class PyPerson:
    cdef Person c_person

    def __cinit__(self, int id):
        self.c_person = Person(id)

    def get_id(self):
        return self.c_person.getId()
        
    def set_id(self, int id):
        self.c_person.setId(id)

group.h

#ifndef GROUP_H
#define GROUP_H
#include "person.h"

class Group {
    private:
        Person* my_leader;
    public:
        Group();
        Group(Person& leader);
        Person* getLeader();
        void setLeader(Person& leader);
};
#endif

group.cpp

#include "group.h"
#include "person.h"

// Default constructor
Group::Group() {}

// Overloaded constructor
Group::Group(Person& leader) {
    this->setLeader(leader);
}

// Get and set leader
Person* Group::getLeader() {
    return this->my_leader;
}
void Group::setLeader(Person& leader) {
    this->my_leader = &leader;
}

cgroup.pxd

from cperson cimport Person

cdef extern from "group.cpp":
    pass

cdef extern from "group.h":
    cdef cppclass Group:
        Group() except +
        Group(Person&) except +
        Person* getLeader();
        void setLeader(Person&);

pygroup.pyx

# distutils: language = c++

from cperson cimport Person
from cgroup cimport Group
from pyperson import PyPerson

cdef class PyGroup:
    cdef Group c_group

    def __cinit__(self, Person& leader):
        self.c_group = Group(leader)

    def get_leader(self):
        cdef PyPerson leader = PyPerson(0)
        leader.c_person = self.c_group.getLeader()
        return leader
        
    def set_leader(self, Person& leader):
        self.c_group.setLeader(leader)

setup.py

from setuptools import setup
from Cython.Build import cythonize
setup(ext_modules=cythonize(["pyperson.pyx", "pygroup.pyx"]))

Executing the command "python3 setup.py build_ext --inplace" in a folder containing the code above yields the following error message:

Error compiling Cython file:
------------------------------------------------------------
...

    def __cinit__(self, Person& leader):
        self.c_group = Group(leader)

    def get_leader(self):
        cdef PyPerson leader = PyPerson(0)
            ^
------------------------------------------------------------

pygroup.pyx:14:13: 'PyPerson' is not a type identifier

Error compiling Cython file:
------------------------------------------------------------
...
    def __cinit__(self, Person& leader):
        self.c_group = Group(leader)

    def get_leader(self):
        cdef PyPerson leader = PyPerson(0)
        leader.c_person = self.c_group.getLeader()
                                               ^
------------------------------------------------------------

pygroup.pyx:15:48: Cannot convert 'Person *' to Python object
Traceback (most recent call last):
  File "setup.py", line 5, in <module>
    setup(ext_modules=cythonize(["pyperson.pyx", "pygroup.pyx"]))
  File "/home/sindre/.local/lib/python3.8/site-packages/Cython/Build/Dependencies.py", line 1102, in cythonize
    cythonize_one(*args)
  File "/home/sindre/.local/lib/python3.8/site-packages/Cython/Build/Dependencies.py", line 1225, in cythonize_one
    raise CompileError(None, pyx_file)
Cython.Compiler.Errors.CompileError: pygroup.pyx

Any help in resolving these errors and achieving the stated goal would be greatly appreciated.

As promised, here are the links to the stackoverflow-posts I have checked out: How do I return wrapped C++ Objects in Cython from another wrapped Object? How to return a C++ wrapped object in Cython Cython - Exposing C++ (vector and non-vector) objects returned by C++ function to Python How to expose a function returning a C++ object to Python without copying the object?

EDIT:

According to the advice from DavidW, I changed the 5th line in pygroup.pyx to read

from pyperson cimport PyPerson

but this did not fix my issue. Instead, I got an additional compilation error reading

pygroup.pyx:5:0: 'pyperson.pxd' not found

Considering that PyPerson is defined in pyperson.pyx, not in pyperson.pxd, I think this error makes sense.

I experimented some more to see if I could resolve the errors myself, and ended up with the following code in pygroup.pyx (I did not change any of the other files described above):

# distutils: language = c++

from cperson cimport Person
from cgroup cimport Group
from pyperson import PyPerson

cdef class PyGroup:
    cdef Group c_group

    def __cinit__(self, Person& leader):
        self.c_group = Group(leader)

    def get_leader(self):
        leader = PyPerson(0)
        leader.c_person = self.c_group.getLeader()
        return leader
        
    def set_leader(self, Person& leader):
        self.c_group.setLeader(leader)

With the code above, I get only the following error:

Error compiling Cython file:
------------------------------------------------------------
...
    def __cinit__(self, Person& leader):
        self.c_group = Group(leader)

    def get_leader(self):
        leader = PyPerson(0)
        leader.c_person = self.c_group.getLeader()
                                               ^
------------------------------------------------------------

pygroup.pyx:15:48: Cannot convert 'Person *' to Python object

This confuses me, because I did not think leader.c_person would be a Python object, considering that c_person was defined using cdef. I also do not understand how to convert an instance of Person into something accessible to Python.

SSB
  • 1
  • 1
  • `from pyperson import PyPerson` -> this should be a `cimport` instead so that Cython can recognise the type at compile-time. Voting to close as a typo (since I think you know this, because the other definitions are `cimport`ed correctly) – DavidW Oct 18 '21 at 20:54
  • Thanks for the reply, @DavidW. I actually thought that it was correct to use import there, but now that you mentioned it, I realize that my logic was lacking. I'll update that line to use ```cimport``` when I get to work tomorrow morning (I'm on European time), and report back subsequently. – SSB Oct 18 '21 at 22:18
  • @DavidW: Changing that line to using cimport did not resolve the issue. Please see the edit I have added. – SSB Oct 19 '21 at 08:07
  • You can't access `c_person` unless Cython knows the type - it needs to be identified at compile-time rather than runtime. I suggest you create `pyperson.pxd` so that you can cimport your class – DavidW Oct 19 '21 at 11:06
  • Oh, I see. Thanks to your suggestion my code now compiles. It does not behave entirely as expected, but that's probably material for a new question if I cannot figure it out myself. Thanks so much for the help! – SSB Oct 19 '21 at 12:19
  • @SSB welcome to SO and glad to have you here - unfortunately it is not clear what is the question - always try to summarize the question in 1 line - and then elaborate - what you have here is too long and nobody is patient to read and try to understand what is the question ! – Areza Oct 22 '21 at 09:49

0 Answers0