pythonc++swig

Can a Python class derive from a SWIG-wrapped C++ class?


I am using SWIG version 4.0.2 in a Windows Subsystem for Linux (WSL) Ubuntu distribution. I can wrap a C++ class (EventProcessor), create an instance of that class in Python, and provide that instance to a wrapped global C++ function (registerEventProcessor()). But, can I define a class in Python that derives from the base EventProcessor class?

The simple C++ class and global function to wrap is represented by the following header file.

example4.h
#pragma once

class EventProcessor
{
private:
  int m_value;

public:
  EventProcessor( void )
  : m_value( 42 )
  {
  }

  int getValue( void ) const
  {
    return m_value;
  }

  void setValue( int value )
  {
    m_value = value;
  }
};

void registerEventProcessor( int packetId, EventProcessor * pProc )
{
  if ( pProc )
  {
    // Something simple to demonstrate that the function is successful:
    // Store the given packet ID.
    pProc->setValue( packetId );
  }
}

The SWIG interface file I use to wrap the C++ code is the following.

example4.swg
%module example4

%{
  #define SWIG_FILE_WITH_INIT

  #include "example4.h"
%}

%include "example4.h"

The command lines I use to generate the C++ wrapper code and then build the Python extension module are the following.

finch@laptop:~/work/swig_example$ swig -c++ -python example4.swg
finch@laptop:~/work/swig_example$ python3 example4_setup.py build_ext --inplace
running build_ext
building '_example4' extension
... a lot of output, no errors ...

Use base C++ class in Python - SUCCESS

The following is a demonstration that I can successfully create an instance of the base C++ class and provide that instance to the global function. The global function stores the given packet ID in the instance, which I can fetch afterwards.

finch@laptop:~/work/swig_example$ python3
Python 3.10.12 (main, Nov 20 2023, 15:14:05) [GCC 11.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import example4
>>> b = example4.EventProcessor()
>>> b.getValue()
42
>>> b.setValue( 20 )
>>> b.getValue()
20
>>> example4.registerEventProcessor( 30, b )
>>> b.getValue()
30

Use derived Python class - FAILURE

The derived Python class is represented by the following file.

derived4.py
import example4

class SOFProcessor( example4.EventProcessor ):
  def __init__( self ):
    print( "SOFProcessor.ctor" )

The following is a demonstration that I can create an instance of the derived Python class.

finch@laptop:~/work/swig_example$ python3
Python 3.10.12 (main, Nov 20 2023, 15:14:05) [GCC 11.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import example4
>>> import derived4
>>> d = derived4.SOFProcessor()
SOFProcessor.ctor

However, I cannot even fetch the value stored in that instance.

>>> d.getValue()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/finch/work/swig_example/example4.py", line 72, in getValue
    return _example4.EventProcessor_getValue(self)
TypeError: in method 'EventProcessor_getValue', argument 1 of type 'EventProcessor const *'

Neither can I provide that instance to the global function.

>>> example4.registerEventProcessor( 100, d )
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/finch/work/swig_example/example4.py", line 83, in registerEventProcessor
    return _example4.registerEventProcessor(packetId, pProc)
TypeError: in method 'registerEventProcessor', argument 2 of type 'EventProcessor *'

A typedef to override ... something?

Is there a typedef I can define in the SWIG interface file so that an instance of the derived Python class can be correctly wrapped and provided to the C++ side?


Solution

  • Following the excellent suggestion by @MarkTolonen, I read the documentation about directors. I realized the essential thing I was missing, while looking at the example Python code in that documentation.

    The base C++ class and global function are the same. The SWIG interface file is the same. The command lines I use to generate the C++ wrapper code and build the Python extension module are the same.

    The essential thing that fixes the problem is in the derived Python class. In its __init__() method, I must remember to call the base __init__() method.

    derived.py
    import example4
    
    class SOFProcessor( example4.EventProcessor ):
      def __init__( self ):
      super().__init__()
      print( "SOFProcessor.ctor" )
    

    Calling the base __init__() method is essential because that is where important SWIG-generated magic wrapper fixup code is called.

    1. example4.EventProcessor.__init__(), defined in the example4.py file automatically generated by SWIG, calls...
    2. _example4.EventProcessor_swiginit(), defined in the example4_wrap.cxx file automatically generated by SWIG, calls...
    3. SWIG_Python_InitShadowInstance(), also defined in example4_wrap.cxx, is where the magic happens. If I grokked it all, it would not be magic, would it?

    The following is a demonstration that, after creating an instance of the derived Python class, I can can fetch the value stored in that instance, and provide that instance to the global function.

    finch@laptop:~/work/swig_example$ python3
    Python 3.10.12 (main, Nov 20 2023, 15:14:05) [GCC 11.4.0] on linux
    Type "help", "copyright", "credits" or "license" for more information.
    >>> import example4
    >>> import derived4
    >>> d = derived4.SOFProcessor()
    SOFProcessor.ctor
    >>> d.getValue()
    42
    >>> example4.registerEventProcessor( 100, d )
    >>> d.getValue()
    100