I'm trying to learn how to wrap C++ library classes (whose constructors take std::string as an argument) in Cython to build a python interface for my library. But the resulting module is failing to return an object of the class I'm trying to wrap even though it doesn't return any error or exception. I present a minimal (but unfortunately still long) example below:
My toy project's directory structure is as follows (all paths mentioned below are relative to PBTOY2 project root directory shown):
C:\USERS\<MyName>\PROJECTS\PBTOY2
├───Examples
└───MyLib
├───include
│ └───MyLib
├───python
│ └───src
└───src
└───MyLib
My toy library is defined as follows in MyLib/include/MyLib/mylib.h
:
#pragma once
#include <iostream>
#include <string>
#include <MyLib/exports.h>
EXPIMP_TEMPLATE template class MYLIB_API std::basic_string<char,std::char_traits<char>,std::allocator<char>>;
class MYLIB_API Person {
public:
Person(std::string name, int id, std::string email);
Person(int id);
~Person();
const std::string getName() const;
const std::string getEmail() const;
const int getID() const;
const bool isValid() const;
private:
std::string name{};
int id{};
std::string email{};
};
where exports.h
lives in MyLib/include
and contains:
#pragma once
#ifdef MYLIB_EXPORTS
# define MYLIB_API __declspec(dllexport)
# define EXPIMP_TEMPLATE
#else
# define MYLIB_API __declspec(dllimport)
# define EXPIMP_TEMPLATE extern
#endif
The library is implemented in MyLib/src/MyLib/mylib.cpp
as follows:
#include <MyLib/mylib.h>
Person::Person(std::string name, int id, std::string email) : name{ name }, id{ id }, email{ email } {
}
Person::Person(int id) : id{ id } {
}
Person::~Person() {}
const std::string Person::getName() const {
return name;
}
const std::string Person::getEmail() const {
return email;
}
const int Person::getID() const {
return id;
}
const bool Person::isValid() const {
if (name.empty()) {
return false;
}
return true;
}
I test the library with an executable built from the source in example1.cpp
(found inExamples
which contains the following:
#include <iostream>
#include <MyLib/mylib.h>
int main (int argc, char *argv[]) {
std::string name{ "Homer" };
int id {1};
std::string email{ "me@there.com"};
Person p = Person(name, 1, email);
std::cout << "Hello, " << p.getName() << "! You are number " << p.getID() << "! Can I contact you at " << p.getEmail() << "?" << std::endl;
return 0;
}
All of this compiles and runs just fine (using CMake in a Windows 10 environment.) The library compiles to mylib.dll
and the executable compiles to toy-example.exe
.
Meanwhile, in MyLib/python/src
I've created the file MyLib.pxd
which contains the following:
# distutils: language = c++
from libcpp.string cimport string
cdef extern from "mylib.h":
cdef cppclass Person:
Person(string, int, string) except +
string getName()
string getEmail()
int getID()
and the file MyLib.pyx
which contains:
# cython: c_string_type=unicode, c_string_encoding=utf8
from MyLib cimport Person
cdef class PyPerson:
cdef Person* c_person_ptr
def __cinit__(self, str name, int id, str email):
print("creating the pointer for person with name ", name)
self.c_person_ptr = new Person(name, id, email)
print("created the pointer")
def get_name(self):
return self.c_person_ptr.getName()
def get_id(self):
return self.c_person_ptr.getID()
def get_email(self):
return self.c_person_ptr.getEmail()
def __dealloc__(self):
print("deallocating pointer")
del self.c_person_ptr
I have a setup.py
file in MyLib/python
which contains:
from setuptools import setup, Extension
from Cython.Build import cythonize
import os
ext = [Extension(name = "*",
sources = [os.path.join(os.path.dirname(__file__), "src", "*.pyx")],
language = "c++",
library_dirs = ["../../build/MyLib/Debug"],
libraries = ['MyLib',])
]
setup( name = "MyLib",
ext_modules = cythonize(ext, language_level = "3"),
include_dirs = ['../include/MyLib', '../include'],
)
and finally, a python script for testing the library
import os
dllPath = r'C:\Users\<MyHomeDirectory>\projects\PBToy2\build\install\bin'
os.add_dll_directory(dllPath)
if os.path.exists(dllPath):
import MyLib
print("I'm really here")
myPerson = MyLib.PyPerson("Homer", 0, "hsimpson@somewhere.com")
print( "my ID is ", myPerson.get_id())
else:
print("sorry, couldn't find the module")
(where I have edited out the actual name of my home directory in the above)
I'm building the MyLib
extension in MyLib/python
with the command python setup.py build_ext --inplace
.
I do get one warning about needing a dll-interface for the class std::_Compressed_pair<std::allocator<char>,std::_String_val<std::_Simple_types<_Elem>>,true>
but when I try to include a directive for this in mylib.h
Visual Studio does not recognize the type. (careful readers will note I do have a directive for another type in MyLib.h
that Visual Studio does recognize. I put it there to account for another warning I was getting).
the python module builds successfully, and useMylib.py
does run, but never returns from __cinit__
with a pointer to a Person
, and the call to myPerson.get_id()
never has a chance to execute. It just silently stops after getting as far as the print statement in MyLib.pyx
where I print "creating the pointer for person with name..." suggesting that a pointer is never returned from the call just below this print statement.
I've been able to do this kind of thing with a class defined outside of a library, but I need to be able to do it inside a library that compiles to a dll as in the case presented here.
I'm really stumped, and I haven't seen anything that I could call a "duplicate" of this problem here or anywhere else. My apologies if I've overlooked something. And thanks in advance for plowing through this question!
well, I solved my own problem (sort of). That's usually what happens, but this time not before posting my question. I was building my C++ project with a Debug configuration. I decided on a whim to try a Release configuration and voila. The problem vanished. I hope this helps someone like me who's trying to do something similar.