在C库中调用exit()会终止使用swig包装该库的python脚本

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

有没有一种方法可以包装exit()函数以返回到脚本或引发异常?

解决方法:

您可以使用longjmp和on_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__])
%}

其中使用以下思路:

> Pickle以序列化数据,然后通过管道将其写入工作进程.
> os.fork生成工作进程,并使用os.fdopen创建一个更好的对象以供在Python中使用
> SWIG’s advanced renaming以隐藏我们向模块用户包装的实际功能,但仍将其包装
> replace the module with a Python object的技巧,该技巧实现__getattr__返回工作进程的代理功能
> __dir__以使TAB在ipython中正常工作
> 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
>>>

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

上一篇:java-SWIG更改功能签名


下一篇:如何从python使用C lib