StdIn and StdOut - Felix John COLIBRI. |
- abstract : sending and receiving strings to and from a CONSOLE project which uses Readln and Writeln
- key words : Windows pipes, StdIn, StdOut, Readln, Writeln, CONSOLE, CreateProcess
- software used : Windows XP, Delphi 6
- hardware used : Pentium 1.400Mhz, 256 M memory, 140 G hard disc
- scope : Delphi 1 to 8 for Windows
- level : Delphi developer
- plan :
1 - Introduction
For a simple CGI server, we had to communicate with a CGI.EXE CONSOLE application which uses
Readln and
Writeln. When our web server itself is a CONSOLE application, those input / output procedures work as expected. But whenthe CGI server is a GUI application, Delphi tells us that
Readln and
Writeln are no longer valid.
A couple of communications found using Google and the Borland Newsgroups didprovide the solution. It was still a one day fight before I understood what was required. Here is our result, without all possible bells and whistles, but which somehow seem to work, at least for our simple CGI server.
2 - IO principle
2.1 - Console applications
Here is a simple
my_echo.
exe CONSOLE program, which reads a string, and sends it back in uppercase:
(*$APPTYPE CONSOLE *) program p_console_read_write_one; var g_string: String; begin readln(g_string); writeln(UpperCase(g_string)); end. |
If we compile and execute
| we will see a black DOS windows, waiting for input: |
| we type some string on the keyboard and hit Enter, like Hello Enter |
| the string will be echoed back And since this is the last instruction, the program will terminate (and youwon't see anything unless you block the program with another Readln before the END, and type Enter to quit the program). |
Basically, the keyboard feeds the
Readln, and
Writeln sends the output to the screen. In C parlance,
Readln grabs input from the
StdIn handle, and
Writeln sends characters to the
StdOut handle.
2.2 - GUI Applications
We now want to call this program from another Delphi GUI application, which will provide the string, and display the uppercase answer:
So basically we have to send our string to my_echo.StdIn, and fetch whatever my_echo.StdOut sends back. We CANNOT use my_gui.Writeln andmy_gui.Readln, since Delphi tells us that my_gui.StdIn and my_gui.StdOut are not available in GUI application. We must use pipes which are some kindof files with two handles: a read handle and a write handle.
To send Hello from the gui to the echo:
- my_gui writes the string at the write extremity of the pipe
- my_echo reads this string from the read extremity of the same pipe:
In our case, the GUI application will create two pipes:
- a std_in pipe where the my_gui application pushes the string, and my_echo will use for its Readln calls
- a std_out pipe where my_echo will place its Writeln strings and that my_gui will use to get this string
Therefore
- my_gui creates both pipes
- it launches my_echo by creating a Windows Process with special parameters to force my_echo to use our pipes for StdIn and StdOut
- after my_eco is started
- my_gui write a string
- my_echo reads this string and writes the uppercase string and terminates
- my_gui reads the uppercase string
- everything is cleaned up
3 - The Delphi source code
3.1 - The echo program
The echo program
Reads and
Writes, and we also added logging to be able tomonitor what's going on. Here is the program:
(*$APPTYPE CONSOLE *) program p_console_read_write_one; var g_string: String; begin readln(g_string); writeln(UpperCase(g_string)); end. |
3.2 - The u_c_stdin_sdtout_exe unit
This unit creates the pipes, starts the console exe and can read and writestrings to and from the console exe.
Since the pipe uses two handles, we placed them in a record:
t_pipe= record m_read_handle, m_write_handle: tHandle; end; |
And the handles are created with this function:
function f_create_pipe(Var pv_pipe: t_pipe): boolean; const k_pipe_buffer_size= 4096; begin with pv_pipe do begin // -- Create the pipe result:= CreatePipe(m_read_handle, m_write_handle, nil, k_pipe_buffer_size);
// -- recreate the handles, this time with the "inheritable" flag if result then result:= DuplicateHandle(GetCurrentProcess, m_read_handle, GetCurrentProcess, @ m_read_handle, 0, True, DUPLICATE_CLOSE_SOURCE OR DUPLICATE_SAME_ACCESS); if result then result:= DuplicateHandle(GetCurrentProcess, m_write_handle, GetCurrentProcess, @ m_write_handle, 0, True, DUPLICATE_CLOSE_SOURCE OR DUPLICATE_SAME_ACCESS); end; end; // f_create_pipe procedure close_pipe(p_pipe: t_pipe); begin with p_pipe do begin CloseHandle(m_read_handle); CloseHandle(m_write_handle); end; end; // close_pipe |
Note that:
- the buffer size value included in the CreatePipe is irrelevant, since eachReadFile and WriteFile has its own size parameters
- the strange DuplicateHandle call is mandatory to allow the handles to be inherited by the child process that will be created later.
It seems that the same result could be obtained by using an NT security record during the CreatePipe call, but this record has to be created, initialized etc.
- our creation function will be called to initialize both StdIn and StdOut pipes
The console .EXE is launched as a child process of the gui process, using the Windows
CreateProcess function:
function CreateProcess(lpApplicationName: PChar; lpCommandLine: PChar; lpProcessAttributes, lpThreadAttributes: PSecurityAttributes; bInheritHandles: BOOL; dwCreationFlags: DWORD; lpEnvironment: Pointer; lpCurrentDirectory: PChar; const lpStartupInfo: TStartupInfo; var lpProcessInformation: TProcessInformation): BOOL; stdcall;
|
where:
- lpCommandLine is the console exe name and any command line parameters
- bInheritHandles forces the new process to inherit the handles that can bespecified in the lpStartupInfo parameter
- lpStartupInfo is a record containing initial parameters for the new process, including the hStdIn, hStdOut and hStdErr handles (if bInheritHandles is True)
- lpProcessInformation is the record filled by the CreateProcess call and containing the new hProcess handle. This handle will be used to check the process status, and eventually to kill it
The
CreateProcess, sending and receiving have been encapsulated in a
c_stdin_stdout_process CLASS. This is its definition (partial):
c_stdin_stdout_process= class(c_basic_object) m_exe_name: String; m_stdin_pipe, m_stdout_pipe: t_pipe;
m_process_handle: Cardinal; m_received_string: String;
constructor create_stdin_stdout_process(p_name: String);
function f_create_stdin_stdout_pipes: Boolean; procedure close_stdin_stdout_pipes;
function f_create_process: Boolean;
procedure write_string(p_string: AnsiString); procedure read_string;
procedure do_stop_process;
destructor Destroy; override; end; // c_stdin_stdout_process
|
and:
- m_exe_name: the path and name of the console exe which must be initialized before calling the main f_create_process method
- m_stdin_pipe and m_stdout pipes are the two pipes which must be created before calling f_create_process
- during f_create_process
- the lpStartupInfo will be initialized:
VAR l_startup_info: TStartupInfo; Zeromemory(@ l_startup_info, SizeOf(l_startup_info)); with l_startup_info do begin cb:= sizeof(l_startup_info); dwFlags:= STARTF_USESHOWWINDOW or STARTF_USESTDHANDLES; hStdInput:= m_stdin_pipe.m_read_handle; hStdError:= m_stdout_pipe.m_write_handle; hStdOutput:= m_stdout_pipe.m_write_handle; end; // with l_startup_info |
- CreateProcess is called
- l_process_info.hProcess is saved
- write_string can then be called to send a string to my_console.Readln
- read_string will read a string and place it in m_received_string
- kill_process can be used to kill the process
3.3 - The gui application
Here is a sample application:
- clicking the "start_" button will
- create a c_stdin_stdout_exe object
- call the f_create_stdin_stdout pipes
- fill the m_exe_name with p_console_read_write.exe
- call f_create_process
- clicking "do_write_" will sent Edit1.Text to p_console_read_write.exe
- clicking "do_read_" will fetch the any string that p_console_read_write.Write has sent
- kill can abort the p_console_read_write.exe
Here is a snapshot of the project:
4 - Improvements
This unit has been created to solve CGI server problem. In our case:
- our simple server will send a "small" string (like 100 or 200 character long) to the CGI exe
- the CGI.EXE only peforms one Readln, processes this string, which resultsin a single Writeln. And after this Writeln, the CGI.EXE reaches the end and the CGI.EXE terminates.
- the CGI server will read back the "small" HTML page (also about 100 or 200 characters)
We did not spend much time to investigate other communication possibilities and on "industrial strength" issues:
- are all the handles closed at the end, or if some exception is raised
- what happens to programs which require many read / write sequences
- can we transfer huge blocks (several megs). If the CONSOLE sends a big amount in one Write, is a single read enough ? What if the CGI.EXE callsseveral Writeln ?
Let us mention some possible solutions:
5 - Download the Sources
Here are the source code files:
Those .ZIP files contain:
- the main program (.DPR, .DOF, .RES), the main form (.PAS, .DFM), and anyother auxiliary form
- any .TXT for parameters
- all units (.PAS) for units
Those .ZIP
- are self-contained: you will not need any other product (unless expressly mentioned).
- can be used from any folder (the pathes are RELATIVE)
- will not modify your PC in any way beyond the path where you placed the .ZIP (no registry changes, no path creation etc).
To use the .ZIP:
- create or select any folder of your choice
- unzip the downloaded file
- using Delphi, compile and execute
To remove the .ZIP simply delete the folder.
本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请
点击举报。