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.