2013年11月16日 星期六

[python] named pipe on windows 7 using ctypes

同樣這篇也是誤入岐途。

原先使用 .Net 的 NamedPipeServer 及 NamePipeClient 好好的。不小心看到網路上有提到 python 也有支援。很開心地就拿來用,結果是失敗。(原po1原po2)

在沒有小心查證之下,又堅信 python 必定可使用 named pipe,嘗試了諸多的 post。都是被無情地打槍。(族繁不及備載) 最後就是看到使用 windows 內 kernel32 的一組 API 達成這個功能(原po):

  1. CreateNamedPipeA
  2. ConnectNamedPipe
  3. WaitNamedPipeA
  4. SetNamedPipeHandleState
  5. ReadFile
  6. WriteFile
  7. FlushFileBuffers
  8. DisconnectNamedPipe

因此,一個很不巧又很湊巧地,讓我必須要去學一下 ctypes 是怎麼回事。

Windows API?

ctypes 是 python 一個很重要的 module,可以用來跟 dll 或 shared libraries 溝通。它提供了一組與 C 語言相容的資料型別(data type),以此做為純 python 的溝通橋樑。也就是說,我這次不小心踩到 Windows API啦…。(我寫 VB6 時最討厭遇到 Windows API)

先從 Windows API 來看,一個 NamePipe 的範例包含兩個部份,一個我硬把它叫 Server,一個我硬把它叫 Client。這是從 .Net 的 NamedPipeServer 及 NamePipeClient 來推論的。

在 server 這邊的流程是(很可惜網路上現在的 MSDN 很少描述這種東西了,或者這些都會在舊的 MSDN 裡,這裡所寫的流程是由網路上搜尋及試誤後得到的可能的一種正確流程。):

  1. 呼叫 CreateNamedPipeA,拿到 handle。
  2. 呼叫 ConnectNamedPipe,等待另一方連上。
  3. 在連上之後,使用 ReadFile 讀取資料,或使用 WriteFile 傳送資料(呼叫 FlushFileBuffers 確定資料沒有留在 buffer)。
  4. 使用 DisconnectNamedPipe 關閉連結,使用 CloseHandle 釋放資源。

在 client 這邊的流程是:

  1. 使用 CreateFileA 得到 handle。handle 有可能是幾種數值。如果 server 已經在等待中,handle 會直接拿到。如果不是 handle 是 INVALID_HANDLE_VALUE,接下來可以從 GetLastError 得到是否為 ERROR_PIPE_BUSY,若是 pipe busy 可以呼叫 WaitNamedPipeA,等 server 開啟。
  2. 成功拿到 handle 之後,可能需要改變 pipe 的傳送方式為 byte。此時使用 SetNamedPipeHandleState。此步驟可省略。
  3. 在連上之後,使用 ReadFile 讀取資料,或使用 WriteFile 傳送資料(呼叫 FlushFileBuffers 確定資料沒有留在 buffer)。
  4. 使用 DisconnectNamedPipe 關閉連結,使用 CloseHandle 釋放資源。

