只要您愿意使用某些特定于平台的功能,您就可以完全按照指定的方式制定解决方案。我将在我的示例中使用 GNU libc,但 BSD 肯定有一个等价物,并且也有在 Windows 上模拟相同的选项。我还将重点关注标准输出和输出,尽管标准输入的相应示例只需要调整。
要真正满足您的要求,我们需要解决两件事,所以我将依次解决这些问题。
将 Python IO 对象映射到 FILE*:
首先,我们需要找到一种方法,让对 FILE* 的操作真正得到反映。
在 GNU/Linux 上,libc 提供 fopencookie 作为 GNU 特定的扩展。 (BSD 等价物是funopen,Windows 似乎更复杂,需要一个线程和匿名管道来模拟)。
使用fopencookie,我们可以创建一个FILE* 对象,该对象的行为完全符合您的预期,但会将底层IO 调用映射到对函数指针的调用。所以我们需要做的就是提供一些使用 Python C API 来完成工作的函数
请注意,如果您在 Python 中关心的所有对象都是 file 的实例,那么您只需使用一些 file specific C API calls 而不是 fopencookie:
%module test
%{
//#define _GNU_SOURCE - not needed, Python already does that!
#include <stdio.h>
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 的类型映射“专门化”而不是完全替换通用FILE * 类型映射,因此这两种变体都可以在同一个界面中使用。您也可以使用%apply 而不是实际重命名参数,以避免在使用%import 时需要修改现有的头文件。
这意味着我们现在可以在 Python 中调用 hello() 并在每次调用时将 sys.stdout 的值隐式传递给函数。
我们还通过正确跟踪我们是否应该在函数调用结束时对 FILE 对象调用 fclose 来改进我展示的第一个示例中的一个问题。这是我们在匹配特定情况的输入类型映射中设置的类型映射的本地变量。
其实在C里改stdout
通常,如果您想在 C 中真正更改 stdout,您可以使用 freopen。这样做的原因不仅仅是做作业是stdout isn't guaranteed to be a modifiable lvalue。
在实践中,尽管您曾经能够在某些平台上侥幸逃脱。在我的测试中,虽然 Linux/GCC 不再是这些平台之一,但我的分配对行为没有影响。
在这种情况下我们也不能使用freopen,至少在我们使用fopencookie 的情况下不能使用,因为没有指向 freopen 的文件路径。对于 Python 文件对象巧妙地映射到 Linux 上真正的 FILE* 的情况,我们可以使用类似以下伪代码:
freopen("/proc/self/fd/%d" % fileno(f), "w", stdout);
替换标准输出。我们仍然需要在每个 C 调用之前安排这件事,(可能滥用%exception mechanism 来制作那个钩子)以保持Python->C 标准输出映射最新。这非常丑陋且使用受限,并且对于多线程应用程序也有些缺陷。
另一种替代方法是通过修改后的trick like this 将修改挂钩到sys.stdout 等。同样,这很丑陋,仍然不能解决一般情况。
最后,如果在现有 C 代码中完全替换 stdout、stderr 和 stdin 确实是您想要做的事情,我建议您执行以下操作。您为每个文件句柄生成一个线程,每个文件句柄都有一个 pipe() 对。然后,您使用freopen 从 /proc(或通过 Windows 中的命名管道)打开管道的一端(取决于它是哪个句柄)。然后,每个管道的另一端在一个线程中使用,以阻塞等待管道上发生 IO。当 IO 发生时,您的代码会查找当前 Python 文件句柄并代理对该句柄的调用。这是可靠、正确、便携且相当简单的。
改进
如果您真正使用此代码,您可能想要做以下事情:
- 按照评论解决 GIL 问题
- 使
FILE* 对象可以是RW 而不仅仅是W
- 添加相应的 stderr 和 stdin 辅助类型映射
- 提供 BSD/Windows 替代代码路径而不是
fopencookie。