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:

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

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

Console.WriteLine("Connected.");
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.");
server.Close();
server.Dispose();

Python
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
    f.seek(0)                               # EDIT: This is also necessary
    print 'Wrote:', s

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

    time.sleep(2)
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.
References:

6 comments:

  1. I have followed you example, but keep getting a broken pipe error on the c# side after the number of characters in the message changes. For example when it goes from Message[9] to Message[10], a broken pipe error is thrown by C# and nothing is sent back to python.

    Do you know what might be causing this?

    ReplyDelete
    Replies
    1. I finally got back to this problem and fixed it. It turns out the f.seek(0) is necessary after writing to the pipe also. See the EDIT in the python source code. This probably has to do with something the python file implementation is doing under the hood.

      Good catch! Apparently I never waited the 20 seconds to get to that point - sorry for taking so long to fix this!

      Delete
    2. This comment has been removed by the author.

      Delete
    3. help me:

      Traceback (most recent call last):
      File "C:\Users\Son\Desktop\Namedpipe", line 4, in
      f = open(r'\\.\pipe\NPtest', 'r+b', 0)
      IOError: [Errno 2] No such file or directory: '\\\\.\\pipe\\NPtest'

      Delete
  2. What about the other way around? That is, creating the named pipe (server) in Python and the client in C#? My question is mainly how to know that a client as connected to the other end of the pipe, when using open in Python to create the pipe!

    ReplyDelete
  3. Almost 10 years later and thanks for just writing it down cause this was the one example I've found that actually works.

    ReplyDelete