Python,stdout,C和SWIG
假设我在c中有一个函数:
你好ç
void hello() { printf("Hello How are you"); }
现在将其包装到python函数hello.py
。
现在,当我运行python函数hello.py
,我得到输出,但我想将它保存到变量。
我尝试过使用这种方法:
import sys import StringIO stdout = sys.stdout result = StringIO.StringIO() sys.stdout = result hello() sys.stdout = stdout result_osr_string = result.getvalue() print result.getvalue()
我认为问题在于swig到python的转换。 因为当我为一个简单的python函数尝试上面的方法时,它的工作原理。
我尝试使用fprintf
而不是printf
但即使这样也行不通。
如果您愿意使用某些特定于平台的function,您可以完全按照指定制定解决方案。 我将在我的示例中使用GNU libc,但BSD肯定有一个等效的,并且有一些选项可以在Windows上模拟相同的。 我也将重点关注stdout和输出,尽管stdin的相应示例仅需要调整。
要真正满足您的要求,我们需要解决两件事,所以我将按顺序解决这些问题。
将Python IO对象映射到FILE*
:
首先,我们需要找到一种方法来实际反映FILE*
上的操作。
在GNU / Linux上,libc提供了fopencookie
作为GNU特定扩展。 (BSD等价物是funopen
,Windows似乎更复杂,需要一个线程和匿名管道来模拟相同的)。
使用fopencookie
我们可以创建一个FILE*
对象,其行为完全符合您的预期,但将底层IO调用映射到函数指针调用。 因此,我们需要做的就是提供一些使用Python C API来完成工作的函数
请注意,如果您在Python中关注的所有对象都是file
实例,那么您只需使用一些特定于文件的C API调用而不是fopencookie:
%module test %{ //#define _GNU_SOURCE - not needed, Python already does that! #include static ssize_t py_write(void *cookie, const char *buf, size_t size) { // Note we might need to acquire the GIL here, depending on what you target exactly PyObject *result = PyObject_CallMethodObjArgs(cookie, PyString_FromString("write"), PyString_FromStringAndSize(buf, size), NULL); (void)result; // Should we DECREF? return size; // assume OK, should really catch instead though } static int py_close(void *cookie) { Py_DECREF(cookie); return 0; } static FILE *fopen_python(PyObject *output) { if (PyFile_Check(output)) { // See notes at: https://docs.python.org/2/c-api/file.html about GIL return PyFile_AsFile(output); } cookie_io_functions_t funcs = { .write = py_write, .close = py_close, }; Py_INCREF(output); return fopencookie(output, "w", funcs); } %} %typemap(in) FILE * { $1 = fopen_python($input); } %typemap(freearg) FILE * { // Note GIL comment above here also // fileno for fopencookie always returns -1 if (-1 == fileno($1)) fclose($1); } %inline %{ void hello(FILE *out) { fprintf(out, "Hello How are you\n"); } %}
这足以让以下Python工作:
import sys import StringIO stdout = sys.stdout result = StringIO.StringIO() sys.stdout = result from test import hello hello(sys.stdout) sys.stdout = stdout result_osr_string = result.getvalue() print "Python: %s" % result.getvalue()
通过在每个函数调用中将FILE*
作为参数传递,这可以确保我们永远不会得到对Python句柄的陈旧引用,后者在其他地方被替换。
使流程透明化
在上面的例子中,我们必须明确说明每个函数调用使用哪个IO对象。 我们可以通过使用由包装器代码自动填充的参数来简化这一过程并接近您的示例。 在这个例子中,我将修改上面的类型映射,以自动使用sys.stdout
作为FILE *py_stdout
类的参数:
%typemap(in) FILE * (int needclose) { $1 = fopen_python($input); needclose = !PyFile_Check($input); } %typemap(freearg) FILE * { // Note GIL comment above if (needclose$argnum) fclose($1); } %typemap(in,numinputs=0) FILE *py_stdout (int needclose) { PyObject *sys = PyImport_ImportModule("sys"); PyObject *f = PyObject_GetAttrString(sys, "stdout"); needclose = !PyFile_Check(f); $1 = fopen_python(f); Py_DECREF(f); Py_DECREF(sys); } %inline %{ void hello(FILE *py_stdout) { fprintf(py_stdout, "Hello How are you\n"); } %}
请注意,这里FILE *py_stdout
“specializes”的typemap不是完全替换genericsFILE *
typemap,因此两个变体都可以在同一个接口中使用。 您还可以使用%apply
而不是实际重命名参数,以避免在使用%import
需要修改现有的头文件。
这意味着我们可以在Python中调用hello()
并在每次调用时将sys.stdout
的值隐式传递给函数。
我们通过正确跟踪我们是否应该在函数调用结束时在FILE对象上调用fclose
来改进我在第一个示例中的问题。 这是我们在输入类型映射中设置的类型映射的本地变量,它匹配我们的特定情况。
实际上在C中更改stdout
通常,如果你想在C中真正改变stdout
,你会用freopen
来做。 这样做的原因并不仅仅是做一个赋值是stdout
不能保证是一个可修改的左值 。
在实践中,虽然你曾经能够在某些平台上逃脱这一点。 在我的测试中虽然Linux / GCC不再是那些平台之一,但我的任务对行为没有影响。
我们也不能在这种情况下使用freopen
,至少不能用于我们使用fopencookie
的情况,因为没有指向freopen的文件路径。 对于Python文件对象整齐地映射到Linux上的真实FILE*
,我们可以使用类似下面的伪代码:
freopen("/proc/self/fd/%d" % fileno(f), "w", stdout);
替换stdout。 我们仍然需要安排在每次 C调用之前发生这种情况(可能滥用%exception
机制来实现该挂钩)以保持Python-> C stdout映射最新。 这非常难看并且在使用上受到限制,并且对于multithreading应用程序而言有些缺陷。
另一种替代方法是通过像这样的修改技巧来挂钩对sys.stdout
等的修改。 再次,这是非常丑陋的,仍然无法解决一般情况。
最后如果完全替换现有C代码中的stdout,stderr和stdin确实是你想做的事情,我建议如下。 您为每个文件句柄生成一个线程,每个文件句柄都有一个pipe()对。 然后使用freopen
从/ proc(或通过Windows中的命名管道)打开管道的一端(取决于它是哪个句柄)。 然后在一个线程中使用每个管道的另一端来阻止等待IO在管道上发生。 当IO发生时,您的代码会查找当前的Python文件句柄并代理对该句柄的调用。 这是可靠,正确,便携和相当简单的。
改进
如果你真的使用这个代码,你可能想要做以下事情:
- 以评论的方式解决GIL问题
- 使
FILE*
对象能够成为RW而不仅仅是W. - 添加相应的stderr和stdin帮助程序类型映射
- 提供BSD / Windows替代代码路径而不是
fopencookie
。
c函数printf没有注意python的sys.stdout中存储的值。 您应该使用sprintf或snprintf将文本打印到cstring并从c函数返回char *
。 Swig会将它包装在python字符串对象中。