13

I have 2 classes, A and B. B inherits from A.

//C++    
class A
{
    public:
        int getA() {return this->a;};
        A() {this->a = 42;}
    private:
        int a;

};

class B: public A
{
    public:
       B() {this->b = 111;};
       int getB() {return this->b;};
    private:
        int b;

};

Now I would like to interface those two classes using Cython and have the possibility to call the getA() method from a B instance:

a = PyA()
b = PyB()
assert a.getA() == b.getA()

Currently my pyx file looks like this:

cdef extern from "Inherit.h" :
    cdef cppclass A:
       int getA()

    cdef cppclass B(A):
       int getB()


cdef class PyA:
    cdef A* thisptr

    def __cinit__(self):
       print "in A: allocating thisptr"
       self.thisptr = new A()
    def __dealloc__(self):
       if self.thisptr:
           print "in A: deallocating thisptr"
           del self.thisptr

    def getA(self):
       return self.thisptr.getA()

cdef class PyB(PyA):
    def __cinit__(self):
       if self.thisptr:
          print "in B: deallocating old A"
          del self.thisptr
       print "in B: creating new b"
       self.thisptr = new B()

    def __dealloc__(self):
       if self.thisptr:
           print "in B: deallocating thisptr"
           del self.thisptr
           self.thisptr = <A*>0

    def getB(self):
       return (<B*>self.thisptr).getB()

While I hope this code is not doing anything too dangerous, I also hope there is a better way to handle it.

Also using the module generates the following output:

>>> from inherit import *
>>> b = PyB()
in A: allocating thisptr
in B: deallocating old A
in B: creating new b
>>> b.getA()
42
>>> b.getB()
111
>>> del b
in B: deallocating thisptr

And I don't really like allocating an A instance just to free it immediately after.

Any advice about how to do it correctly ?

ascobol
  • 7,554
  • 7
  • 49
  • 70
  • [This article](https://github.com/cython/cython/wiki/WrappingSetOfCppClasses) shows a pitfall (double free) that the answers below did not take care of. –  Mar 06 '19 at 16:56

3 Answers3

8

I make some experiments, and have quite ready answer but now i see where is the problem:

If your extension type has a base type, the __cinit__ method of the base type is automatically called before your __cinit__ method is called; you cannot explicitly call the inherited __cinit__ method.

So the real problem is that Cython types still do not have constructors, only pre initializer hook __cinit__ which behave more like default constructors. You cannot call virtual method from constructor and you cannot call it from __cinit__ either (if you make a call, it behave like non virtual).

Somehow inside __cinit__ the type(self) return correct type object, but it is useless. Cython do not have static fields, methods and type object can be only instance of type (no metaclasses). Python @staticmethod is easy overridable, so it is useless.

So there is no other way like to put allocation inside def __init__(self):, and check for initialized thisptr wherever you use it.

You may consider creating a global dummy C++ object, and assign it to thisptr to avoid checking and crashing. There are no post initializer hook, so you will be unable to check if correct initialization already have taken place.

Guillaume Jacquenot
  • 11,217
  • 6
  • 43
  • 49
Arpegius
  • 5,817
  • 38
  • 53
3

I have never laid eyes on Cython before, so please forgive me if this is way off. That said:

In C++, any time you have bar : foo (bar inheriting from foo), if foo has a default constructor, it will be called automatically when bar is created.... unless you call your own custom constructor for the parent.

I don't know Python, but some quick Googling tells me that the same principles apply. i.e. the default constructor for PyA will only be called if PyB doesn't manually call another.

In that case, wouldn't something like this work?

cdef class PyA:
    cdef A* thisptr

    def __cinit__(self, bypasskey="")
       if bypasskey == "somesecret"
           print "in A: bypassing allocation"
       else
           print "in A: allocating thisptr"
           self.thisptr = new A()

...

cdef class PyB(PyA):
    def __cinit__(self):
       super( PyB, self ).__init__("somesecret")

Mind you, I'm sure it's crude. But perhaps the idea here will give you something to work with?


Here's another idea. I'm almost certain it'll work (but that the syntax is off), and it's certainly much cleaner than the above:

cdef class PyA:
    cdef A* thisptr

    def __cinit__(self, t=type(A))
           self.thisptr = new t()

...

cdef class PyB(PyA):
    def __cinit__(self):
       super( PyB, self ).__init__(type(B))

Or maybe it'll look like this?

cdef class PyA:
    cdef A* thisptr

    def __cinit__(self, t=A)
           self.thisptr = new t()

...

cdef class PyB(PyA):
    def __cinit__(self):
       super( PyB, self ).__init__(B)

I'm not posting this for the bounty (and you're not forced to assign it to anyone), I'm just sharing some thoughts with you.

I think you can/should be able to avoid "crashing the interpreter" if you either

a) make the second constructor only visible to b (don't know if this is possible), or

b) check if a is null before using it elsewhere, or

c) check that the calling function was the constructor for b before bypassing allocation.

Also, the Cython C++ documentation makes it rather clear that there may not be idiomatic solutions to all C++ adaptations, with vague, hand-wavy quotes like "It might take some experimenting by others (you?) to find the most elegant ways of handling this issue."

Mahmoud Al-Qudsi
  • 28,357
  • 12
  • 85
  • 125
  • well, by opening a bounty, I was looking for an idiomatic construct for this case. You say that you don't know Python and Cython. While your answer can be adapted to be legal Python (and Cython) code, this would give the Python user the power to crash the interpreter, which is to my opinion much worse than wasting memory allocations. – ascobol May 09 '12 at 17:30
  • My reply to you is too long for a comment, please see the latter half of my post. – Mahmoud Al-Qudsi May 09 '12 at 17:37
  • a) I think there can only be one constructor in Cython, as in Python. b) then in every method we would need to check for a correct initialization... c) maybe this is possible with an arbitrary pre-defined value in the C module. In PyA __ cinit__ we would check for an additional argument with this non-trivial value which would bypass the allocation. In this case a user cannot crash the interpreter "by accident". – ascobol May 09 '12 at 18:51
  • @ascobol You're right - I hadn't considered that Python might not have multiple constructors. I've changed both my original answer to having only one constructor (and it's a little/lot safer) as well as provided a new answer that might actually be an idiomatic solution. – Mahmoud Al-Qudsi May 09 '12 at 19:55
  • unfortunately c) seems not doable since PyA 'constructor' is called automatically before PyB constructor (see the output in my question), so we cannot call it explicitly from cython. Your other idea cannot work neither for the same reason and also because cinit arguments must be python types (either pure python types or types created by cython) and not C++ ones. Maybe Cython is missing some feature ? – ascobol May 10 '12 at 07:54
1

(I'm new to both Python and Cython, so take this answer for what it's worth.) If you initialize the thisptr in __init__ functions rather than __cinit__ functions, things seem to work in this particular example without the extra allocation/deletion... basically change your __cinit__ functions above to:

def __init__(self):
    print "in A: creating new A"
    self.thisptr = new A()

And

def __init__(self):
    print "in B: creating new B"
    self.thisptr = new B()

respectively. However, I am sure that this is at least theoretically unsafe (and probably practically unsafe as well) but perhaps someone could comment on exactly how unsafe...

For example, from the Cython introduction paper we know that "__init__ is not guaranteed to be run (for instance, one could create a sub-class and forget to call the ancestor constructor)." I have not been able to construct a test case where this occurs, but this is probably due to a general lack of Python knowledge on my part...

JW Peterson
  • 285
  • 3
  • 6