在 C++ 库中调用 exit() 会终止使用 swig 包装该库的 python 脚本 [英] Calling exit() in C++ library terminates python script that wrapps that library using swig

查看:41
本文介绍了在 C++ 库中调用 exit() 会终止使用 swig 包装该库的 python 脚本的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I'm writing a Swig-Python wrapper for a C++ library. When critical error occurs, the library calls exit(err);, which in turn terminates the whole python script that executes functions from that library.

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

Is there a way to wrap around exit() function to return to the script or throw an exception?

推荐答案

你可以使用 longjmpon_exit 为这个问题组合一个大规模的 hack,尽管我强烈建议避免这有利于具有多个进程的解决方案,我将在后面的答案中概述.

You can put together a massive hack for this using longjmp and on_exit, although I highly recommend avoiding this in favour of a solution with multiple processes, which I'll outline later in the answer.

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

Suppose we have the following (broken by design) header file:

#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 代码:

We want to wrap it and convert the call to exit() into a Python exception instead. One way to achieve this would be something like the following interface, which uses %exception to insert C code around the call to every C function from your Python interface:

%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"

当我们编译它时,它有效":

This "works" when we compile it:

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
>>>

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

It's very ugly though, almost certainly not portable and most likely undefined behaviour as well.

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

My advice would be not to do that and use a solution with two processes communicating instead. We can still have SWIG help us generate a nice module and better yet we can rely on some high-level Python constructs to help us with that. The complete example looks like:

%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 的高级重命名 以隐藏我们封装的实际函数模块的用户,但仍然包装他们
  4. 用实现 __getattr__<的 Python 对象替换模块的技巧/code> 返回工作进程的代理函数
  5. __dir__ 保持 TAB 在 ipython 中工作
  6. on_exit 拦截出口(但不转移)并通过预先编写的 ASCII 腌制对象将代码报告回来
  1. Pickle to serialise data before writing it over pipes to a worker process.
  2. os.fork to spawn the worker process, with os.fdopen creating a nicer object for use in Python
  3. SWIG's advanced renaming to hide the actual functions we wrapped from users of the module, but still wrap them
  4. A trick to replace the module with a Python object that implements __getattr__ to return proxy functions for the worker process
  5. __dir__ to keep TAB working inside ipython
  6. on_exit to intercept the exit (but not divert it) and report the code back via a pre-written ASCII pickled object

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

You could make the call to library_init transparent and automatic if you wished so. You also need to handle the case where the worker hasn't been started or has already exited better (it'll just block in my example). And you'll also need to ensure that the worker gets cleaned up at exit properly, but it now lets you run:

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
>>>

并且仍然是(有点)可移植的,但绝对定义良好.

and still be (somewhat) portable, but definitely well defined.

这篇关于在 C++ 库中调用 exit() 会终止使用 swig 包装该库的 python 脚本的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