Search code examples
c++dllcython

Cython wrapper for exported C++ class with std::string members fails to return obect


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{ "[email protected]"};

    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, "[email protected]")
    
    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!


Solution

  • 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.