Creating DLLs with Simply Fortran
Posted on 2017-07-17
Simply Fortran for Windows can easily produce dynamic link libraries, or DLLs, using the appropriate "New Project" entries on its Start screen. Often times, though, users wish to create DLLs that can interact with other languages and programs, which can mildly complicate matters on Windows. In this post, we'll look at creating a DLL with some functions that can be seamlessly called from the C language. Routines callable from C, of course, are generally callable from any language without much additional effort.
For this example post, we'll create a file averaging.f that contains a number of routines for computing different statsistical "averages," mean, median, and mode to be specific. A simple implementation of a mean routine in (fixed-format) Fortran would be:
c
c Computes the mean of array x
c
function mean(x, n)
implicit none
real::mean
integer, intent(in)::n
real, dimension(:), intent(in)::x
real::array_sum
array_sum = sum(x(1:n),1)
mean = array_sum / real(n)
end function mean
This function does include an integer declaring the length of our array because we eventually want to interact with another language; the C language does not have arrays that explicitly store their sizes.
As written, this routine may or may not be callable from C. The call is somewhat ambiguous, so we'll need to properly declare this function to conform more to what C expects. First, we need to understand that C treats arrays simply as pointers. To maximize portability, we should change our Fortran code to accept a C pointer. Second, C normally passes "by value," as opposed to Fortran's default of "by reference." When a C routine is passing an integer to a function, it is explicitly passing the value of that integer, not a reference to a common memory location like Fortran might use (depending on how we've declared our arguments). Third, we should employ the features of Fortran's C binding abilities, declaring the function appropriately. Our function declaration can now be changed to:
c
c Computes the mean of array x
c
function mean(xc, n) bind(c)
use ISO_C_BINDING
implicit none
real(kind=c_float)::mean
type(c_ptr), value::xc
integer(kind=c_int), value::n
real(kind=c_float), dimension(:), pointer::x
real::array_sum
call C_F_POINTER(xc, x, [n])
array_sum = sum(x,1)
mean = array_sum / real(n)
end function mean
In this updated definition, you'll notice that the function no longer directly accepts an array as an argument. Instead, a C pointer, declared as type(c_ptr) is expected as an argument. Additionally, both the pointer and the integer arguments now have the value attribute in their declarations. As explained, we need to alert the Fortran compiler that these arguments will be passed "by value." We've also changed our declarations of the actual array and the integer argument to be kind=c_float and kind=c_int respectively. These kind declarations, provided by the intrinsic ISO_C_BINDING module, ensure that the declarations in Fortran and the calling C routine both match regardless of the underlying platform.
Since a pointer to an array is passed in, we actually want to access this region of memory as a standard Fortran array. To do so, we've used the C_F_POINTER intrinsic subroutine that accepts a C array and assigns it to a Fortran array such that it may be used as any Fortran-native array would be. This step would be problematic, though, if we hadn't used the proper kind when declaring our Fortran array.
The above routine should work beautifully if called from a C routine on any platform, but we're trying to move towards a complete DLL for Windows. We need one minor addition to this function so that we can ensure its interoperability with other langugages. First, we need to declare that this function is to be exported using a special type of comment that acts as a compile directive. In this case, we'll add:
function mean(xc, n) bind(c)
use ISO_C_BINDING
implicit none
cGCC$ ATTRIBUTES DLLEXPORT :: mean
...
The addition of the cGCC$ ATTRIBUTES DLLEXPORT line simply instructs the compiler to export this function so that external programs can call it. This step can be essential if you're working with, for example, Microsoft Visual C++, as we'll see shortly.
On 32-bit Windows (or any 32-bit process on 64-bit Windows), DLLs can provide functions using a whole assortment of calling conventions, an extremely technical topic that is nonetheless important. Generally speaking DLLs normally provide functions in the stdcall format, but there are countless exceptions. In order to clear up any confusion surrounding the calling convention on 32-bit Windows, we can explicitly declare it:
function mean(xc, n) bind(c)
use ISO_C_BINDING
implicit none
cGCC$ ATTRIBUTES STDCALL, DLLEXPORT :: mean
...
The addition of STDCALL on 64-bit Windows is useless as only one calling convention is used on that operating system as a general rule. Simply Fortran doesn't need to provide calling convention information for 64-bit DLLs as it conforms to the Windows standard.
Assuming, though, that we're building a 32-bit DLL, our function definition in its entirety has become:
c
c Computes the mean of array x
c
function mean(xc, n) bind(c)
use ISO_C_BINDING
implicit none
cGCC$ ATTRIBUTES STDCALL, DLLEXPORT :: mean
real(kind=c_float)::mean
type(c_ptr), value::xc
integer(kind=c_int), value::n
real(kind=c_float), dimension(:), pointer::x
real::array_sum
call C_F_POINTER(xc, x, [n])
array_sum = sum(x,1)
mean = array_sum / real(n)
end function mean
We can similarly write routines to compute median and mode of an array, and the entire source code is available in averaging.f. The routine for mode might be of particular interest as it has to return an array of modes since there can theoretically be multiple.
The file linked can now be compiled into an entirely standards-compliant DLL for Windows. If we're going to create an executable calling this DLL using Simply Fortran, we can just add this DLL to a project and properly declare these three external functions in the calling code.
We might want to add a single addition, though, if we're planning on calling this from a program compiled by a Microsoft compiler. Specifically, Microsoft compilers use a .lib file to link to a DLL automatically. Simply Fortran, though, generates a .a file as an import library, which is not comaptible. We need to make one change to the Project Options, specifically adding the following to the Linker box under Compiler Flags in the Project Options window:
-Wl,--output-def,fortran-library.def
The above command asks the linker to generate a library definition file for use with Microsoft's library utilities. Simply Fortran probably should present this option explicitly to the user rather than requiring adding this flag, but it currently doesn't (perhaps in the next version?). When we compile the DLL, we now get an additional file, fortran-library.def, that contains:
EXPORTS
mean@8 @1
median@8 @2
mode@12 @3
The inclusion of the DLLEXPORT attribute in the earlier function definitions causes the names of functions to be properly emitted into this file. This file can be used with Microsoft's library utility to generate a proper .lib file:
lib /machine:i386 /def:fortran-library.def
The command above results in a file fortran-library.lib that may be used with Microsoft Visual C++.
A C routine that calls these functions is relatively straightforward:
#include <stdio.h>
#include <math.h>
#include <stdlib.h>
#include <time.h>
#define ARRAY_SIZE 256
extern float __stdcall mean(float x[], int n);
extern float __stdcall median(float x[], int n);
extern int __stdcall mode(float x[], int n, float m[]);
int main()
{
float values[ARRAY_SIZE];
int i, n;
time_t t;
float m[ARRAY_SIZE];
/* Intializes random number generator */
srand((unsigned)time(&t));
for (i = 0; i < ARRAY_SIZE; i++) {
/* Populate this array with values [0,50) */
values[i] = (float)(rand() % 50);
}
printf("Mean (from Fortran): %f\n", mean(values, ARRAY_SIZE));
printf("Median (from Fortran): %f\n", median(values, ARRAY_SIZE));
n = mode(values, ARRAY_SIZE, m);
if(n == 0)
printf("There was no mode\n");
else {
printf("Modes: ");
for(i=0;i<n;i++) {
printf("%f ", m[i]);
}
printf("\n");
}
return 0;
}
Prototypes for our DLL routine appear near the beginning of the C source code. Note that, to be explicit, we've again stated that these routines are using the stdcall convention. If we were on another platform or creating a 64-bit executable, we wouldn't need the __stdcall declaration. If we compile the above C code, using Simply Fortran's C compiler or Microsoft's C compiler, and link with the appropriate import library, we now have an executable that should proprely call into a Fortran DLL.
The source for both files in their entirety are below:
- Fortran source code: averaging.f
- C driver source code: driver.c
In the next post, we'll look at calling these routines from Microsoft Excel as opposed to a custom C program.