pythonc++templatescython

Cython wrapping - a class template - non-standard example


First, post. I'm sorry for any formatting errors. I'm working on porting a c++ project to a python. I cannot change the c++ code. Within this project, I'm working on porting a class template that depends on a bool as input to define the class. My various attempts at doing this has failed either where I simply don't have correct cython code or where I get error during cython's process of generating c++ code. I haven't been able to solve my problem via other stack exchange posts [1] [2] [3] or via the documentation.

For my "best" attempt, I have something that looks like the following:

In the c++ header file foo.hh

class Foo{
public:
     static constexpr bool T
     Foo(Foo&& x) =  default;
     explicit Foo(const Foo<not T>&& x)
//Other methods and constructors not relevant.
}

The project I'm working with has multiple .pxdfile. The main file exposes the c++ code while the other .pxd files are for module specific tasks.
In main_decl.pxd

cdef cppclass Foo[T]
cdef cppclass Foo[T]:
    Foo(Foo& x)
    Foo(const Foo[T]&& x) #  I'm unsure if this is correct.
    #  other methods below

in the module specific file foo.pxd

from .main_decl cimport *

cdef class Foo:
    cdef Foo *thisptr

and finally the module itself Foo.pyx

# distutils: language = c++
cimport cython


cdef class Foo(object):
    def __init__(self, T):  
        pass
    def __cinit__(self):
        self.thisptr = NULL
    def __dealloc__(self):
        del self.thisptr

In other parts of the project, the above pattern works for defining class. The error

main/foo.cpp -o build/temp.linux-x86_64-cpython-310/main/foo.o
      main/foo.cpp:1591:15: error: ‘T’ was not declared in this scope
       1591 |   main::Foo<T>  *thisptr;
            |               ^~~
      main/Foo.cpp:1591:18: error: template argument 1 is invalid
       1591 |   main::Box<T>  *thisptr;

Any advice on this would be appreciated. I suspect this has something to do with how the template name T is used but am unsure of how to approach fixing this.


Solution

  • I guess the challenge is to correctly wrap a C++ template class Foo in cython in such a way python wrapper can handle both Foo<true> and Foo<false>.

    foo.hh

    #pragma once
    
    template <bool T>
    class Foo {
    public:
        static constexpr bool value = T;
    
        Foo() {}
        Foo(Foo&& x) = default;
        explicit Foo(const Foo<!T>&& x) {}
    };
    

    C++ header defines a template class Foo that depends on a boolean. where you have Foo<true> and Foo<false> as different types.

    main_decl.pxd The C++ template specializations as separate classes:

    cdef extern from "foo.hh" namespace "main":
        cdef cppclass FooTrue "Foo<true>":
            Foo()
            Foo(FooTrue& x)
            Foo(const FooFalse&& x)
    
        cdef cppclass FooFalse "Foo<false>":
            Foo()
            Foo(FooFalse& x)
            Foo(const FooTrue&& x)
    

    FooTrue is cython representation of Foo<true>, and FooFalse is for Foo<false>. That ensures the template argument is handled correctly.

    foo.pxd here you create Cython wrapper classes for the two C++ classes:

    from .main_decl cimport FooTrue, FooFalse
    
    cdef class PyFooTrue:
        cdef FooTrue* thisptr
    
    cdef class PyFooFalse:
        cdef FooFalse* thisptr
    

    PyFooTrue and PyFooFalse are classes that hold pointers to the C++ Foo<true> and Foo<false> objects.

    Foo.pyx PyFoo determines whether to use Foo<true> or Foo<false> based on the boolean T (at runtime):

    from .foo cimport FooTrue, FooFalse  # import the classes
    
    cdef class PyFoo:
        cdef FooTrue* thisptr_true
        cdef FooFalse* thisptr_false
        cdef bint is_true
    
        def __init__(self, bint T):
            if T:
                self.thisptr_true = new FooTrue()  # the correct class
                self.is_true = True
            else:
                self.thisptr_false = new FooFalse()  # the correct class
                self.is_true = False
    
        def __dealloc__(self):
            if self.is_true:
                del self.thisptr_true
            else:
                del self.thisptr_false
    

    depending on the boolean T, either FooTrue or FooFalse is instantiated. The C++ object is created and destroyed correctly.

    setup.py for Building the Extension:

    from setuptools import setup
    from Cython.Build import cythonize
    from setuptools.extension import Extension
    
    extensions = [
        Extension(
            name="foo.Foo",  # the package structure
            sources=["foo/Foo.pyx"],  # source path
            include_dirs=["foo", "."],  # include necessary directories
            language="c++",
        )
    ]
    
    setup(
        ext_modules=cythonize(extensions)
    )
    

    enter image description here