情况
我想使用 SWIG 为 C++ API 创建 Python 语言绑定。某些 API 函数可能会引发异常。C++ 应用程序具有自定义异常的层次结构,如下例所示:
std::exception
-> API::Exception
-> API::NetworkException
-> API::TimeoutException
-> API::UnreachableException
-> API::InvalidAddressException
所需的行为如下:
所有异常类型都应该有一个匹配的 Python 类作为wrapper。这些包装类应该是有效的 Python 异常。
当 API 调用引发C++ 异常时,应将其捕获。应抛出相应的Python 异常(即捕获的 C++ 异常的包装类)。
这应该是一个动态过程:Python异常类型是在运行时决定的,仅基于捕获的 C++ 异常的运行时类型。这样,无需在 SWIG 接口文件中描述完整的异常层次结构。
问题和疑问
包装类没有 Python 例外。
虽然 SWIG 为所有自定义异常创建包装类(就像任何其他类一样),但这些类不是 Python 异常。基本异常的包装器(
API::Exception
在示例中)扩展Object
而不是BaseException
Python 类,Python 中的所有异常都应从该类派生。此外,似乎不可能让 SWIG 手动添加父类。请注意,当通过使用 SWIG 和 Java 时,这是可能的
%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的反射部分在运行时按名称搜索 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 中给出了以下(预期的)行为:
>>> import numbers
>>> fact(11)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
Exception: C++ self-defined exception Value too big