Introduction
This article discusses bridging from a program written in Fortran to a DLL written in the .NET language C#. Many articles are dedicated to bridging from a C-language to Fortran. The purpose of this article is to give a complete example of bridging from Fortran to C#.
In simple terms, part of the legacy system consists of a client program which calls a core DLL; both of which were written in Fortran. The calling program is maintained by one organization, the core DLL by another. The core DLL is then migrated to the .NET language C#. The task is to build a Fortran to C# bridge to:
- satisfy the legacy interface and
- support the migrated implementation of the core DLL in C#
Set-up
Using Visual Studio 2015 with the Intel® Parallel Studio XE 2016 Update 3 Composer Edition for Fortran Windows* Integration for Microsoft Visual Studio 2015 package, the following projects were created:
Client
– a Visual Fortran Console Application representing the client program Core
– a Visual Fortran Dynamic-link Library satisfying the legacy interface to the client program CppWrapper
– a Visual C++/CLR Class Library bridging from native C++ to managed C# CSharpCore
– a Visual C# Class Library representing the migrated Core functionality
The code contained in these projects is illustrative in nature: it places a premium on simplicity and readability and contains only the programming constructs necessary to illustrate concepts of C-Interopability.
Client
The Client
program is maintained by another organization. For demonstration purposes, a short program has been created whose sole purpose is to link to the Core
library and call its core
method. At the beginning of the Client
program, the Core
library is imported:
The Client
calls the core
method of the Core
library. The method name and its parameters are specified by an interface document signed-off by the two stakeholder organizations.
CALL CORE_METHOD(p1, p2, p3, p4, p5, p6, p7, p8)
The core
method has 8 calling parameters: 4 input and 4 output. The input parameters give the Client
organization the ability to control aspects of the computation and the output. The output parameters communicate results back from the Core
organization to the Client
organization. The parameters for the core
method are defined in the variable section of the Client
:
INTEGER :: p1
REAL (KIND=precision), dimension(xdim_p2,ydim_p2) :: p2
REAL (KIND=precision), dimension(num_p3) :: p3
INTEGER :: p4
INTEGER :: p5
INTEGER :: p6
character(LEN=len_p7), dimension(num_p7), TARGET :: p7
INTEGER, dimension(num_p8) :: p8
In particular, the parameters of the core
method must satisfy the requirements stated in the interface
document:
Name | Type | Dimension | Input/Output |
p1 | int | - | input |
p2 | 2-d double precision array | xdim_p2, ydim_p2 | input |
p3 | 1-d double precision array | num_p3 | output |
p4 | int | - | input |
p5 | int | - | output |
p6 | int | - | output |
p7 | 1-d char array | num_p7 | output |
p8 | 1-d int array | num_p8 | input |
In the attached code sample, the parameters are defined with: xdim_p2
= 4, ydim_p2
= 5, num_p3
= 8, num_p7
= 10, num_p8
= 5, precision
= 8 and len_p7
= 80.
Core
The Core
library is maintained by the second organization. The purpose of the Core
library is to implement the core domain logic. As stated above, the Core
library which was originally written in Fortran has been migrated to a C# library.
Since Fortran and C++ are both native programming languages, it should be possible to call a method of a C++ library directly from the Fortran Client. With regard to the figure in the introduction, this would imply calling a method of the CppWrapper
library directly from the Client
.
Examining the definitions of the parameters in the Client
call of the last section, we see that parameter p7
is a one-dimensional array whose elements are character-type with length 80
. According to the chapter, Interoperability with C of the Fortran Standard, [1], "if the type is character, the length type parameter is interoperable if and only if its value is one", [2]. Since the length type, len_p7
, is 80 and not 1, p7
is not directly interoperable with C. In other words, a direct interoperability between the Client
and the CppWrapper
library is not possible. For this reason, it was necessary to introduce the Core
library. The Core
library is a Fortran DLL and is shown in the figure.
Before getting into the details of p7
, the syntax to export the core
method for access by the calling Client
is:
To enable C-Interopability, the ISO_C_BINDING
module, which supports Fortran’s interoperability with C by exposing native C types, must be imported:
USE ISO_C_BINDING, ONLY: C_INT, C_FLOAT, C_DOUBLE, C_CHAR, C_LOC, C_PTR, C_NULL_CHAR
The ONLY syntax restricts the entities which are accessible from the module. It also shows the reader exactly which entities are being used in the program.
The main purpose of the Core
library is to define a Fortran C-Interoperable interface to the CppWrapper
library. The interface accesses the CppWrapper
function shown in the BIND
attribute's NAME
label:
INTERFACE
SUBROUTINE CORE_FORTRAN_WRAPPER(p1, p2, p3, p4, p5, p6, nos_p7, ptrs_p7, p8) _
BIND(C,NAME='CORE_C_WRAPPER')
USE ISO_C_BINDING
implicit none
include "defs.fi"
INTEGER (C_INT), VALUE, intent(in) :: p1
REAL (KIND=8), dimension(xdim_p2,ydim_p2), intent(in) :: p2
REAL (KIND=8), dimension(num_p3), intent(out) :: p3
INTEGER (C_INT), VALUE, intent(in) :: p4
INTEGER (C_INT), intent(out) :: p5
INTEGER (C_INT), intent(out) :: p6
INTEGER (C_INT), VALUE, intent(in) :: nos_p7
TYPE (C_PTR), dimension(num_p7), intent(out) :: ptrs_p7
INTEGER (C_INT), dimension(num_p8), intent(in) :: p8
END SUBROUTINE CORE_FORTRAN_WRAPPER
END INTERFACE
Notice that the p7
parameter in the core
method of the Client
has been replaced by two parameters in the interface
definition of the Core
library:
nos_p7
- an input integer containing the size of the p7
array ptrs_p7
- an array of pointers the same size of the p7
array
That is, to pass p7
(what is essentially an array of string
s), between Fortran and C, it is replaced by a pointer array with each element pointing to the address of the first element of the string
. This method was suggested in [2]. With this parameter re-definition, the core
method shown in the interface is now C-Interoperable.
Before calling this method, all we have to do is set the values of the pointer array, ptrs_p7
:
do i=1,num_p7
ptrs_p7(i) = C_LOC(p7(i))
end do
Note that the input integers p1
, p4
, and nos_p7
are passed with the VALUE
attribute. These parameters are interoperable with the corresponding formal parameter type of the CppWrapper
function (C_INT <-> int
). The output integers p5
, and p6
are passed without the VALUE
attribute. These parameters are interoperable with the format parameter pointer type, a reference type, of the CppWrapper
function (C_INT <-> int*
). The CppWrapper
library is described in the next section. For details, see [1].
Finally, the interface method call is:
CALL CORE_FORTRAN_WRAPPER(p1, p2, p3, p4, p5, p6, nos_p7, ptrs_p7, p8)
CppWrapper
In MS Visual Studio, a C++ Dynamic-link library with Common Language Runtime Support, e.g., a C++/CLI DLL, has been created. In the implementation of the CppWrapper
method, the native parameters are translated to CLI parameters. After this has been accomplished, the C# core method can be called.
CppWrapper.h
The target method of the Fortran native library, the Core
library of the last section, is defined in the file CppWrapper.h. As mentioned above, the target method's name was defined in the interface subroutine.
namespace CppWrapper {
extern "C" void API CORE_C_WRAPPER(
int p1,
double p2[][ParameterSize::xdim_p2],
double* p3,
int p4,
int* p5,
int* p6,
int nos_p7,
char** ptrs_p7,
int* p8);
}
The parameter p2
is a 2-dimensional double precision array. Array declarations in Fortran and C are the reverse of one another: Fortran arrays are grouped in column-major order and C arrays in row-major order, [3] . To illustrate this, consider the 2-dimensional array AF:
In Fortran, AF is declared as AF(2,3)
and the elements of AF are stored in column-major order. Thus, the memory allocation is {Address, Value} = {(1,22), (2,33), (3,6), (4,7), (5,40), (6,50)}.
Now consider the 2-dimensional array AC:
In C++, AC is declared as AC[3,2]
and since the elements of AC are stored in row-major order, the memory allocation is {Address, Value} = {(0,22), (1,33), (2,6), (3,7), (4,40), (5,50)}. With the exception of the address index, this is the same memory allocation as AF(2,3)
, e.g., AF(2,3)
is equivalent to AC[3,2]
. So, when passing a 2-dimensional array from Fortran to C, C-Interoperability requires that the array dimension arguments be reversed.
Secondly, according to the C syntax, when a 2-dimensional array is a formal parameter in a function definition, the column size must be explicitly given, [4]. Putting these two facts together implies that for the p2
array in the interface, the column value dimension is xdim_p2
.
The ptrs_p7
array is an output parameter array of pointers to char
without the VALUE
attribute. Thus, as mentioned in the last section, it is interoperable with the formal parameter pointer type, namely char**
. For the same reason, the p8
array interoperates with int*
.
CppWrapper.cpp
The implementation file contains the CppWrapper
library function and a CLI class DoWork
. This class references a C# assembly and contains a public
method which has the responsibility of translating parameters between the CppWrapper
library function and the CSharp Core assembly method.
Therefore, to complete the bridge to C#, the following tasks are necessary:
- Define a variable
work
declared of type CppWrapper::DoWork
. - Call
work
's public
method to translate native parameters to their managed counterparts. - Call a CSharp
Core
library method with managed calling parameters. - Translate managed output parameters back to their native counterparts.
An input integer parameter such as p1
is handled in a straightforward way:
Int32 _p1 = (Int32)p1;
For the multidimensional array p2
, a managed array is created to store its elements:
array<double, 2>^ _p2 = gcnew array<double, 2>(ParameterSize::xdim_p2, ParameterSize::ydim_p2);
The first argument of the template syntax defines the array type and the second argument its dimension. The gcnew
operator creates a managed object on the CLI heap and returns a handle to that object. A handle, denoted by ^
, is a reference to an object on the CLI managed heap. For details, see [5].
Using the following code the 2-dimensional array, _p2
, is filled:
for (int j = 0; j < ParameterSize::ydim_p2; j++)
for (int i = 0; i < ParameterSize::xdim_p2; i++)
_p2[i, j] = *(p2[0, 0] + i + j*ParameterSize::xdim_p2);
As previously stated, multidimensional arrays in Fortran are stored contiguously in memory in column-major order. Since this implies the row index varies fastest, a row-wise filling of the array is necessary.
For the output double precision array p3
, a managed array must be created to pass to the CLI class method:
array<double>^ _p3 = gcnew array<double>(ParameterSize::num_p3);
To pass an output integer pointer parameter like p5
, it is casted to the managed IntPtr
:
IntPtr ptr_p5 = (IntPtr)p5;
Next, consider the ptrs_p7
array. To translate this array to its C++/CLI counterpart, the following two steps are necessary:
- Create the managed array object:
Int32 _nos_p7 = nos_p7;
array<String^>^ _p7 = gcnew array<String^>(_nos_p7);
- Initialize it with the addresses of the pointers:
for (int i = 0; i < _nos_p7; i++)
{
char* _chars = ptrs_p7[i];
String^ theString = gcnew String(_chars);
_p7[i] = theString;
}
Handling of the p8
array is similar.
Now that all the parameters have been re-worked as managed variables, the CLI library method can be called:
work.CORE_CLI_WRAPPER(_p1, _p2, _p3, _p4, ptr_p5, ptr_p6, _p7, _p8);
The signature of this method is:
public:void CORE_CLI_WRAPPER(
Int32 p1,
array<double, 2>^% p2,
array<double>^% p3,
Int32 p4,
IntPtr ptr_p5,
IntPtr ptr_p6,
array<String^>^% p7,
array<Int32>^% p8)
Here, the %
operator is a tracking reference and behaves like the native reference δ in C++. That is, just as a δ
is obtained by dereferencing a * in C++; a %
is obtained by dereferencing a ^
in CLI. Again, see [5] for details. In the implementation of this method, the C# method is called from the CSharpCore
library:
Methods::CoreMethod(p1, p2, p3, p4, ptr_p5, ptr_p6, p7, p8);
After the call, the output parameters such as p3
or p7
have been set. It is necessary to copy these managed variables back to their unmanged counterparts. This is done with the help of .NET Framework Marshal
class found in the InteropServices
assembly:
using namespace System::Runtime::InteropServices;
The Marshal
class has a series of copy
methods depending on the direction being copied (managed to unmanaged or unmanaged to managed) and on type. The following copy
command copies from managed, _p3
, to unmanaged, p3
, starting at position 0
for a length of _p3->Length
.
Marshal::Copy(_p3, 0, IntPtr(p3), _p3->Length);
To handle copying string
s from managed to unmanaged, as required for the output parameter, ptrs_p7
, two further Marshal
class methods are required: StringToHGlobalAnsi
and FreeHGlobal
, [6]. The StringToHGlobalAnsi
method copies a managed string
into unmanaged memory while converting it to ANSI format. In addition, this method allocates the unmanaged memory needed for the copy
. Since unmanaged memory has been allocated with the call to StringToHGlobalAnsi
, it is necessary to free it by calling FreeHGlobal
.
Int32 ns = 0;
for each (String^ str in _p7)
{
char* chars =
(char*)(Marshal::StringToHGlobalAnsi(str)).ToPointer();
sprintf_s(ptrs_p7[ns++], ParameterSize::len_p7, "%s", chars);
Marshal::FreeHGlobal(System::IntPtr((void*)chars));
}
The array of strings _p7
is set in the CSharpCore
library. The above code writes the unmanaged memory returned from StringToHGlobalAnsi
to the buffers pointed to by the pointers ptrs_p7
. This means that the original members of the output parameter p7
now contain the string
s _p7
.
The CSharpCore
library was created in MS Visual Studio as a C# library. However, since a discussion of this library doesn't necessarily enhance the illustration of the Fortran to C# bridge or Fortran C-Interoperability, it shall be omitted.
Conclusion
The puropse of this article was to demonstate how to bridge from one of the most popular legacy programming languages, Fortran, to one of the most popular and powerful modern programming languages, C#. A Fortran to C# bridge is important because it allows for the implementation of legacy domain logic in a .NET Framework language. In such an environment, best practices, design principles and modern tools of software development are more effable.
References
- https://gcc.gnu.org/wiki/GFortranStandards
- http://stackoverflow.com/questions/9686532/arrays-of-strings-in-fortran-c-bridges-using-iso-c-binding
- https://en.wikipedia.org/wiki/Row-_and_column-major_order
- Al Kelley and Ira Pohl, A Book on C. The Benjamin/Cummings Publishing Co., 1990.
- Nishant Sivakumar, C++/ CLI in Action. Manning Publications Co., 2007.
- https://msdn.microsoft.com/en-us/library/system.runtime.interopservices.marshal(v=vs.110).aspx