Thursday, December 13, 2012

Named Pipes between C# and Python

There's a lot of over-complicated information on the internet for communicating between a C# process and a Python process using named pipes on Windows.  I'll start with the code:

// Open the named pipe.
var server = new NamedPipeServerStream("NPtest");

Console.WriteLine("Waiting for connection...");

var br = new BinaryReader(server);
var bw = new BinaryWriter(server);

while (true) {
    try {
        var len = (int) br.ReadUInt32();            // Read string length
        var str = new string(br.ReadChars(len));    // Read string

        Console.WriteLine("Read: \"{0}\"", str);

        str = new string(str.Reverse().ToArray());  // Just for fun

        var buf = Encoding.ASCII.GetBytes(str);     // Get ASCII byte array     
        bw.Write((uint) buf.Length);                // Write string length
        bw.Write(buf);                              // Write string
        Console.WriteLine("Wrote: \"{0}\"", str);
    catch (EndOfStreamException) {
        break;                    // When client disconnects

Console.WriteLine("Client disconnected.");

import time
import struct

f = open(r'\\.\pipe\NPtest', 'r+b', 0)
i = 1

while True:
    s = 'Message[{0}]'.format(i)
    i += 1
    f.write(struct.pack('I', len(s)) + s)   # Write str length and str                               # EDIT: This is also necessary
    print 'Wrote:', s

    n = struct.unpack('I',[0]    # Read str length
    s =                           # Read str                               # Important!!!
    print 'Read:', s

In this example, I implement a very simple protocol, where every "message" is a 4-byte integer (UInt32 in C#, 'I' (un)pack format in Python), which indicates the length of the string that follows. The string is ASCII. Important things to note here:
  • Python
    • The third parameter to open() means "unbuffered". Otherwise, it will default to line-buffered, which means it will wait for a newline character before actually sending it through the pipe.
    • I'm not sure why, but omitting the seek(0) will cause an IOError #0. I was clued to this by a StackOverflow question.