In previous tutorial (Execute a Console Program and Capture Its Output) we were using anonymous pipes to read output from console program. The pipes were created using direct Windows API routines. Therefore we were using procedural programming approach in that specific area. On the other hand, Delphi is well known as object oriented programming language. So why don't we implement OOP in manipulating anonymous pipes and reap OOP advantages, such as reusability.
In this tutorial we will try to encapsulate windows API calls in creating and managing anonymous pipe into a class. This way we can reuse and maintain the codes easily. We will name this class TAnonymPipe.
TAnonymPipe Class, the Design
These are the features of TAnonymPipe.
- Create the pipe
- Read data from the pipe
- Write data from the pipe
- Destroy/finalize the pipe
Let's not confuse the "create" pipe with usual delphi class constructor name, Create. When creating the class we don't always want the pipe to be created immediately. In order to mimic more of the real world pipe, let's use Open for the method that responsible to actually create the pipe. Imagine that in the real world, a pipe can not be used unless at least one of its ends is open.
Now we have an Open method to implement in TAnonymPipe.
Get Data from the Pipe
Of course we need to get data from our communication pipe. It is obvious we need a method for this. And like previously, let's use simple but intuitive name. This time I believe Get would be a good name. To make it easier the use of our TAnonymPipe class, I would like to have several "versions" of this Get method. They are:
- function Get(var ABuffer; const ABufferSize: Cardinal): Cardinal;.
This will get data from the pipe and put it into a memory buffer. ABufferSize defines both the buffer size (in bytes) and the number of bytes the caller tries to get.
- function Get(ADest: TStream; const ASize: Cardinal=0): Cardinal;
This will get data from the pipe and put it into the given instance of TStream. ASize simply specifies the number of bytes the caller wishes to get. When ASize equals 0, this method simply gets all available data.
- function GetAnsiStr: AnsiString;
This will read the whole current content of the pipe and return it to the caller as an AnsiString.
Another mandatory thing we need to have in our communication data is way(s) to put/write data to it. As usually, we will use simple and intuitive name for method(s) related with this operation. This time we will use Put. Like with getting data, we also want to make it easier for users of TAnonymPipe by providing several "Put" methods to fit most popular approaches in transferring data.
- function Put(const ABuffer; const ADataSize: Cardinal): Cardinal;
This method put data that's being held in a memory buffer into the pipe. ABuffer is the memory location holding the data, and ADataSize specifies the number of bytes should be read from ABuffer and put to the pipe.
- function Put(ASource: TStream; const ADataSize: Cardinal=0): Cardinal;
This method puts data currently stored in the given TStream instance to the pipe. ADataSize specifies how many bytes should be read from the stream, from the current position, and put into the pipe. If ADataSize equals 0, the method will try to put the whole content of the stream.
- procedure PutAnsiStr(const AStr: AnsiString);
This method writes the content of an AnsiString variable into the pipe.
To avoid memory or resource leak, we need to "close" the pipe when we don't need it anymore. Like with the Open method, we also will use method name that mimic the real world pipe, i.e. Close.
Could We Peek, Please?
At the last moment, it occured to me that sometimes we need to know the content of the pipe without actually removing the content. In real world, it's like peeking into the inside of the pipe to see whether it has something. Le't have several Peek methods.
- function Peek: Boolean;
This method checks the pipe if it has some data. It returns true if there is data in the pipe, otherwise it returned false.
- function Peek(var AContentSize: Cardinal): Boolean;
This method operates like the above Peek, but it also returns the size of data available inside the pipe.
- function Peek(var ABuffer; const ABufferSize: Cardinal; var AContentSize: Cardinal): Boolean;
This method works like the previous Peek methods, but it also read the content into the given memory location.
- function Peek(var AStr: AnsiString): Boolean;
This method also works similarly like the previous Peek methods, but it would read current content of the pipe into the passed AnsiString variable.
TAnonymPipe=class public procedure Open; procedure Close; // read methods function Get(var ABuffer; const ABufSize: Cardinal): Cardinal; overload; function Get(ADest: TStream; const AToReadCount: Cardinal=0): Cardinal; overload; function GetAnsiStr: AnsiString; // write methods function Put(const ABuffer; const ADataSize: Cardinal): Cardinal; overload; function Put(ASrc: TStream; const AToWriteCount: Cardinal=0): Cardinal; overload; procedure PutAnsiStr(const AStr: AnsiString); // Peek methods function Peek: Boolean; overload; function Peek(var AContentSize: Cardinal): Boolean; overload; function Peek(var ABuffer; const ABufferSize: Cardinal; var AContentSize: Cardinal): Boolean; overload; function Peek(var AStr: AnsiString): Boolean; overload; // data to go into the pipe should get through here property Input : THandle read FInput; // data from the pipe should be read from here property Output : THandle read FOutput; // indicates if the pipe is active and ready for communication property IsOpened: Boolean read GetIsOpened write SetIsOpened; // returns true if there is no data in the pipe's output buffer property IsEmpty : Boolean read GetIsEmpty; end;Note that some new public properties were added to the class. The first three were mandatory, while IsEmpty was a nice helper. I considered IsEmpty as just a nice helper, because actually we could use Peek here. But I believe in some situation code like "if Pipe.IsEmpty then ..." would be easier to understand than "if Pipe.Peek then ...".
Key Methods Implementation
Here we want to create the anonymous pipe and keep the read and write handle to respective variabel. The implementation is as the following. Note that if you had read the Execute a Console Program and Capture Its Output tutorial, you will familiar with the codes.
procedure TAnonymPipe.Open; var vSecurityDesc: TSecurityDescriptor; vSecurityAttributes: TSecurityAttributes; begin if IsOpened then Exit; if (Win32Platform = VER_PLATFORM_WIN32_NT) then begin //initialize security descriptor (in NT based Windows) InitializeSecurityDescriptor(@vSecurityDesc, SECURITY_DESCRIPTOR_REVISION); SetSecurityDescriptorDacl(@vSecurityDesc, True, nil, false); vSecurityAttributes.lpSecurityDescriptor := @vSecurityDesc; end else vSecurityAttributes.lpSecurityDescriptor := nil; if not CreatePipe(FOutput, FInput, @vSecurityAttributes, 0) then begin FInput := 0; FOutput := 0; raise EAnonymousePipeCreationerror.Create('Can not create anonymous pipe. System error message: '+ SysErrorMessage(GetLastError)); end; end;
- TSecurityAttributes' or SECURITY_ATTRIBUTES' official information can be found in this msdn page.
- Procedure Close;
Here we want to finalize and clean up the created pipe.
procedure TAnonymPipe.Close; begin if not GetIsOpened then Exit; CloseHandle(FInput); CloseHandle(FOutput); FInput := 0; FOutput := 0; end;Function Get
In this case we talk about Get method that read the content of the pipe and put it into the given memory location. And the implementation was:
function TAnonymPipe.Get(var ABuffer; const ABufferSize: Cardinal): Cardinal; begin Result := 0; if not GetIsOpened then raise EClosedPipe.Create('Can not read from closed pipe'); if not ReadFile(FOutput, ABuffer, ABufferSize, Result, nil) then raise EReadPipeFailure.Create('Failed reading pipe. System error: ' + SysErrorMessage(GetLastError)); end;Function Put
Here we only discuss the Put version that read the content of a given memory location and put it into the pipe. And the implementation was:
function TAnonymPipe.Put(const ABuffer; const ADataSize: Cardinal): Cardinal; begin if not GetIsOpened then raise EClosedPipe.Create('Can not write to a closed pipe'); if not WriteFile(FInput, ABuffer, ADataSize, Result, nil) then raise EWritePipeFailure.Create('Failed writing to pipe. System error: ' + SysErrorMessage(GetLastError)); end;Function Peek
Here we only discuss the most complex Peek version out of the four, i.e. the one that read the current content of the pipe into AnsiString variable.
function TAnonymPipe.Peek(var AStr: AnsiString): Boolean; var vReadSize: Cardinal; vBytesLeft: Cardinal; vAvailBytes: Cardinal; begin Result := PeekNamedPipe(FOutput, nil, 0, nil, @vAvailBytes, nil) and (vAvailBytes > 0); if not Result then Exit; SetLength(AStr, vAvailBytes); PeekNamedPipe(FOutput, @AStr, Length(AStr), @vReadSize, @vAvailBytes, @vBytesLeft); end;In this case (actually for all Peek versions), you might be interested to look into Windows API PeekNamedPipe. Visit this msdn page to read official information about it.
For this writing, I created a DUnit project for testing the functionalities of TAnonymPipe. With DUnit unit tests, if in the future we do some change to TAnonymPipe code, we can make sure that the change does not break any functionality by running this DUnit test project.
Full source code of TAnonymPipe along with the DUnit demo project is contained in the following zip file, Demo.zip 746.74KB 806 downloads. Feel free to use or improve any codes.
Edited by LuthfiHakim, 28 April 2013 - 06:22 AM.