I am developing a server-client application in which the client calls a server's API which gives a Python interface for user input. It means the client interface and server interface is written in Python whereas the socket code is in C++.
On the server side:-
I have a class, Test, in C++ and this class is inherited in Python named TestPython using director feature of SWIG.
Also I have an exception class MyException in C++.
Now a function of TestPython class throws MyException() from Python code.
I want to handle exception thrown from Python in C++ code using SWIG.
Below is code snippet:
C++ Code-
class MyException
{
public:
string errMsg;
MyException();
MyException(string);
~MyException();
};
class Test
{
int value;
public:
void TestException(int val);
Test(int);
};
Python Code -
class TestPython(Test):
def __init__(self):
Test.__init__(self)
def TestException(self,val):
if val > 20:
throw MyException("MyException : Value Exceeded !!!")
else:
print "Value passed = ",val
Now, if the TestException() function is called, it should throw MyException. I want to handle this MyException() exception in my C++ code.
So can anyone suggest my how to do that, I mean what should I write in my *.i(interface) file to handle this.
The above TestException() written in Python is called by the client, so I have to notify the client if any exception is thrown by the server.
To do this you basically need to write a %feature("director:except") that can handle a Python exception and re-throw it as a C++ one. Here's a small but complete example:
Suppose we have the following header file we wish to wrap:
#include <iostream>
#include <exception>
class MyException : public std::exception {
};
class AnotherException : public std::exception {
};
class Callback {
public:
virtual ~Callback() { std::cout << "~Callback()" << std:: endl; }
virtual void run() { std::cout << "Callback::run()" << std::endl; }
};
inline void call(Callback *callback) { if (callback) callback->run(); }
And this Python code that uses it:
import example
class PyCallback(example.Callback):
def __init__(self):
example.Callback.__init__(self)
def run(self):
print("PyCallback.run()")
raise example.MyException()
callback = PyCallback()
example.call(callback)
We can define the following SWIG interface file:
%module(directors="1") example
%{
#include "example.h"
%}
%include "std_string.i"
%include "std_except.i"
%include "pyabc.i"
// Python requires that anything we raise inherits from this
%pythonabc(MyException, Exception);
%feature("director:except") {
PyObject *etype = $error;
if (etype != NULL) {
PyObject *obj, *trace;
PyErr_Fetch(&etype, &obj, &trace);
Py_DecRef(etype);
Py_DecRef(trace);
// Not too sure if I need to call Py_DecRef for obj
void *ptr;
int res = SWIG_ConvertPtr(obj, &ptr, SWIGTYPE_p_MyException, 0);
if (SWIG_IsOK(res) && ptr) {
MyException *e = reinterpret_cast< MyException * >(ptr);
// Throw by pointer (Yucky!)
throw e;
}
res = SWIG_ConvertPtr(obj, &ptr, SWIGTYPE_p_AnotherException, 0);
if (SWIG_IsOK(res) && ptr) {
AnotherException *e = reinterpret_cast< AnotherException * >(ptr);
throw e;
}
throw Swig::DirectorMethodException();
}
}
%feature("director") Callback;
%include "example.h"
Which handles an error from a director call, looks to see if it was one of our MyException instances and then re-throws the pointer if it was. If you have multiple types of exception being thrown then you will probably need to use PyErr_ExceptionMatches to work out what type it is first.
We could throw also by value or reference using:
// Throw by value (after a copy!)
MyException temp = *e;
if (SWIG_IsNewObj(res))
delete e;
throw temp;
instead, but note that if you threw a subclass of MyException in Python this would fall foul of the object slicing problem.
I'm not quite sure if the code is 100% correct - in particular I think the reference counting is correct, but I could be wrong.
Note: In order to make this example work (%pythonabc wouldn't work otherwise) I had to call SWIG with -py3. This in turn meant I had to upgrade to SWIG 2.0, because my installed copy of Python 3.2 had removed some deprecated functions from the C-API that SWIG 1.3.40 called.
Related
Given those interfaces:
class ITemperature
{
public:
virtual ~ITemperature() = deafult;
virtual int get_temp() const = 0;
};
class IHumidity
{
public:
virtual ~IHumidity() = deafult;
virtual int get_humidity() const = 0;
};
And this SUT:
class SoftwareUnderTest
{
public:
SoftwareUnderTest(std::unique_ptr<ITemperature> p_temp,
std::unique_ptr<IHumidity> p_humidity)
: m_temp{std::move(p_temp)}, m_humidity{std::move(p_humidity)}
{}
bool checker()
{
assert(m_temp && "No temperature!");
if (m_temp->get_temp() < 50)
{
return true;
}
assert(m_humidity && "No humidity");
if (m_humidity->get_humidity() < 50)
{
return true;
}
return false;
}
private:
std::unique_ptr<ITemperature> m_temp;
std::unique_ptr<IHumidity> m_humidity;
};
And this mocks:
class MockITemperature : public ITemperature
{
public:
MOCK_METHOD(int, get_temp, (), (const override));
};
class MockIHumidity : public IHumidity
{
public:
MOCK_METHOD(int, get_humidity, (), (const override));
};
I want to make a test that checks that get_temp is called and also that the second assert (the one that checks that the humidity is nullptr), but when a do this test, I get the assert, but I the expectation tells me that it's never called (but it is actually called once)
this is the test:
class Fixture : pu`blic testing::Test
{
protected:
void SetUp() override
{
m_sut = std::make_unique<SoftwareUnderTest>(m_mock_temperature, m_mock_humidity);
}
std::unique_ptr<StrickMockOf<MockITemperature>> m_mock_temperature = std::make_shared<StrickMockOf<MockITemperature>>();
std::unique_ptr<StrickMockOf<MockIHumidity>> m_mock_humidity;
std::unique_ptr<SoftwareUnderTest> m_sut;
};
TEST_F(Fixture, GIVEN_AnInvalidHumidityInjection_THEN_TestMustDie)
{
EXPECT_CALL(*m_mock_temperature, get_temp).Times(1);
ASSERT_DEATH(m_sut->checker(), "No humidity");
}
Apparently, this is a known limitation, see here and here.
From what I have managed to discover by experimentation so far:
If you can live with the error message about leaking mocks (haven't checked if it's true or a false positive, suppressing it by AllowLeak triggers the actual crash), it can be done by making the mocks outlive the test suite and then wrapping references/pointers to them in one more interface implementation.
//mocks and SUT as they were
namespace
{
std::unique_ptr<testing::StrictMock<MockIHumidity>> mock_humidity;
std::unique_ptr<testing::StrictMock<MockITemperature>> mock_temperature;
}
struct MockITemperatureWrapper : MockITemperature
{
MockITemperatureWrapper(MockITemperature* ptr_) : ptr{ptr_} {assert(ptr);}
int get_temp() const override { return ptr->get_temp(); }
MockITemperature* ptr;
};
struct Fixture : testing::Test
{
void SetUp() override
{
mock_temperature
= std::make_unique<testing::StrictMock<MockITemperature>>();
m_mock_temperature = mock_temperature.get();
// testing::Mock::AllowLeak(m_mock_temperature);
m_sut = std::make_unique<SoftwareUnderTest>(
std::make_unique<MockITemperatureWrapper>(m_mock_temperature), nullptr);
}
testing::StrictMock<MockITemperature>* m_mock_temperature;
std::unique_ptr<SoftwareUnderTest> m_sut;
};
TEST_F(Fixture, GIVEN_AnInvalidHumidityInjection_THEN_TestMustDie)
{
EXPECT_CALL(*m_mock_temperature, get_temp).WillOnce(testing::Return(60));
ASSERT_DEATH(m_sut->checker(), "No humidity");
}
https://godbolt.org/z/vKnP7TsrW
Another option would be passing a lambda containing the whole to ASSERT_DEATH:
TEST_F(Fixture, GIVEN_AnInvalidHumidityInjection_THEN_TestMustDie)
{
ASSERT_DEATH(
[this] {
EXPECT_CALL(*m_mock_temperature, get_temp)
.WillOnce(testing::Return(60));
m_sut->checker();
}(), "No humidity");
}
Works, but looks ugly, see here.
Last but not least: one can use custom assert or replace__assert_failed function and throw from it (possibly some custom exception), then use ASSERT_THROW instead of ASSERT_DEATH. While I'm not sure replacing __assert_failed is legal standard-wise (probably not), it works in practice:
struct AssertFailed : std::runtime_error
{
using runtime_error::runtime_error;
};
void __assert_fail(
const char* expr,
const char *filename,
unsigned int line,
const char *assert_func )
{
std::stringstream conv;
conv << expr << ' ' << filename << ' ' << line << ' ' << assert_func;
throw AssertFailed(conv.str());
}
Example: https://godbolt.org/z/Tszv6Echj
I want to make a test that checks that get_temp is called and also
that the second assert (the one that checks that the humidity is
nullptr), but when a do this test, I get the assert, but I the
expectation tells me that it's never called (but it is actually called
once)
First you have to understand how death test are working.
Before executing code in a macro ASSERT_DEATH gtest forks test process so when death happens test can continue.
Then forked process is executing code which should lead to process death.
Now test process joins forked process to see result.
Outcome is that in one process checker() is executed and mock is invoked and in test process it is checker() is not invoked so also mock is not invoked. That is why you get an error that mock is not satisfied.
Now answer from alager makes mock eternal so missing expected call is not reported. And since code uses global state adding other tests will lead to some problems. So I would not recommend this approach.
After edit he moved EXPECT_CALL inside ASSERT_DEATH so now only forked process expects call, but this is not verified since process dies before mock is verified. So again I would not recommend this approach either.
So question is what you should do? IMO your problem is that you are testing to much of implementation details. You should loosen test requirement (drop StrictMock or make it even make it NiceMock). Still I find this a bit clunky. Live demo
I would change code in such way that it is impossible to construct SoftwareUnderTest with nullptr dependencies. You can use gsl::not_null for that.
It seems to be due to some tricky mechanism that is used in googletest to assert death (they mention creating a child process). I did not find a way to fix it correctly, but I found one (not so great) workaround:
SoftwareUnderTest(ITemperature* p_temp, IHumidity* p_humidity) // changed signature to allow leaks, I guess you cannot really do it in the production
and then:
class Fixture : public testing::Test
{
public:
Fixture(): m_mock_temperature(new MockITemperature), m_mock_humidity(nullptr) {}
~Fixture() {
// Mock::VerifyAndClearExpectations(m_mock_temperature); // if I uncomment that, for some reason the test will fail anyway
std::cout << "Dtor" << std::endl;
// delete m_mock_temperature; // if I delete the mock correctly, the test will fail
}
protected:
void SetUp() override
{
// m_sut.reset(new SoftwareUnderTest(m_mock_temperature.get(), m_mock_humidity.get()));
m_sut.reset(new SoftwareUnderTest(m_mock_temperature, m_mock_humidity));
}
// std::unique_ptr<MockITemperature> m_mock_temperature; // if I use smart pointers, the test will fail
// std::unique_ptr<MockIHumidity> m_mock_humidity;
MockITemperature* m_mock_temperature;
MockIHumidity* m_mock_humidity;
std::unique_ptr<SoftwareUnderTest> m_sut;
};
TEST_F(Fixture, GIVEN_AnInvalidHumidityInjection_THEN_TestMustDie)
{
EXPECT_CALL(*m_mock_temperature, get_temp).Times(1).WillOnce(Return(60)); // this is to ensure to go over first check, seems you forgot
ASSERT_DEATH(m_sut->checker(), "No humidity");
std::cout << "after checks" << std::endl;
}
Sorry, that's all I could figure out for the moment. Maybe you can submit a new issue in gtest github while waiting for a better answer.
I'm unable to check the exception throw by my code in gtests. Here's a snippet of the test suite which runs the test:
EXPECT_THROW({
try{
// Insert a tuple with more target columns than values
rows_changed = 0;
query = "INSERT INTO test8(num1, num3) VALUES(3);";
txn = txn_manager.BeginTransaction();
plan = TestingSQLUtil::GeneratePlanWithOptimizer(optimizer, query, txn);
EXPECT_EQ(plan->GetPlanNodeType(), PlanNodeType::INSERT);
txn_manager.CommitTransaction(txn);
TestingSQLUtil::ExecuteSQLQueryWithOptimizer(
optimizer, query, result, tuple_descriptor, rows_changed, error_message);
}
catch (CatalogException &ex){
EXPECT_STREQ("ERROR: INSERT has more target columns than expressions", ex.what());
}
}, CatalogException);
I'm pretty sure that CatalogException is thrown. I even tried getting the details of the thrown exception by outputting it to cerr, and it showed Exception Type: Catalog.
This is not a duplicate question, I searched for answers on SO and I'm not using new in my code which throws the error. Here's the snippet which does that:
if (columns->size() < tup_size)
throw CatalogException(
"ERROR: INSERT has more expressions than target columns");
Finally, here's the definition of CatalogException:
class CatalogException : public Exception {
CatalogException() = delete;
public:
CatalogException(std::string msg) : Exception(ExceptionType::CATALOG, msg) {}
};
The idea from EXPECT_THROW is, that the macro catches the exception. If you catch the exception by yourself, gmock don't now anything about a thrown exception.
I suggest to just write the statement into the EXPECT_THROW, which actually trigger the exception. Everything else can be written before.
For example:
TEST(testcase, testname)
{
//arrange everything:
//...
//act + assert:
EXPECT_THROW(TestingSQLUtil::ExecuteSQLQueryWithOptimizer( optimizer, query, result,
tuple_descriptor, rows_changed, error_message)
,CatalogException);
}
I assume, that TestingSQLUtil::ExecuteSQLQueryWithOptimizer is trigger the thrown exception.
addition:
I tried to rebuild your exception hierarchy. This example works for me very well. The test passes, which means the exception is thrown.
enum class ExceptionType
{
CATALOG
};
class Exception {
public:
Exception(ExceptionType type, std::string msg) {}
};
class CatalogException : public Exception {
CatalogException() = delete;
public:
CatalogException(std::string msg) : Exception(ExceptionType::CATALOG, msg) {}
};
void testThrow() {
throw CatalogException( "ERROR: INSERT has more expressions than target columns");
}
TEST(a,b) {
EXPECT_THROW( testThrow(), CatalogException);
}
I'd like to write a unit-test for a method that prints to the standard output.
I have already changed the code so it prints to a passed-in File instance instead that is stdout by default. The only thing I am missing is some in-memory File instance that I could pass-in. Is there such a thing? Any recommendation? I wish something like this worked:
import std.stdio;
void greet(File f = stdout) {
f.writeln("hello!");
}
unittest {
greet(inmemory);
assert(inmemory.content == "hello!\n")
}
void main() {
greet();
}
Any other approach for unit-testing code that prints to stdout?
Instead of relying on File which is quite a low level type, pass the object in via an interface.
As you have aluded to in your comment OutputStreamWriter in Java is a wrapper of many interfaces designed to be an abstraction over byte streams, etc. I'd do the same:
interface OutputWriter {
public void writeln(string line);
public string #property content();
// etc.
}
class YourFile : OutputWriter {
// handle a File.
}
void greet(ref OutputWriter output) {
output.writeln("hello!");
}
unittest {
class FakeFile : OutputWriter {
// mock the file using an array.
}
auto mock = new FakeFile();
greet(inmemory);
assert(inmemory.content == "hello!\n")
}
In my implementation for python integration into a c++ application, I am adding support for nodes that might or might not be valid. Internally these are stored as weak pointers, so I was thinking of having an isValid() method that users can use before calling the exposed methods. If they call an exposed method on an invalid node it would throw an exception.
However, I was wondering if it's possible to be a bit more pythonic than that. Is it possible to internally check whether the pointer is valid before calling the exposed method, and if it isn't making the python object None?
An example of what I want is here:
>>> my_valid_node = corelibrary.getNode("valid_node")
>>> my_valid_node.printName()
valid_node
Now however, something somewhere else in the system might invalidate the node, but from python's point of view, I want the node to become None.
>>> my_valid_node.printName()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'NoneType' object has no attribute 'printName'
Can anyone think of a way to do this?
There is no clean way to make a reference to an object become a reference to None when an external event occurs. Nevertheless, when working towards a Pythonic interface, one could:
Implement the __nonzero__ method to allow the object to be evaluated in a boolean context.
Throw a Python exception when the weak_ptr fails to lock. One simple solution, would be to access a member attribute on a default constructed boost::python::object, as it references None.
Note that attribute lookup customization points, such as __getattr__, will not be sufficient enough as the object pointed to by weak_ptr may expire between attribute access and dispatch to the C++ member functions.
Here is a complete minimal example based on the above details. In this example, spam and spam_factory, a factory that instantiates spam objects managed by shared_ptr, are considered legacy types. A spam_proxy auxiliary class that references spam via weak_ptr and additionally auxiliary functions help adapt the legacy types to Python.
#include <string>
#include <boost/make_shared.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/smart_ptr/weak_ptr.hpp>
#include <boost/python.hpp>
/// Assume legacy APIs.
// Mockup class containing data.
class spam
{
public:
explicit spam(const char* name)
: name_(name)
{}
std::string name() { return name_; }
private:
std::string name_;
};
// Factory for creating and destroying the mockup class.
class spam_factory
{
public:
boost::shared_ptr<spam> create(const char* name)
{
instance_ = boost::make_shared<spam>(name);
return instance_;
}
void destroy()
{
instance_.reset();
}
private:
boost::shared_ptr<spam> instance_;
};
/// Auxiliary classes and functions to help obtain Pythonic semantics.
// Helper function used to cause a Python AttributeError exception to
// be thrown on None.
void throw_none_has_no_attribute(const char* attr)
{
// Attempt to extract the specified attribute on a None object.
namespace python = boost::python;
python::object none;
python::extract<python::object>(none.attr(attr))();
}
// Mockup proxy that has weak-ownership.
class spam_proxy
{
public:
explicit spam_proxy(const boost::shared_ptr<spam>& impl)
: impl_(impl)
{}
std::string name() const { return lock("name")->name(); }
bool is_valid() const { return !impl_.expired(); }
boost::shared_ptr<spam> lock(const char* attr) const
{
// Attempt to obtain a shared pointer from the weak pointer.
boost::shared_ptr<spam> impl = impl_.lock();
// If the objects lifetime has ended, then throw.
if (!impl) throw_none_has_no_attribute(attr);
return impl;
}
private:
boost::weak_ptr<spam> impl_;
};
// Use a factory to create a spam instance, but wrap it in the proxy.
spam_proxy spam_factory_create(
spam_factory& self,
const char* name)
{
return spam_proxy(self.create(name));
}
BOOST_PYTHON_MODULE(example)
{
namespace python = boost::python;
// Expose the proxy class as if it was the actual class.
python::class_<spam_proxy>("Spam", python::no_init)
.def("__nonzero__", &spam_proxy::is_valid)
.add_property("name", &spam_proxy::name)
;
python::class_<spam_factory>("SpamFactory")
.def("create", &spam_factory_create) // expose auxiliary method
.def("destroy", &spam_factory::destroy)
;
}
Interactive usage:
>>> import example
>>> factory = example.SpamFactory()
>>> spam = factory.create("test")
>>> assert(spam.name == "test")
>>> assert(bool(spam) == True)
>>> if spam:
... assert(bool(spam) == True)
... factory.destroy() # Maybe occurring from a C++ thread.
... assert(bool(spam) == False) # Confusing semantics.
... assert(spam.name == "test") # Confusing exception.
...
Traceback (most recent call last):
File "<stdin>", line 5, in <module>
AttributeError: 'NoneType' object has no attribute 'name'
>>> assert(spam is not None) # Confusing type.
One could argue that while the interface is Pythonic, the object's semantics are not. With weak_ptr semantics not being too common in Python, one generally does not expect the object referenced by a local variable to be destructed. If weak_ptr semantics are necessary, then consider introducing a means to allow the user to obtain shared ownership within a specific context via the context manager protocol. For example, the following pattern allows an object's validity to be checked once, then be guaranteed within a limited scope:
>>> with spam: # Attempt to acquire shared ownership.
... if spam: # Verify ownership was obtained.
... spam.name # Valid within the context's scope.
... factory.destroy() # spam is still valid.
... spam.name # Still valid.
... # spam destroyed once context's scope is exited.
Here is a complete extension of the previous example, where in the spam_proxy implements the context manager protocol:
#include <string>
#include <boost/make_shared.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/smart_ptr/weak_ptr.hpp>
#include <boost/python.hpp>
/// Assume legacy APIs.
// Mockup class containing data.
class spam
{
public:
explicit spam(const char* name)
: name_(name)
{}
std::string name() { return name_; }
private:
std::string name_;
};
// Factory for creating and destroying the mockup class.
class spam_factory
{
public:
boost::shared_ptr<spam> create(const char* name)
{
instance_ = boost::make_shared<spam>(name);
return instance_;
}
void destroy()
{
instance_.reset();
}
private:
boost::shared_ptr<spam> instance_;
};
/// Auxiliary classes and functions to help obtain Pythonic semantics.
// Helper function used to cause a Python AttributeError exception to
// be thrown on None.
void throw_none_has_no_attribute(const char* attr)
{
// Attempt to extract the specified attribute on a None object.
namespace python = boost::python;
python::object none;
python::extract<python::object>(none.attr(attr))();
}
// Mockup proxy that has weak-ownership and optional shared ownership.
class spam_proxy
{
public:
explicit spam_proxy(const boost::shared_ptr<spam>& impl)
: shared_impl_(),
impl_(impl)
{}
std::string name() const { return lock("name")->name(); }
bool is_valid() const { return !impl_.expired(); }
boost::shared_ptr<spam> lock(const char* attr) const
{
// If shared ownership exists, return it.
if (shared_impl_) return shared_impl_;
// Attempt to obtain a shared pointer from the weak pointer.
boost::shared_ptr<spam> impl = impl_.lock();
// If the objects lifetime has ended, then throw.
if (!impl) throw_none_has_no_attribute(attr);
return impl;
}
void enter()
{
// Upon entering the runtime context, guarantee the lifetime of the
// object remains until the runtime context exits if the object is
// alive during this call.
shared_impl_ = impl_.lock();
}
bool exit(boost::python::object type,
boost::python::object value,
boost::python::object traceback)
{
shared_impl_.reset();
return false; // Do not suppress the exception.
}
private:
boost::shared_ptr<spam> shared_impl_;
boost::weak_ptr<spam> impl_;
};
// Use a factory to create a spam instance, but wrap it in the proxy.
spam_proxy spam_factory_create(
spam_factory& self,
const char* name)
{
return spam_proxy(self.create(name));
}
BOOST_PYTHON_MODULE(example)
{
namespace python = boost::python;
// Expose the proxy class as if it was the actual class.
python::class_<spam_proxy>("Spam", python::no_init)
.def("__nonzero__", &spam_proxy::is_valid)
// Support context manager protocol.
.def("__enter__", &spam_proxy::enter)
.def("__exit__", &spam_proxy::exit)
.add_property("name", &spam_proxy::name)
;
python::class_<spam_factory>("SpamFactory")
.def("create", &spam_factory_create) // expose auxiliary method
.def("destroy", &spam_factory::destroy)
;
}
Interactive usage:
>>> import example
>>> factory = example.SpamFactory()
>>> spam = factory.create("test")
>>> with spam:
... assert(bool(spam) == True)
... if spam:
... assert(spam.name == "test")
... factory.destroy()
... assert(bool(spam) == True)
... assert(spam.name == "test")
...
>>> assert(bool(spam) == False)
The exact pattern may not be the most Pythonic, but it provides a clean way to guarantee the object's lifetime within a limited scope.
The observer pattern appears frequently in my C++ project, which I now want to expose to the Python interpreter via Cython bindings. I tried to construct a minimal example illustrating the situation. A Spectacle accepts any object derived from the abstract base class Observer, such as an Onlooker. When we call Spectacle::event(), each registered observer is notified.
This is the content of the file ObserverPattern.h:
class Spectacle {
private:
std::vector<Observer*> observers;
public:
Spectacle() {};
virtual ~Spectacle() {};
virtual void registerObserver(Observer* observer) {
this->observers.push_back(observer);
}
virtual void event() {
std::cout << "event triggered" << std::endl;
for (Observer* observer : this->observers) {
observer->onEvent();
}
}
};
class Observer {
public:
Observer() {};
virtual ~Observer() {};
virtual void onEvent() = 0;
};
class Onlooker : public Observer {
public:
Onlooker() {};
virtual ~Onlooker() {};
virtual void onEvent() {
std::cout << "event observed" << std::endl;
}
};
And this is the content of my .pyx file, containing the bindings:
cdef extern from "ObserverPattern.h":
cdef cppclass _Spectacle "Spectacle":
_Spectacle() except +
void registerObserver(_Observer* observer)
void event()
cdef extern from "ObserverPattern.h":
cdef cppclass _Observer "Observer":
_Observer() except +
void onEvent()
cdef extern from "ObserverPattern.h":
cdef cppclass _Onlooker "Onlooker":
_Onlooker() except +
void onEvent()
cdef class Spectacle:
cdef _Spectacle _this
def event(self):
self._this.event()
def registerObserver(self, Observer observer):
self._this.registerObserver(observer._this)
cdef class Observer:
cdef _Observer* _this # must be a pointer because _Observer has pure virtual method
cdef class Onlooker(Observer):
pass # what should be the class body?
This does compile, but segfaults when event() is called and the observers are notified:
>>> spec = CythonMinimal.Spectacle()
>>> look = CythonMinimal.Onlooker()
>>> spec.registerObserver(look)
>>> spec.event()
event triggered
Segmentation fault: 11
What is the problem here and how could a fix look like?
Your problem is essentially "implement a C++ interface in Python".
The only portable way to do this is to write an actual C++ class
that will call back into Python.
Cython has undocumented experimental_cpp_class_def option that allows to
create C++ classes using Cython syntax. It's not pretty (IMO), but it works
for many scenarios.
Here is how you could implement Observer that delegates to the provided
Python callable:
from cpython.ref cimport PyObject, Py_INCREF, Py_DECREF
cdef cppclass ObserverImpl(_Observer):
PyObject* callback
__init__(object callback): # constructor. "this" argument is implicit.
Py_INCREF(callback)
this.callback = <PyObject*>callback
__dealloc__(): # destructor
Py_DECREF(<object>this.callback)
void onEvent():
(<object>this.callback)() # exceptions will be ignored
And that's how you could use it:
def registerObserver(self, callback not None): # user passes any Python callable
self._this.registerObserver(new ObserverImpl(callback))
C++ objects, just like C structures, can't hold Cython-managed object
references. That's why you have to use PyObject* field and manage reference
counting yourself. Inside methods you can, of course, cast to and use any Cython feature.
Another tricky moment is exception propagation. onEvent() method, being defined in C++, can't propagate Python exceptions. Cython will simply ignore exceptions it can't propagate. If you want to do better, catch them yourself and store somewhere for later inspection or rethrow as C++ exception. (I think it's impossible to throw C++ exceptions in Cython syntax, but you can call an external throwing helper function.)
If your observer has more than one method, then callback would be a Python class and instead of calling it directly you would call its methods, like (<object>this.callback).onEvent().
Obviously, ObserverImpl can also be coded directly in C++. Py_INCREF, Py_DECREF and PyObject_Call/PyObject_CallMethod are the only Python APIs necessary.