我正在为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
>>>
并且仍然(某种程度上)可移植,但定义明确.