相信老程式人一定會知道,這些 API 在尾巴有 A 的,也有同名但尾巴不帶字的,或者是尾巴帶 W 的。現在 MSDN 不太強調這些資訊,我想這些差異現在應該是被微軟處理掉,開發者可以不用去在乎了。(在 http://msdn.microsoft.com/en-us/library/windows/desktop/aa363858(v=vs.85).aspx 最下面一行。不懂的人還是看不懂。這告訴我們 .Net 就對了,不要再用 Windows API?)

之所以要先介紹 Named Pipe 的 Windows API,是因為,用 ctypes,就是得按照 Windows API 的規矩來!ctypes 不是一個已經包好(wrapped)的轉接器(adapter),而是要拿來做轉接器的工具。已經打包好的是 pywin32,我自己誤很大。

現在來仔細看 CreateNamedPipe[5] 的定義,進一步了解該從 MSDN 查到什麼資訊,如何用 ctypes 與它溝通:

HANDLE WINAPI CreateNamedPipe(
_In_      LPCTSTR lpName,
_In_      DWORD dwOpenMode,
_In_      DWORD dwPipeMode,
_In_      DWORD nMaxInstances,
_In_      DWORD nOutBufferSize,
_In_      DWORD nInBufferSize,
_In_      DWORD nDefaultTimeOut,
_In_opt_  LPSECURITY_ATTRIBUTES lpSecurityAttributes
);
第一行看到的是 HANDLE WINAPI CreateNamedPipe,知道它會回傳一個 HANDLE 型態的值。
因為我們很取巧地使用 python,所以可以很取巧地不用刻意準備對應的型別來承接。
再來第二行 _In_ LPCTSTR lpName,說明這是一個傳入參數,型別是 LPCTSTR,
這個型別的意義請參考網路上的好文,我這裡也很投機地使用python 的 str 型別來傳。


接下來的 DWORD,應該大家都熟悉地知道是 2-byte bytes 型別。


這些的對應,我也很投機地找到一個地方有記錄,是別人整理好的在 rpython 的專案中。
但是重要的是,dwOpenMode、dwPipeMode 要填什麼值?
這個就要回到 MSDN(參考[5])的網頁裡找到對應的數值。例如 dwOpenMode,有基本的三個數值:


  • PIPE_ACCESS_DUPLEX 0x00000003
  • PIPE_ACCESS_INBOUND 0x00000001
  • PIPE_ACCESS_OUTBOUND 0x00000002

同時可以有下列三個不同的 flag 可調整 pipe 對於資料寫讀時的行為:



  • FILE_FLAG_FIRST_PIPE_INSTANCE 0x00080000
  • FILE_FLAG_WRITE_THROUGH 0x80000000
  • FILE_FLAG_OVERLAPPED 0x40000000

又同時有以下三個 flag,可調整 security access 的模式:



  • WRITE_DAC 0x00040000L
  • WRITE_OWNER 0x00080000L
  • ACCESS_SYSTEM_SECURITY 0x01000000L

所以,從上述三大類中,挑選正確的數值。通常大家都是這麼做的:



PIPE_ACCESS_DUPLEX = 0x3  # 宣告常數
FILE_FLAG_WRITE_THROUGH = 0x80000000
dwOpenMode = PIPE_ACCESS_DUPLEX | FILE_FLAG_WRITE_THROUGH


然後把 dwOpenMode 放進呼叫裡。


假設像我一樣抄完別人的 code,要呼叫 CreateNamedPipe 這個 function 就是這樣:



hPipe = windll.kernel32.CreateNamedPipeA(path,
                                                 PIPE_ACCESS_DUPLEX,
                                                 PIPE_TYPE_BYTE |
                                                 PIPE_READMODE_BYTE |
                                                 PIPE_WAIT,
                                                 PIPE_UNLIMITED_INSTANCES,
                                                 BUFSIZE, BUFSIZE,
                                                 NMPWAIT_USE_DEFAULT_WAIT,
                                                 None
                                                )


在 python doc 的 15.17.1.1. 說到會自動代入兩個物件,windll, oledll,從這兩個物件開始操作。


標準的 c data type,有 python doc 15.17.1.4. Fundamental data types 可以查,
但是對於 Windows API 裡特別的型別(例如 LPCTSTR) 就要試,或著查看 pywin32 之類的程式碼對照。


Array, Pointer 怎辦?


接下來還會遇到的是陣列,指標的問題。通常是為了應付 out 的參數,像是 ReadFile:


BOOL WINAPI ReadFile(
_In_         HANDLE hFile,
_Out_        LPVOID lpBuffer,
_In_         DWORD nNumberOfBytesToRead,
_Out_opt_    LPDWORD lpNumberOfBytesRead,
_Inout_opt_  LPOVERLAPPED lpOverlapped
);

第三行的 _Out_        LPVOID lpBuffer,我們得準備個陣列給它,而且還要是個指標。


首先,我在 python 從來沒想過要宣告陣列。據抄來的 code 是這樣的(我忘記哪抄來的):



buff = (c_ubyte * 4)()  # c_ubyte 型別,長度為 4


要讓它是個指標,就照官方文件給個 byref function 包著或建立一個 pointer 物件(byref 比較 lightweight)



lpbuff = byref(buff)
or
pbuff = pointer(buff)


在第五行的 _Out_opt_    LPDWORD lpNumberOfBytesRead,LPDWORD 的處理方式則是:



cbRead = c_ulong(0)
fSuccess = windll.kernel32.ReadFile(self.hpipe,
                                    byref(chBuf, offset),
                                    rsize,
                                    byref(cbRead),
                                    None)


offset += cbRead.value


如此,在 cbRead.value 就可以看到值。


指標還有個常用到的特色是指標移動,像是上面的第三行,byref(chBuf, offset),
得到的是 chBuf 再往後移動 offset 的指標。常寫 c 或 c++ 的人會了解。


為什麼直接用 ctypes ?


除了誤入岐途之外,也有一個好處,pywin32 真的不小。如果只是想要一個 named pipe,
我自己 285行(特化過的 named pipe server + client)就解決了。
程式碼長度約 12190 bytes。deploy 不用帶 pywin32 或叫 user 安裝。
目前我想到的是這個優點。不過網路上有很多人說,pywin32 解決很多轉換的步驟。
至少少掉很多查文件、設定常數的重覆工作。


 


參考:


[1] http://code.activestate.com/lists/python-list/446422/


[2] http://www.nullege.com/codes/search/ctypes.OleDLL


[3] http://stackoverflow.com/questions/3517159/ctypes-offset-into-a-buffer


[4] http://docs.python.org/2/library/ctypes.html#ctypes-arrays


[5] http://msdn.microsoft.com/en-us/library/windows/desktop/aa365150(v=vs.85).aspx


[6] http://stackoverflow.com/questions/1430446/create-a-temporary-fifo-named-pipe-in-python


[7] http://jonathonreinhart.blogspot.tw/2012/12/named-pipes-between-c-and-python.html

沒有留言:

張貼留言