【问题标题】:Calling exit() in C++ library terminates python script that wrapps that library using swig在 C++ 库中调用 exit() 会终止使用 swig 包装该库的 python 脚本
【发布时间】:2016-03-06 17:57:13
【问题描述】:

我正在为 C++ 库编写 Swig-Python 包装器。当发生严重错误时,库调用exit(err);,这反过来会终止执行该库中的函数的整个python脚本。

有没有办法环绕exit()函数返回脚本或抛出异常?

【问题讨论】:

  • 这可能对你有帮助:Override a function call in C
  • 当您有异常可用时从库中调用exit() 表明作者可能对错误处理一无所知。考虑修复该库或替换它。

标签: python c++ swig


【解决方案1】:

您可以使用 longjmpon_exit 对此进行大规模破解,但我强烈建议您避免这样做,而是使用具有多个进程的解决方案,我将在后面的答案中概述。

假设我们有以下(按设计破坏)头文件:

#ifndef TEST_H
#define TEST_H

#include <stdlib.h>

inline void fail_test(int fail) {
  if (fail) exit(fail);
}

#endif//TEST_H

我们想要包装它并将对exit() 的调用转换为 Python 异常。实现此目的的一种方法类似于以下接口,它使用%exception 在 Python 接口对每个 C 函数的调用周围插入 C 代码:

%module test

%{
#include "test.h"
#include <setjmp.h>

static __thread int infunc = 0;
static __thread jmp_buf buf;

static void exithack(int code, void *data) {
  if (!infunc) return;
  (void)data;
  longjmp(buf,code);
}
%}

%init %{
  on_exit(exithack, NULL);
%}

%exception {
  infunc = 1;
  int err = 0;
  if (!(err=setjmp(buf))) {
    $action
  }
  else {
    // Raise exception, code=err
    PyErr_Format(PyExc_Exception, "%d", err);
    infunc = 0;
    on_exit(exithack, NULL);
    SWIG_fail;
  }
  infunc = 0;
}

%include "test.h"

当我们编译它时这个“工作”:

swig3.0 -python -py3 -Wall test.i
gcc -shared test_wrap.c -o _test.so -I/usr/include/python3.4 -Wall -Wextra -lpython3.4m 

我们可以通过以下方式进行演示:

Python 3.4.2 (default, Oct  8 2014, 13:14:40)
[GCC 4.9.1] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import test
>>> test.fail_test(0)
>>> test.fail_test(123)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
Exception: 123
>>> test.fail_test(0)
>>> test.fail_test(999)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
Exception: 999
>>>

虽然它非常丑陋,但几乎可以肯定它不是可移植的,而且很可能也是未定义的行为。


我的建议是不要这样做,而是使用两个进程通信的解决方案。我们仍然可以让 SWIG 帮助我们生成一个不错的模块,而且更好的是,我们可以依靠一些高级 Python 构造来帮助我们。完整的示例如下所示:

%module test

%{
#include "test.h"

static void exit_handler(int code, void *fd) {
  FILE *f = fdopen((int)fd, "w");
  fprintf(stderr, "In exit handler: %d\n", code);
  fprintf(f, "(dp0\nVexited\np1\nL%dL\ns.", code);
  fclose(f);
}
%}

%typemap(in) int fd %{
  $1 = PyObject_AsFileDescriptor($input);
%}

%inline %{
  void enter_work_loop(int fd) {
    on_exit(exit_handler, (void*)fd);
  }
%}

