Introduction
Suppose I have this C code:
#include <stdio.h>
// Of course, these functions are simplified for the purposes of this question.
// The actual functions are more complex and may receive additional arguments.
void printout() {
puts("Hello");
}
void printhere(FILE* f) {
fputs("Hello\n", f);
}
That I'm compiling as a shared object (DLL): gcc -Wall -std=c99 -fPIC -shared example.c -o example.so
And then I'm importing it into Python 3.x running inside Jupyter or IPython notebook:
import ctypes
example = ctypes.cdll.LoadLibrary('./example.so')
printout = example.printout
printout.argtypes = ()
printout.restype = None
printhere = example.printhere
printhere.argtypes = (ctypes.c_void_p) # Should have been FILE* instead
printhere.restype = None
Question
How can I execute both printout() and printhere() C functions (through ctypes) and get the output printed inside the Jupyter/IPython notebook?
If possible, I want to avoid writing more C code. I would prefer a pure-Python solution.
I also would prefer to avoid writing to a temporary file. Writing to a pipe/socket might be reasonable, though.
The the expected state, the current state
If I type the following code in one Notebook cell:
print("Hi") # Python-style print
printout() # C-style print
printhere(something) # C-style print
print("Bye") # Python-style print
I want to get this output:
Hi
Hello
Hello
Bye
But, instead, I only get the Python-style output results inside the notebook. The C-style output gets printed to the terminal that started the notebook process.
Research
As far as I know, inside Jupyter/IPython notebook, the sys.stdout is not a wrapper to any file:
import sys
sys.stdout
# Output in command-line Python/IPython shell:
<_io.TextIOWrapper name='<stdout>' mode='w' encoding='UTF-8'>
# Output in IPython Notebook:
<IPython.kernel.zmq.iostream.OutStream at 0x7f39c6930438>
# Output in Jupyter:
<ipykernel.iostream.OutStream at 0x7f6dc8f2de80>
sys.stdout.fileno()
# Output in command-line Python/IPython shell:
1
# Output in command-line Jupyter and IPython notebook:
UnsupportedOperation: IOStream has no fileno.
Related questions and links:
- Python ctypes: Python file object <-> C FILE *
- Python 3 replacement for PyFile_AsFile
- Using fopen, fwrite and fclose through ctypes
- Python ctypes DLL stdout
- Python: StringIO for Popen - Workaround for the lack of
fileno()inStringIO, but only applies tosubprocess.Popen.
The following two links use similar solutions that involve creating a temporary file. However, care must be taken when implementing such solution to make sure both Python-style output and C-style output gets printed in the correct order.
- How do I prevent a C shared library to print on stdout in python?
- Redirecting all kinds of stdout in Python
Is it possible to avoid a temporary file?
I tried finding a solution using C open_memstream() and assigning the returned FILE* to stdout, but it did not work because stdout cannot be assigned.
Then I tried getting the fileno() of the stream returned by open_memstream(), but I can't because it has no file descriptor.
Then I looked at freopen(), but its API requires passing a filename.
Then I looked at Python's standard library and found tempfile.SpooledTemporaryFile(), which is a temporary file-like object in memory. However, it gets written to the disk as soon as fileno() is called.
So far, I couldn't find any memory-only solution. Most likely, we will need to use a temporary file anyway. (Which is not a big deal, but just some extra overhead and extra cleanup that I'd prefer to avoid.)
It may be possible to use os.pipe(), but that seems difficult to do without forking.
sys.stdoutandsys.stderrback to the originals so that Python output goes through the pipe as well.stdout, the suggestion from @ThomasK should work.os.dup2the write end of the pipe to file 1. Forprinthere, you can calllibc.fdopen(1, b'wb')to get a newFILEfor fd 1. Setrestypeto an opaqueFILEpointer, e.g.class FILE(ctypes.Structure): pass;PFILE = ctypes.POINTER(FILE);libc.fdopen.restype = PFILE. For the C lib, uselibc = ctypes.CDLL(ctypes.util.find_library('c'), use_errno=True). If the call fails (i.e. the result is booleanFalse), useerr = ctypes.get_errno();raise OSError(err, os.strerror(err))to raise a formatted exception.sys.stdout, sys.stderr = sys.__stdout__, sys.__stderr__first, then it becomes the normal case. Finally set them back to storedipykernel.iostream.OutStreams.%pip install wurlitzerand then%load_ext wurlitzer(for classic notebooks, at least) before running your C code. (I found this OP while researching this question at the Jupyter Discourse Forum.)