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

36

我正在编写一个SWIG包装器,用于封装一个自定义的C++库,该库定义了自己的C++异常类型。该库的异常类型比标准异常更丰富和具体化。(例如,其中一个类表示解析错误,并具有一系列行号。) 我如何保留异常类型将这些异常传递回Python?

6个回答

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) 进行四个补充,具体如下:

步骤 1: 在头指令 (通常未命名的 %{...%} 块) 中添加指向 Python 感知异常的指针声明,我们将其称为 pMyException。步骤 2 将定义这个指针:

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

步骤2: 添加一个初始化指令("m"特别恶心,但这是swig v1.3.40当前在其构造的包装器文件中需要的)。pMyException在步骤1中声明:

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

第三步: 正如早期的帖子中提到的那样,我们需要一个异常指令-请注意,"%except(python)"已被弃用。这将把c+++函数“do_something”包装在try-except块中,该块捕获c++异常并将其转换为上述第2步定义的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*);

第4步: 因为swig在.pyd dll周围设置了一个Python封装(一个“阴影”模块),我们还需要确保我们的Python代码可以“看穿”到.pyd文件。以下方法对我有效,并且最好不要直接编辑swig py封装代码:

%pythoncode %{
    MyException = _mylibrary.MyException
%}

对于原帖作者来说,这可能已经太晚了,但或许其他人会发现上面的建议有所帮助。对于小的任务,您可能更喜欢手写C++扩展包装器的清晰性,而不是SWIG接口文件的混乱。


补充:

在我上面列出的接口文件中,我省略了标准模块指令,因为我只想描述必要的添加内容以使异常起作用。但是,您的模块行应该类似于:

%module mylibrary

另外,如果您正在使用distutils(至少在开始时我建议使用它),则您的setup.py应该具有类似以下代码的内容,否则当_mylibrary未被识别时,步骤4将失败:

/* 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 上需要编译异常。你的平台可能不需要这个标志; google 有详细信息。
我已使用自己的名称测试了该代码,我在此处将它们转换为“mylibrary”和“MyException”,以帮助概括解决方案。希望转录错误为少数或没有。主要重点是 %init 和 %pythoncode 指令。
static PyObject* pMyException;
在头文件指令中。希望这能澄清解决方案。

这看起来非常有前途--我会试一下。顺便说一句,现在还不算太晚,因为我们只是捕获RuntimeError并检查错误消息的文本--很糟糕。 - Barry
用一个名为“SWIG_fail”的宏替换你在调用“PyErr_SetString”后使用的“return NULL”,这样更通用,即无论返回的数据类型如何都可以正常工作。 - Hermes

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特定内容的情况下。

您需要在想要进行异常处理的调用之前放置此代码。


2
+1,只需要确保在他/她的SWIG接口文件中包含%include exception.i(引入SWIG_exception宏)。 - Shmil The Cat

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

0

来自Swig文档

%except(python) {
try {
$function
}
catch (RangeError) {
    PyErr_SetString(PyExc_IndexError,"index out-of-bounds");
    return NULL;
}
}

这很接近了。它保留了异常的类型,但是如何封装自定义异常类型呢?也许 SWIG 的示例可以涵盖这个问题。 - Barry

0

-1

您也可以使用:

捕获错误:http://www.swig.org/Doc3.0/SWIGPlus.html#SWIGPlus_catches

示例:

%catches(std::exception, std::string, int, ...);

为每个函数生成一个try catch块:

  try {
    result = (namespace::Function *)new namespace::Function ((uint16_t const *)arg1);
  }
  catch(std::exception &_e) {
    SWIG_exception_fail(SWIG_SystemError, (&_e)->what());
  }
  catch(std::string &_e) {
    SWIG_Python_Raise(SWIG_From_std_string(static_cast< std::string >(_e)), "std::string", 0); SWIG_fail;
  }
  catch(int &_e) {
    SWIG_Python_Raise(SWIG_From_int(static_cast< int >(_e)), "int", 0); SWIG_fail;
  }
  catch(...) {
    SWIG_exception_fail(SWIG_RuntimeError,"unknown exception");
  }

-1:这不起作用,Python崩溃了。除非我在函数声明中指定throw(),否则%catches是不够的。而且,在这种情况下,%catches是多余的。链接的SWIG文档仅提到使用%catches指令与函数名一起使用。 - Melebius
%catches 实际上比 %exception 包装更多的 SWIG 包装函数异常。 - hugo24

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接