Recently, I’ve encountered the need to read the contents of ANSYS Mechanical result files (e.g. file.rst, file.rth) into a C++ application that I am writing for a client. Obviously, these files are stored in a proprietary binary format owned by ANSYS, Inc. Even if the format were published, it would be daunting to write a parser to handle it. Fortunately, however, ANSYS supplies a series of routines that are stored in a library called BinLib which allow a programmer to access the contents of a result file in a procedural way. That’s great! But, the catch is the routines are written in FORTRAN… I’ve been programming for a long time now, and I’ll be honest, I can’t quite stomach FORTRAN. Yes, the punch card days were before my time, but seriously, doesn’t a compiler have something better to do than gripe about what column I’m typing on… (Editor’s note: Matt does not understand the pure elegance of FORTRAN’s majestic simplicity. Any and all FORTRAN bashing is the personal opinion of Mr. Sutton and in no way reflects the opinion of PADT as a company or its owners. – EM)
So, the problem shifts from how to read an ANSYS result file to how to interface between C/C++ and FORTRAN. It turns out this is more complicated than it really should be, and that is almost exclusively because of the abomination known as CHARACTER(*) arrays. Ah, FORTRAN… You see, if weren’t for the shoddy character of CHARACTER(*) arrays the mapping between the basic data types in FORTRAN and C would be virtually one for one. And thus, the mechanics of function calls could fairly easily be made to be identical between the two languages. If the function call semantics were made identical, then sharing code between the two languages would be quite straightforward. Alas, because a CHARACTER array has a kind of implicit length associated with it, the compiler has to do some kind of magic within any function signature that passes one or more of these arrays. Some compilers hide parameters for the length and then tack them on to the end of the function call. Some stuff the hidden parameters right after the CHARACTER array in the call sequence. Some create a kind of structure that combines the length with the actual data into a special type. And then some compilers do who knows what… The point is, there is no convention among FORTRAN compilers for handling the function call semantics, so there is no clean interoperability between C and FORTRAN.
Fortunately, the Intel FORTRAN compiler has created this markup language for FORTRAN that functions as an interoperability framework that enables FORTRAN to speak C and vice versa. There are some limitations, however, which I won’t go into detail on here. If you are interested you can read about it in the Intel FORTRAN compiler manual. What I want to do is highlight an example of what this looks like and then describe how I used it to solve my problem. First, an example:
What you see in this image is code for the first function you would call if you want to read an ANSYS result file. There are a lot of arguments to this function, but in essence what you do is pass in the file name of the result file you wish to open (Fname), and if everything goes well, this function sends back a whole bunch of data about the contents of the file. Now, this function represents code that I have written, but it is a mirror image of the ANSYS routine stored in the binlib library.
I’ve highlighted some aspects of the code that constitute part of the interoperability markup. First of all, you’ll notice the markup BIND highlighted in red. This markup for the FORTRAN function tells the compiler that I want it to generate code that can be called from C and I want the name of the C function to be “CResRdBegin”. This is the first step towards making this function callable from C. Next, you will see highlighted in blue something that looks like a comment. This however, instructs the compiler to generate a stub in the exports library for this routine if you choose to compile it into a DLL. You won’t get a .lib file when compiling this as a .dll without this attribute. Finally, you see the ISO_C_BINDING and definition of the type of character data we can make interoperable. That is, instead of a CHARACTER(261) data type, we use an array of single CHARACTER(1) data. This more closely matches the layout of C, and allows the FORTRAN compiler to generate compatible code. There is a catch here, though, and that is in the Title parameter. ANSYS, Inc. defines this as an array of CHARACTER(80) data types. Unfortunately, the interoperability stuff from Intel doesn’t support arrays of CHARACTER(*) data types. So, we flatten it here into a single string. More on that in a minute.
You will notice too, that there are markups like (c_int), etc… that the compiler uses to explicitly map the FORTRAN data type to a C data type. This is just so that everything is explicitly called out, and there is no guesswork when it comes to calling the routine. Now, consider this bit of code:
First, I direct your attention to the big red circle. Here you see that all I am doing is collecting up a bunch of arguments and passing them on to the ANSYS, Inc. routine stored in BinLib.lib. You also should notice the naming convention. My FORTRAN function is named CResRdBegin, whereas the ANSYS, Inc. function is named ResRdBegin. I continue this pattern for all of the functions defined in the BinLib library. So, this function is nothing more than a wrapper around the corresponding binlib routine, but it is annotated and constrained to be interoperable with the C programming language. Once I compile this function with the FORTRAN compiler, the resulting code will be callable directly from C.
Now, there are a few more items that have to be straightened up. I direct your attention to the black arrow. Here, what I am doing is converting the passed in array of CHARACTER(1) data into a CHARACTER(*) data type. This is because the ANSYS, Inc. version of this function expects that data type. Also, the ANSYS, Inc. version needs to know the length of the file path string. This is stored in the variable ncFname. You can see that I compute this value using some intrinsics available within the compiler by searching for the C NULL character. (Remember that all C strings are null terminated and the intent is to call this function from C and pass in a C string.)
Finally, after the call to the base function is made, the strings representing the JobName and Title must be converted back to a form compatible with C. For the jobname, that is a fairly straightforward process. The only thing to note is how I append the C_NULL_CHAR to the end of the string so that it is a properly terminated C string.
For the Title variable, I have to do something different. Here I need to take the array of title strings and somehow represent that array as one string. My choice is to delimit each title string with a newline character in the final output string. So, there is a nested loop structure to build up the output string appropriately.
After all of this, we have a C function that we can call directly. Here is a function prototype for this particular function.
So, with this technique in place, it’s just a matter of wrapping the remaining 50 functions in binlib appropriately! Now, I was pleased with my return to the land of C, but I really wanted more. The architecture of the binlib routines is quite easy to follow and very well laid out; however, it is very, very procedural for my tastes. I’m writing my program in C++, so I would really like to hide as much of this procedural stuff as I can. Let’s say I want to read the nodes and elements off of a result file. Wouldn’t it be nice if my loops could look like this:
That is, I just do the following:
- Ask to open a file (First arrow)
- Tell the library I want to read nodal geometric data (Second arrow)
- Loop over all of the nodes on the RST file using C++11 range based for loops
- Repeat for elements
Isn’t that a lot cleaner? What if we could do it without buffering data and have it compile down to something very close to the original FORTRAN code in size and speed? Wouldn’t that be nice? I’ll show you how I approached it in Part 2.