Asynchronous Reading in subprocess.h
One hotly requested feature for my single header C/C++ process spawning and management library subprocess.h was the ability to read from the standard output or error of a spawned process while it was still executing. In this PR I’ve added support for this, but it requires some change of behaviour in your code if you want to use it.
C FILE’s and Asynchronous Reading⌗
I use a really clever hack to give my users a standard C FILE handle with
which they can read the standard output and error, and write to the standard
input of any processes they spawn. This is immensely powerful because it means
anyone using my API can use an already familiar paradigm to interact with the
processes they spawn. Users could fprintf
to the standard input of a spawned
process, or call fgets
to read from the standard output or error. While I love
this feature - it causes a complication when trying to support asynchronous
reading.
For the normal process spawning on Windows I was using CreatePipe
to map a
HANDLE
on the parent process to HANDLE
that’ll be used for the standard
pipes on the spawned process. A CreatePipe
is also known as an anonymous pipe
in Windows lingo - and it turns out you cannot sync from this pipe until the
spawned process has complete. But there is another mechanism called a named pipe
using the CreateNamedPipe
call that can help us here.
Named pipes support an additional flag on creation FILE_FLAG_OVERLAPPED
that
lets you get at the data from the pipe before the other end of the pipe has been
closed. I thought ‘Great! I’ll use this et voila! Everything will work!’. It
didn’t.
The problem is that if you use the overlapped creation flags to a named pipe you cannot then do the clever hack anymore to get a C FILE that reads from this. It’ll just hang forever.
So instead I’ve introduced some new helpers methods subprocess_read_stdout
and
subprocess_read_stderr
that use the correct APIs on each operating system to
allow for asynchronous reading from a pipe.
Note that while the main problems for this feature have been with Windows support and the example below is Windows specific - the feature supports all three major desktop platforms. Worry not!
Example⌗
As an example of how we can use this I’ve wrote up the following little example that works on Windows (no reason why it couldn’t work on other operating systems, I just used Windows for the test):
int main() {
const char *const commandLine[] = {"ping", "-t", "www.bbc.net.uk", 0};
struct subprocess_s process;
unsigned bytes_read;
if (0 != subprocess_create(
commandLine,
subprocess_option_inherit_environment | subprocess_option_enable_async,
&process)) {
fprintf(stderr, "subprocess_create failed!");
}
do {
char buffer[1024] = {0};
bytes_read = subprocess_read_stdout(&process, buffer, sizeof(buffer));
printf("Read %u bytes - '%s'\n", bytes_read, buffer);
} while (bytes_read != 0);
}
This will loop infinitely reading from the ping command with the -t
option
specified. It’ll read the output from ping as it is flushed and spit out what it
read and the number of bytes.
Read 42 bytes - '
Pinging www.bbc.net.uk [212.58.237.253] '
Read 24 bytes - 'with 32 bytes of data:
'
Read 27 bytes - 'Reply from 212.58.237.253: '
Read 27 bytes - 'bytes=32 time=22ms TTL=53
'
Read 27 bytes - 'Reply from 212.58.237.253: '
Read 27 bytes - 'bytes=32 time=22ms TTL=53
'
A few things:
- Note the new
subprocess_create
optionsubprocess_option_enable_async
. This is needed to setup the pipe for being able to read from it asynchronously. If you use this option you should refrain from usingsubprocess_stdout
orsubprocess_stderr
to read from the spawned process - they will not work. - Note the new function
subprocess_read_stdout
to read from the standard output of the spawned process. It’ll return non-zero while there is any data still to be read. When the child process terminates then it’ll return 0 signalling the end.
One additional thing to bear in mind is that the subprocess_read_stdout
and
subprocess_read_stderr
functions are blocking. If you want to read from both
of these simultaneously then it is recommended that you either:
- Spawn a thread for one or both to read from it. It is thread safe to read from both pipes of a spawned process at the same time.
- Or use the
subprocess_option_combined_stdout_stderr
option if you just want the full contents of both pipes both don’t care to differentiate from either pipes output.
I hope you find this new functionality useful.
An additional gentle reminder that you can stay tuned for future updates by using the RSS feed.