C 扩展 - 如何将 printf 重定向到 python 记录器?

C extensions - how to redirect printf to a python logger?

我有一个简单的 C 扩展(参见下面的示例),有时使用 printf 函数进行打印。 我正在寻找一种方法来包装来自该 C 扩展的函数调用,以便所有这些 printfs 将被重定向到我的 python 记录器。

hello.c:

#include <Python.h>

static PyObject* hello(PyObject* self)
{
   printf("example print from a C code\n");
   return Py_BuildValue("");
}

static char helloworld_docs[] =
   "helloworld(): Any message you want to put here!!\n";

static PyMethodDef helloworld_funcs[] = {
   {"hello", (PyCFunction)hello,
   METH_NOARGS, helloworld_docs},
   {NULL}
};

static struct PyModuleDef cModPyDem =
{
    PyModuleDef_HEAD_INIT,
    "helloworld",
    "Extension module example!",
    -1,
    helloworld_funcs
};

PyMODINIT_FUNC PyInit_helloworld(void)
{
    return PyModule_Create(&cModPyDem);
};

setup.py:

from distutils.core import setup, Extension
setup(name = 'helloworld', version = '1.0',  \
   ext_modules = [Extension('helloworld', ['hello.c'])])

先用运行

python3 setup.py install

然后:

import helloworld
helloworld.hello()

我希望能够做这样的事情:

with redirect_to_logger(my_logger)
   helloworld.hello()

编辑: 我看到许多 post 展示了如何使 C 的打印静音,但我无法从中弄清楚如何才能我改为在 python 中捕捉指纹。 例如 post:Redirect stdout from python for C calls

我认为这个问题没有引起太大的关注,因为我可能问得太多了,所以我不再关心日志记录了……我如何才能在 python 中捕获 C 打印?到列表或其他什么。

编辑 所以我能够在某种程度上实现我想要的工作代码 - 将 c printf 重定向到 python logger:

import select
import threading
import time
import logging
import re

from contextlib import contextmanager

from wurlitzer import pipes
from helloworld import hello


logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
ch.setFormatter(formatter)
logger.addHandler(ch)

    

class CPrintsHandler(threading.Thread):
    def __init__(self, std, poll_std, err, poll_err, logger):
        super(CPrintsHandler, self).__init__()
        self.std = std
        self.poll_std = poll_std
        self.err = err
        self.poll_err = poll_err
        self.logger = logger
        self.stop_event = threading.Event()

    def stop(self):
        self.stop_event.set()

    def run(self):
        while not self.stop_event.is_set():
            # How can I poll both std and err at the same time?
            if self.poll_std.poll(1):
                line = self.std.readline()
                if line:
                    self.logger.debug(line.strip())

            if self.poll_err.poll(1):
                line = self.err.readline()
                if line:
                    self.logger.debug(line.strip())


@contextmanager
def redirect_to_logger(some_logger):
    handler = None
    try:
        with pipes() as (std, err):
            poll_std = select.poll()
            poll_std.register(std, select.POLLIN)
            poll_err = select.poll()
            poll_err.register(err, select.POLLIN)
            handler = CPrintsHandler(std, poll_std, err, poll_err, some_logger)
            handler.start()
            yield
    finally:
        if handler:
            time.sleep(0.1) # why do I have to sleep here for the foo prints to finish?
            handler.stop()
            handler.join()


def foo():
    logger.debug('logger print from foo()')
    hello()


def main():
    with redirect_to_logger(logger):
        # I don't want the logs from here to be redirected as well, only printf.
        logger.debug('logger print from main()')
        foo()


main()

但我有几个问题:

  1. python 日志也被 CPrintsHandler 重定向和捕获。有没有办法避免这种情况?

  2. 打印顺序不正确:

    python3 redirect_c_example_for_Whosebug.py

    2020-08-18 19:50:47,732 - root - DEBUG - 来自 C 代码的示例打印

    2020-08-18 19:50:47,733 - root - DEBUG - 2020-08-18 19:50:47,731 - root - DEBUG - 来自 main()

    的记录器打印

    2020-08-18 19:50:47,733 - root - DEBUG - 2020-08-18 19:50:47,731 - root - DEBUG - 来自 foo()

    的记录器打印

