如何在SWIG包装库中将C++异常传递给Python?

36 投票
6 回答
12538 浏览
提问于 2025-04-15 14:09

我正在为一个自定义的C++库编写一个SWIG封装,这个库定义了自己的C++异常类型。这个库的异常类型比标准异常更丰富、更具体。(比如,有一个类专门表示解析错误,并且包含了一系列行号。)我该如何将这些异常传回Python,同时保留异常的类型呢?

6 个回答

1

example.h

struct MyBaseException : public std::runtime_error {
  MyBaseException(const std::string& msg)
    : std::runtime_error{msg} {}
};

struct MyDerivedException : public MyBaseException {
  MyDerivedException(const std::string& msg)
    : MyBaseException{msg} {}
};

void foo() { throw MyBaseException{"my base exception"}; }
void bar() { throw MyDerivedException{"my derived exception"}; }
void baz() { throw std::runtime_error{"runtime error"}; }
void qux() { throw 0; }

example.i

%module example
%{
#include "example.h"
%}

%include <std_string.i>
%include <exception.i>

%exception {
  try {
    $action
  } catch (const MyDerivedException& e) {
     PyErr_SetString(SWIG_Python_ExceptionType(SWIGTYPE_p_MyDerivedException), e.what());
     SWIG_fail;
  } catch (const MyBaseException& e) {
     PyErr_SetString(SWIG_Python_ExceptionType(SWIGTYPE_p_MyBaseException), e.what());
     SWIG_fail;
  } catch(const std::exception& e) {
    SWIG_exception(SWIG_RuntimeError, e.what());
  } catch(...) {
    SWIG_exception(SWIG_UnknownError, "");
  }
}

%exceptionclass MyBaseException;

%include "example.h"

test.py

import example

try:
  example.foo()
except example.MyBaseException as e:
  print(e)

try:
  example.bar()
except example.MyDerivedException as e:
  print(e)

try:
  example.baz()
except RuntimeError as e:
  print(e)

try:
  example.qux()
except:
  print("unknown error")

python3 test.py

my base exception
my derived exception
runtime error
unknown error
13

我在这里补充一点,因为这里给出的例子现在说“%except(python)”已经不推荐使用了...

现在(截至swig 1.3.40),你可以进行完全通用的、与脚本语言无关的翻译。我的例子是:

%exception { 
    try {
        $action
    } catch (myException &e) {
        std::string s("myModule error: "), s2(e.what());
        s = s + s2;
        SWIG_exception(SWIG_RuntimeError, s.c_str());
    } catch (myOtherException &e) {
        std::string s("otherModule error: "), s2(e.what());
        s = s + s2;
        SWIG_exception(SWIG_RuntimeError, s.c_str());
    } catch (...) {
        SWIG_exception(SWIG_RuntimeError, "unknown exception");
    }
}

这将在任何支持的脚本语言中生成一个RuntimeError异常,包括Python,而不会在你的其他头文件中引入Python特有的内容。

你需要把这个放在想要进行异常处理的调用之前。

23

我知道这个问题已经有几周了,但我刚刚在为自己寻找解决方案时发现了它。所以我来试着回答一下,不过我得提前警告,这个解决方案可能不太好,因为使用swig的接口文件可能比手动编写包装器要复杂。此外,按照我所知,swig的文档从来没有直接处理用户自定义的异常。

假设你想从你的C++代码模块mylibrary.cpp中抛出一个异常,并附带一个小错误信息,以便在你的Python代码中捕获:

throw MyException("Highly irregular condition...");  /* C++ code */

MyException是一个在其他地方定义的用户异常。

在你开始之前,记得要注意这个异常在你的Python中应该如何被捕获。为了方便说明,假设你的Python代码如下:

import mylibrary
try:
    s = mylibrary.do_something('foobar')
except mylibrary.MyException, e:
    print(e)

我觉得这描述了你的问题。

我的解决方案涉及在你的swig接口文件(mylibrary.i)中添加四个部分,具体如下:

第一步: 在头部指令(通常没有名字的%{...%}块)中添加一个指向Python异常的指针声明,我们称之为pMyException。第二步将定义这个指针:

%{
#define SWIG_FILE_WITH_INIT  /* for eg */
extern char* do_something(char*);   /* or #include "mylibrary.h" etc  */
static PyObject* pMyException;  /* add this! */
%}

第二步: 添加一个初始化指令("m"这个部分特别麻烦,但这是swig v1.3.40在构建的包装文件中所需的) - pMyException是在第一步中声明的:

%init %{
    pMyException = PyErr_NewException("_mylibrary.MyException", NULL, NULL);
    Py_INCREF(pMyException);
    PyModule_AddObject(m, "MyException", pMyException);
%}

第三步: 如之前的帖子所提到的,我们需要一个异常指令 - 注意"%except(python)"已经不推荐使用。这将把C++函数"do_something"包裹在一个try-except块中,捕获C++异常并将其转换为第二步中定义的Python异常:

%exception do_something {
    try {
        $action
    } catch (MyException &e) {
        PyErr_SetString(pMyException, const_cast<char*>(e.what()));
        SWIG_fail;
    }
}

/* The usual functions to be wrapped are listed here: */
extern char* do_something(char*);

第四步: 因为swig在.pyd dll周围设置了一个Python包装(一个“影子”模块),我们还需要确保我们的Python代码可以“透视”到.pyd文件。以下方法对我有效,而且比直接编辑swig的Python包装代码更好:

%pythoncode %{
    MyException = _mylibrary.MyException
%}

这可能对原提问者来说已经太晚了,但也许其他人会发现上述建议有用。对于小项目,你可能更喜欢手动编写的C++扩展包装器的简洁,而不是swig接口文件的混乱。


补充:

在我上面的接口文件中,我省略了标准模块指令,因为我只想描述使异常工作所需的添加部分。但你的模块行应该看起来像:

%module mylibrary

另外,如果你使用distutils(我建议至少在开始时使用),你的setup.py应该有类似以下的代码,否则在无法识别_mylibrary时,第四步将失败:

/* setup.py: */
from distutils.core import setup, Extension

mylibrary_module = Extension('_mylibrary', extra_compile_args = ['/EHsc'],
                    sources=['mylibrary_wrap.cpp', 'mylibrary.cpp'],)

setup(name="mylibrary",
        version="1.0",
        description='Testing user defined exceptions...',
        ext_modules=[mylibrary_module],
        py_modules = ["mylibrary"],)

注意编译标志/EHsc,我在Windows上需要这个标志来编译异常。你的平台可能不需要这个标志;可以在谷歌上查找详细信息。

我使用自己的名称测试了代码,这里我将其转换为“mylibrary”和“MyException”,以帮助概括解决方案。希望转录错误很少或没有。主要要点是%init和%pythoncode指令,以及

static PyObject* pMyException;

在头部指令中。

希望这能澄清解决方案。

撰写回答