情境
我想使用SWIG为C++ API创建Python语言绑定。其中一些API函数可能会抛出异常。C++应用程序具有自定义异常的层次结构,例如以下示例:
std::exception
-> API::Exception
-> API::NetworkException
-> API::TimeoutException
-> API::UnreachableException
-> API::InvalidAddressException
期望的行为如下:
1. 所有异常类型都应该有一个匹配的Python类作为包装器。这些包装类应该是有效的Python异常。
2. 当API调用抛出C++异常时,应该捕获它。应该抛出相应的Python异常(即捕获的C++异常的包装类)。
3. 这应该是一个动态过程:Python异常类型在运行时决定,仅基于捕获的C++异常的运行时类型。这样,不需要在SWIG接口文件中描述完整的异常层次结构。
问题和疑问:
包装类不是Python异常。
尽管SWIG为所有自定义异常(就像为任何其他类一样)创建了包装类,但这些类不是Python异常。基本异常的包装(例如示例中的
API::Exception
)扩展Object
而不是BaseException
,Python中所有异常都应该派生自此类。此外,似乎不可能让SWIG手动添加父类。请注意,当使用带有Java的SWIG时,通过使用
%typemap(javabase)
可以实现这一点(详见SWIG文档)。Python C API如何抛出用户定义的异常?
从Python C API中抛出Python异常的最常见方法是调用
PyErr_SetString
[reference]。下面的演示应用程序也显示了这一点。但是,这仅对Python的标准(内置)异常来说是微不足道的,因为它们的引用存储在Python C API中的全局变量[reference]中。
我知道有一个方法
PyErr_NewException
[reference]可以获取自定义异常的引用,但我没有成功地实现。Python C API如何在运行时评估C++类型,然后按名称查找相应的Python包装类?
我假设可以通过Python C API的reflection部分在运行时按名称搜索Python类。这是正确的方法吗?它在实践中如何完成?
演示应用程序
为了研究这个问题,我创建了一个微小的C++ API,其中只有一个函数可以计算一个数字的阶乘。它有一个最小的自定义异常层次结构,只包含一个类TooBigException
。
请注意,此异常在一般问题中充当基本异常,并且应用程序应该与其任何子类一起工作。这意味着解决方案只能使用捕获的异常的动态(即运行时)类型在Python中重新抛出它(见下文)。
演示应用程序的完整源代码如下:
// File: numbers.h
namespace numbers {
int fact(int n);
}
// File: numbers.cpp
#include "TooBigException.h"
namespace numbers {
int fact(int n) {
if (n > 10) throw TooBigException("Value too big", n);
else if (n <= 1) return 1;
else return n*fact(n-1);
}
}
// File: TooBigException.h
namespace numbers {
class TooBigException: public std::exception {
public:
explicit TooBigException(const std::string & inMessage,
const int inValue);
virtual ~TooBigException() throw() {}
virtual const char* what() const throw();
const std::string & message() const;
const int value() const;
private:
std::string mMessage;
int mValue;
};
}
// File: TooBigException.cpp
#include "TooBigException.h"
namespace numbers {
TooBigException::TooBigException(const std::string & inMessage, const int inValue):
std::exception(),
mMessage(inMessage),
mValue(inValue)
{
}
const char* TooBigException::what() const throw(){
return mMessage.c_str();
}
const std::string & TooBigException::message() const {
return mMessage;
}
const int TooBigException::value() const {
return mValue;
}
}
为了获得我使用的Python绑定,我使用以下SWIG接口文件:
// File: numbers.i
%module numbers
%include "stl.i"
%include "exception.i"
%{
#define SWIG_FILE_WITH_INIT
#include "TooBigException.h"
#include "numbers.h"
%}
%exception {
try {
$action
}
catch (const numbers::TooBigException & e) {
// This catches any self-defined exception in the exception hierarchy,
// because they all derive from this base class.
<TODO>
}
catch (const std::exception & e)
{
SWIG_exception(SWIG_RuntimeError, (std::string("C++ std::exception: ") + e.what()).c_str());
}
catch (...)
{
SWIG_exception(SWIG_UnknownError, "C++ anonymous exception");
}
}
%include "TooBigException.h"
%include "numbers.h"
每次调用API都会被包裹在try-catch块中。首先捕获和处理我们基础类型的异常,然后使用SWIG异常库捕获并重新抛出所有其他异常。
注意,任何
numbers :: TooBigException
的子类都将被捕获,并且应该抛出其动态(即运行时)类型的封装,而不是其静态(即编译时)类型的封装,后者始终为TooBigException
!可以通过在Linux机器上执行以下命令轻松构建项目:
$ swig -c++ -python numbers.i
$ g++ -fPIC -shared TooBigException.cpp numbers.cpp numbers_wrap.cxx \
-I/usr/include/python2.7 -o _numbers.so
当前实现
我的当前实现仍然(成功地)抛出一个固定的标准Python异常。上面的代码<TODO>
然后被替换为:
PyErr_SetString(PyExc_Exception, (std::string("C++ self-defined exception ") + e.what()).c_str());
return NULL;
以下是Python中(预期的)行为:
这个例子展示了Python中的行为:
>>> import numbers
>>> fact(11)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
Exception: C++ self-defined exception Value too big
throw;
- PlasmaHH