%pythoncode %{
import os
import pickle

serialize=pickle.dump
deserialize=pickle.load

def do_work(wrapped, args_pipe, results_pipe):
  wrapped.enter_work_loop(results_pipe)

  while True:
    try:
      args = deserialize(args_pipe)
      f = getattr(wrapped, args['name'])
      result = f(*args['args'], **args['kwargs'])
      serialize({'value':result},results_pipe)
      results_pipe.flush()
    except Exception as e:
      serialize({'exception': e},results_pipe)
      results_pipe.flush()

class ProxyModule():
  def __init__(self, wrapped):
    self.wrapped = wrapped
    self.prefix = "_worker_"

  def __dir__(self):
    return [x.strip(self.prefix) for x in dir(self.wrapped) if x.startswith(self.prefix)]

  def __getattr__(self, name):
    def proxy_call(*args, **kwargs):
      serialize({
        'name': '%s%s' % (self.prefix, name),
        'args': args,
        'kwargs': kwargs
      }, self.args[1])
      self.args[1].flush()
      result = deserialize(self.results[0])
      if 'exception' in result: raise result['exception']
      if 'exited' in result: raise Exception('Library exited with code: %d' % result['exited'])
      return result['value']
    return proxy_call

  def init_library(self):
    def pipes():
      r,w=os.pipe()
      return os.fdopen(r,'rb',0), os.fdopen(w,'wb',0)

    self.args = pipes()
    self.results = pipes()

    self.worker = os.fork()

    if 0==self.worker:
      do_work(self.wrapped, self.args[0], self.results[1])
%}

// rename all our wrapped functions to be _worker_FUNCNAME to hide them - we'll call them from within the other process
%rename("_worker_%s") "";
%include "test.h"

%pythoncode %{
import sys
sys.modules[__name__] = ProxyModule(sys.modules[__name__])
%}

其中使用了以下思路:

  1. Pickle 在将数据通过管道写入工作进程之前对其进行序列化。
  2. os.fork 生成工作进程,os.fdopen 创建更好的对象以在 Python 中使用
  3. SWIG's advanced renaming 向模块的用户隐藏我们封装的实际函数,但仍然封装它们
  4. replace the module with a Python object 实现 __getattr__ 以返回工作进程的代理函数的技巧
  5. __dir__TAB 在 ipython 中工作
  6. on_exit 拦截出口(但不转移它)并通过预先编写的 ASCII 腌制对象报告代码

如果您愿意,您可以对library_init 进行透明且自动的呼叫。您还需要处理工作人员尚未启动或已经更好地退出的情况(在我的示例中它只会阻塞)。而且您还需要确保工人在退出时得到正确清理,但它现在可以让您运行:

Python 3.4.2 (default, Oct  8 2014, 13:14:40)
[GCC 4.9.1] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import test
>>> test.init_library()
>>> test.fail_test(2)
In exit handler: 2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/mnt/lislan/ajw/code/scratch/swig/pyatexit/test.py", line 117, in proxy_call
    if 'exited' in result: raise Exception('Library exited with code: %d' % result['exited'])
Exception: Library exited with code: 2
>>>

并且仍然(在某种程度上)可移植,但定义明确。

【讨论】:

    【解决方案2】:

    但是,如果没有更多信息,很难提供解决方案:

    你写了这个库吗?如果是这样,您可以修改它以抛出logic_error 而不是调用exit

    如果库调用exit,这意味着完全灾难性的失败。库的内部状态很可能不一致(您应该假设它是!)-您确定要在此之后继续该过程吗?如果您没有编写库,那么您将无法对此进行推理。如果您这样做了,请参见上文。

    也许您可以围绕库编写一个包装进程,并跨进程边界编组调用?这在执行时会更慢,编写和维护起来会更痛苦,但它会允许父进程(python)检测到子进程(库包装器)的终止。

    【讨论】:

    • 我无法控制图书馆。在我的 python 脚本中,我想知道我调用的函数发生了错误并采取相应的行动,因此我希望能够“捕获”退出。我想用my_exit()覆盖exit(),这将通过python脚本缓存的异常。
    • 对,但如果你这样做,你应该注意不要调用库中的任何其他内容,因为它会不一致。在此之后采取的唯一合理措施是通知用户并退出该过程。
    • 我认为你最好的选择是分叉你的python进程,在孩子身上完成工作。家长可以等待孩子完成。那么你就安全了。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多