此外,记录器打印所有 go to err,也许我轮询它们的方式导致了这个顺序。

  1. 我对 python 中的 select 不太熟悉,不确定是否有办法同时轮询 std 和 err 并打印先有内容的那个。

在 Linux 上,您可以使用 wurlitzer 来捕获 fprint 的输出,例如:

from wurlitzer import pipes
with pipes() as (out, err):
    helloworld.hello()
out.read()
#'example print from a C code\n'

wurlitzer 基于 this article of Eli Bendersky,如果您不喜欢依赖 third-party 库,可以使用该代码。

遗憾的是,wurlitzer 和文章中的代码仅适用于 Linux(以及可能的 MacOS)。

这里是 Windows 的原型(原型的改进版本可以从 my github 安装)使用 Eli 的方法作为 Cython-extension(可能可以翻译成 ctypes 如果需要):

%%cython

import io
import os

cdef extern from *:
    """
    #include <windows.h>
    #include <io.h>
    #include <stdlib.h>
    #include <stdio.h>
    #include <fcntl.h>

    int open_temp_file() {
        TCHAR lpTempPathBuffer[MAX_PATH+1];//path+NULL

        //  Gets the temp path env string (no guarantee it's a valid path).
        DWORD dwRetVal = GetTempPath(MAX_PATH,          // length of the buffer
                                     lpTempPathBuffer); // buffer for path 
        if(dwRetVal > MAX_PATH || (dwRetVal == 0))
        {
            return -1;
        }

        //  Generates a temporary file name. 
        TCHAR szTempFileName[MAX_PATH + 1];//path+NULL
        DWORD uRetVal = GetTempFileName(lpTempPathBuffer, // directory for tmp files
            TEXT("tmp"),     // temp file name prefix 
            0,                // create unique name 
            szTempFileName);  // buffer for name 
        if (uRetVal == 0)
        {
            return -1;
        }

        HANDLE tFile = CreateFile((LPTSTR)szTempFileName, // file name 
                GENERIC_READ | GENERIC_WRITE,      // first we write than we read 
                0,                    // do not share 
                NULL,                 // default security 
                CREATE_ALWAYS,        // overwrite existing
                FILE_ATTRIBUTE_TEMPORARY | FILE_FLAG_DELETE_ON_CLOSE, // "temporary" temporary file, see https://docs.microsoft.com/en-us/archive/blogs/larryosterman/its-only-temporary
                NULL);                // no template 

        if (tFile == INVALID_HANDLE_VALUE) {
            return -1;
        }

        return _open_osfhandle((intptr_t)tFile, _O_APPEND | _O_TEXT);
    }

    int replace_stdout(int temp_fileno)
    {
        fflush(stdout);
        int old;
        int cstdout = _fileno(stdout);

        old = _dup(cstdout);   // "old" now refers to "stdout"
        if (old == -1)
        {
            return -1;
        }
        if (-1 == _dup2(temp_fileno, cstdout))
        {
            return -1;
        }
        return old;
    }

    int restore_stdout(int old_stdout){
        fflush(stdout);

        // Restore original stdout
        int cstdout = _fileno(stdout);
        return _dup2(old_stdout, cstdout);
    }
    
    
    void rewind_fd(int fd) {
        _lseek(fd, 0L, SEEK_SET);
    }
    """
    int open_temp_file()
    int replace_stdout(int temp_fileno)
    int restore_stdout(int old_stdout)
    void rewind_fd(int fd)
    void close_fd "_close" (int fd)
    
