首页 > 解决方案 > C 扩展 - 如何将 printf 重定向到 python 记录器?

问题描述

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

你好ç:

#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);
};

设置.py:

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

使用第一次运行

python3 setup.py 安装

接着:

import helloworld
helloworld.hello()

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

with redirect_to_logger(my_logger)
   helloworld.hello()

编辑:我看到了一些帖子,展示了如何使 C 中的打印静音,但我无法从中弄清楚如何在 python 中捕获打印。此类帖子的示例:Redirect stdout from python for C calls

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

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

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_stackoverflow.py

    2020-08-18 19:50:47,732 - 根 - 调试 - 从 C 代码打印的示例

    2020-08-18 19:50:47,733 - 根 - 调试 - 2020-08-18 19:50:47,731 - 根 - 调试 - 来自 main() 的记录器打印

    2020-08-18 19:50:47,733 - 根 - 调试 - 2020-08-18 19:50:47,731 - 根 - 调试 - 来自 foo() 的记录器打印

此外,记录器打印全部出错,也许我轮询它们的方式导致了这个顺序。

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

标签: python-3.xloggingstdoutpython-c-apipython-logging

解决方案


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

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

wurlitzer基于Eli Bendersky 的这篇文章,如果您不喜欢依赖第三方库,可以使用该代码。

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

这是一个使用 Eli 的方法作为 Cython 扩展的 Windows 原型(可以从我的 github安装原型的改进版本)(如果需要,可能可以翻译成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

推荐阅读