Writing From Fortran to an AppGraphics Window
Posted on 2023-02-15
Since AppGraphics was introduced, we received a number of queries about the possibility of writing directly to an AppGraphics window from Fortran WRITE statements. This functionality was included in older Microsoft and DEC/Compaq Fortran runtimes, and it may still even be included in another single-architecture compiler.
In the past, we've suggested the non-trivial task of replacing this code with calls to outtext and outtextxy as an alternative. That solution, though, required a substantial amount of code, including custom scrolling code and graphical text formatting. In addition, in order to use Fortran FORMAT statement or strings, intermediate character variables were necessary as well. Saying that the behavior could be easily emulated was a wild simplification of the work needed to do so.
AppGraphics recently introduced a multiline text box that relies on Windows to handle most of the complications of text editing and output. This control resolves most of the problems with the large amount of code necessary to implement text output and scrolling, leaving only the direct connection to the Fortran WRITE statements.
The Autotext Module
To avoid changes to the Fortran runtime library, we'd like introduce a separate module that could look for writes to a given Fortran unit number and display those changes in a multiline text box. Ideally, the interface would be simple and clean. What we'd really like to see is a single call to connect a text box to a unit number, something like:
call assignunit(mytext, myunit)
where mytext is the identifier of a multiline text box and myunit is the unit number to "follow." This call would need to effectively start a separate thread that will monitor the unit myunit for changes. Threading and change monitoring on Windows is absolutely possible, but the API is probably best expressed in C for simpler access to the Windows API.
To make a call to C, we'll need an intermediate function to handle the transition between languages. Consider the following wrapper code:
subroutine assignunit(tb, unit)
use iso_c_binding
implicit none
integer::tb
integer, intent(in)::unit
character(len=8192, kind=c_char), target::filename
logical::is_open
interface
subroutine assignfiletotextbox(tb, cf, u) bind(c, name="assignfiletotextbox")
use iso_c_binding
type(c_ptr), value::cf
integer(kind=c_int), value::tb, u
end subroutine assignfiletotextbox
end interface
filename = " "
inquire(unit=unit, name=filename, opened=is_open)
if(len_trim(filename) > 0) then
filename = trim(filename)//c_null_char
call assignfiletotextbox(tb, c_loc(filename), unit)
end if
end subroutine assignunit
There are a few things to note about this call. First, C doesn't have an understanding of Fortran unit numbers, so we'll need to pass a filename to the routine for monitoring. The INQUIRE statement can retrieve the filename for us based on a unit, so the end user doesn't need to pass a filename into the routine. Second, we have some boilerplate code for passing a proper C string, terminated with a null character, into the C routine defined in an interface.
You might also notice we're passing the unit number itself; we'll talk more about that later.
The Windows API Calls
The goals of our C code should be:
- Launch a thread, passing information about the file to monitor
- Open the file for reading in the most permissive manner possible
- Check the file for changes and display those changes in a text box
- Stop monitoring as soon as the unit is closed by the original Fortran code.
Launching the thread basically involves passing all of our information from Fortran, including the text box identifier, the filename, and the Fortran unit number into a new thread via the Windows CreateThread API call. Our code now becomes interesting.
To check the file for changes, we need to use the Windows FindFirstChangeNotification API and its related calls. This API allows us to monitor a directory for changes. While this call does look at an entire directory rather than just our single file, it is still preferable to rapidly checking a file for changes every tenth of a second, for example. If we extract the directory from the filename, we can set up a change notification handler in code:
HANDLE hChange;
/* Start monitoring the directory for changes */
hChange = FindFirstChangeNotification(dir, FALSE, FILE_NOTIFY_CHANGE_SIZE|FILE_NOTIFY_CHANGE_LAST_WRITE);
We also need to open the file for reading; there should be no harm in doing so immediately in our monitor thread:
HANDLE hFile;
/* Open the file for reading only */
hFile = CreateFile(tp->allocated_filename,
GENERIC_READ,
FILE_SHARE_DELETE|FILE_SHARE_READ|FILE_SHARE_WRITE,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);
In these calls, we are relying on the Windows API pretty heavily rather than the possibly more familiar standard C fopen calls. However, using the Windows API in this case allows us to set some permissions (FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE) that might only be implied with fopen calls.
Next, we can set up a simple loop to look for changes to our file:
do {
/* We'll wait for a directory change notification, but, because of
* various caching situations, sometimes the notification doesn't
* arrive in a timely manner. We'll timeout after 1.5s and check
* if the file write time has changed anyways.
*/
res = WaitForSingleObject(hChange, (DWORD)1500);
GetFileTime(hFile, NULL, NULL, &ft);
/* If the file write time changed, populate the text box again */
if(CompareFileTime(&ft, &ftLast) != 0) {
lastSize = PopulateFrom(hFile, tp->tb, &pos1, &pos2);
GetFileTime(hFile, NULL, NULL, &ftLast);
}
/* The while loop refreshes the change notification iff a change notification was received,
* otherwise, per the Windows API, you need to skip calling it. Note that we also call a
* Fortran routine to check that the unit is indeed still open. If closed, we can quit.
*/
} while((res == WAIT_TIMEOUT || FindNextChangeNotification(hChange)));
This loop might look a bit daunting, so we can walk through it. The call to WaitForSingleObject is passed a handle to our change notifications; it effectively sits and waits for that change to occur or, alternatively, 1500 milliseconds to pass. Once either a change is reported or the call times out, the program continues. Because we're in a separate thread from our main program, there is no harm in blocking execution.
Next, we'll again use the Windows API to check the time of the last write to the file. If the file has been written to since the last pass through the loop, we'll go ahead and call our routine to populate the text box.
The loop's condition requires that either on of two things occurs. If the update was triggered by a timeout, then proceed. If a change notification triggered the update, we need to look for a new update by calling FindNextChangeNotification.
Textbox Updates
The routine PopulateFrom will then take over performing the update. It accepts a textbox to write to, a handle to the file, and two variables meant to hold the last position read in the file of interest. Our routine is relatively simple:
static DWORD PopulateFrom(HANDLE hFile, int tb, LONG *pos1, LONG *pos2)
{
char buf[64];
DWORD bytesRead;
SetFilePointer(hFile, *pos1, pos2, FILE_BEGIN);
ZeroMemory(buf, 64);
while(ReadFile(hFile, buf, 63, &bytesRead, NULL) && bytesRead > 0) {
settextboxcursorposition(tb, MAXINT);
inserttextboxtext(tb, buf);
ZeroMemory(buf, 64);
}
*pos2 = 0;
*pos1 = (LONG)SetFilePointer(hFile, 0, pos2, FILE_CURRENT);
return GetFileSize(hFile, NULL);
}
Upon entry to the routine, we ask the operating system to position our file read operation back to the last read position via SetFilePointer. Next, using a 64-byte buffer, we read the file in a loop. If successful, our AppGraphics calls first advance the cursor to the end of our text box and, next, append the last read bytes of text. If ReadFile fails, meaning there is no longer any text available, we record the current position in the file and return the current file size (which is informational only).
Checking for an Open Unit
As I had explained, we want to exit the monitoring thread if the main program closes the unit of interest. The C portion of the code, however, doesn't have a clean way to check Fortran unit numbers. We have to add our own Fortran routine that will be accessible from C and report if the unit remains open. The following routine does so:
function isfortranunitopen(u) bind(c, name="isfortranunitopen")
use iso_c_binding
implicit none
logical(kind=c_bool)::isfortranunitopen
integer(kind=c_int), value::u
inquire(unit=u, opened=isfortranunitopen)
end function isfortranunitopen
You'll note that this routine is using the BIND statement in its declaration to ensure a clean C call. Inside the routine, it merely checks, using INQUIRE, if the unit remains open. The equivalent C prototype is:
/* A Fortran call to see if the file is opened */
extern bool isfortranunitopen(int u);
We can now update our monitoring loop accordingly to check if the file is still open:
do {
...
} while((res == WAIT_TIMEOUT || FindNextChangeNotification(hChange)) && isfortranunitopen(tp->fortran_unit));
Now our routine will also terminate cleanly if the calling program closes the unit.
An Example Program
As an example, we can try a simple exercise in opening a file and writing information to it incrementally. Consider the following short program:
program main
use appgraphics
use m_autotext
implicit none
integer::myscreen, mytext, myunit
integer::i
call setapplicationdpiaware()
myscreen = initwindow(800, 600, closeflag=.TRUE.)
call settextstyle(MONOSPACE_FONT, HORIZ_DIR, scaledpi(12))
mytext = createmultilinetextbox(5,5,790,590,.false.)
open(newunit=myunit, file="temp.txt", status="UNKNOWN", position="append")
write(myunit, *) "Running Output:"
call assignunit(mytext, myunit)
do i=1,10
write(myunit, *) i*100
flush(myunit)
call sleep(1)
end do
close(myunit)
call loop()
call closewindow(myscreen)
end program main
Basically, all that's occuring here is that we're opening an AppGraphics window, adding a text box, opening a file, connecting it to the text box, and, finally, writing an integer to the text file every second for ten seconds.
One important note is the inclusion of a FLUSH statement immediately following the WRITE statement. The FLUSH statement effectively tells the Fortran runtime to write out any pending data to the specified unit. For performance reasons, the runtime and operating system may cache data to be written until a certain threshhold is reached. However, we'd like our data to be written as soon as possible so it appears in our window. Therefore, we'll request that the unit be flushed.
When we compile everything, you'll get a window that updates intermittently as the file "temp.txt" changes:
We've glossed over a few details like how information is passed into the thread (a C struct), how we determine our directory to monitor, and some C memory management, but all the code is contained in an example project below:
If people like this functionality, perhaps it can be rolled into AppGraphics itself! Let us know what you think on the Forums.