cdef class CStdOutCapture():
    cdef int tmpfile_fd
    cdef int old_stdout_fd
    def start(self): #start capturing
        self.tmpfile_fd = open_temp_file()
        self.old_stdout_fd = replace_stdout(self.tmpfile_fd)
    
    def stop(self): # stops capturing, frees resources and returns the content
        restore_stdout(self.old_stdout_fd)
        rewind_fd(self.tmpfile_fd) # need to read from the beginning
        buffer = io.TextIOWrapper(os.fdopen(self.tmpfile_fd, 'rb'))
        result = buffer.read()
        close_fd(self.tmpfile_fd)
        return result
           

现在:

b = CStdOutCapture()
b.start()
helloworld.hello()
out = b.stop()
print("HERE WE GO:", out)
# HERE WE GO: example print from a C code

如果我有空编辑 C 代码,这就是我要做的。在 C 中打开内存映射并使用 fprintf() 写入其文件描述符。将文件描述符作为 int 公开给 Python,然后使用 mmap 模块打开它或使用 os.openfd() 将其包装在更简单的 file-like 对象中,或将其包装在 file-like C 中的对象并让 Python 使用它。

然后我会创建一个 class 使我能够通过常用接口写入 sys.stdout,即它的 write() 方法(对于 Python's side usage) ,这将使用 select 模块从 C 中轮询文件,该文件在线程中充当其标准输出。然后我会用这个 class 的对象切换 sys.stdout。因此,当 Python 执行 sys.stdout.write(...) 时,字符串将被重定向到 sys.stdout.write(),当循环进入线程检测到来自 C 的文件的输出,它将使用 sys.stdout.write() 写入它。因此,所有内容都将写入屏幕并可供记录器使用。 在这个模型中,严格的 C 部分实际上永远不会写入连接到终端的文件描述符。

你甚至可以在 C 本身中做很多这样的事情,而在 Python 方面留下很少的东西,但是它更容易从 Python 方面影响解释器,因为扩展是整个故事中涉及某种共享库,我们称之为 IPC 和 OS。这就是为什么首先不在扩展和 Python 之间共享标准输出的原因。

如果你想在 C 端继续 printf(),你可以看看如何在编写这整个烂摊子之前在 C 中重定向它。

这个答案完全是理论上的,因为我没有时间去测试它;但据我所知应该是可行的。如果您尝试过,请在评论中告诉我进展情况。也许我错过了什么,但是,我确信这个理论是合理的。 这个想法的美妙之处在于它将 OS 独立,尽管共享内存或连接文件描述符以在 RAM 中分配 space 的部分有时可能是 Windows.[=10 上的 PITA =]

如果您不局限于在 C 中使用 printf,那么使用 python C API 中的打印等价物并传递您想要重定向的位置会更容易消息作为参数。

例如,您的 hello.c 将是:

#include <Python.h>

static PyObject* hello(PyObject* self, PyObject *args)
{
    PyObject *file = NULL;
    if (!PyArg_ParseTuple(args, "O", &file))
        return NULL;
    PyObject *pystr = PyUnicode_FromString("example print from a C code\n");
    PyFile_WriteObject(pystr, file, Py_PRINT_RAW);
   return Py_BuildValue("");                                                                     
}

static char helloworld_docs[] =
   "helloworld(): Any message you want to put here!!\n";

static PyMethodDef helloworld_funcs[] = { 
   {"hello", (PyCFunction)hello,
   METH_VARARGS, helloworld_docs},
   {NULL}
};

static struct PyModuleDef cModPyDem =
{
    PyModuleDef_HEAD_INIT,
    "helloworld",
    "Extension module example!",
    -1, 
    helloworld_funcs
};

PyMODINIT_FUNC PyInit_helloworld(void)
{
    return PyModule_Create(&cModPyDem);
};

我们可以检查它是否与以下程序一起工作:

import sys
import helloworld

helloworld.hello(sys.stdout)
helloworld.hello(sys.stdout)

helloworld.hello(sys.stderr)

在命令行中我们分别重定向每个输出:

python3 example.py 1> out.txt 2> err.txt

out.txt 将有两个打印调用,而 err.txt 将只有一个,正如我们的 python 脚本所预期的那样。

您可以查看 python 的 print 实施,以进一步了解您可以做什么。

cpython print source code