Click here to Skip to main content
15,116,654 members
Articles / Web Development / HTML
Article
Posted 21 Feb 2017

Stats

17.1K views
253 downloads
9 bookmarked

A Bridge From Fortran to C#

Rate me:
Please Sign up or sign in to vote.
4.94/5 (16 votes)
21 Feb 2017CPOL10 min read
This article discusses bridging from Fortran to the .NET language C#.

Image 1

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:

  1. satisfy the legacy interface and
  2. 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:

  1. Client – a Visual Fortran Console Application representing the client program
  2. Core – a Visual Fortran Dynamic-link Library satisfying the legacy interface to the client program
  3. CppWrapper – a Visual C++/CLR Class Library bridging from native C++ to managed C#
  4. 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:

FORTRAN
!! import the CORE DLL
!DEC$ ATTRIBUTES DLLIMPORT :: CORE_METHOD

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.

FORTRAN
!! call method from CORE DLL
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:

FORTRAN
!! variable declarations
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:

FORTRAN
!! export the CORE DLL
!DEC$ ATTRIBUTES DLLEXPORT :: CORE_METHOD

To enable C-Interopability, the ISO_C_BINDING module, which supports Fortran’s interoperability with C by exposing native C types, must be imported:

FORTRAN
!! import package
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:

FORTRAN
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:

  1. nos_p7 - an input integer containing the size of the p7 array
  2. ptrs_p7 - an array of pointers the same size of the p7 array

That is, to pass p7 (what is essentially an array of strings), 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:

FORTRAN
!! to pass an array of strings: assign to array of pointers and pass the pointer array
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:

FORTRAN
!! call to FORTRAN wrapper
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.

C++
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:

22 6 40
33 7 50

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:

22 33
6 7
40 50

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:

  1. Define a variable work declared of type CppWrapper::DoWork.
  2. Call work's public method to translate native parameters to their managed counterparts.
  3. Call a CSharp Core library method with managed calling parameters.
  4. Translate managed output parameters back to their native counterparts.

An input integer parameter such as p1 is handled in a straightforward way:

// p1(in)
Int32 _p1 = (Int32)p1;

For the multidimensional array p2, a managed array is created to store its elements:

C++
// p2(in) - store in managed array
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:

C++
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:

C++
// p3(out)
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:

C++
// p5(out)
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:

  1. Create the managed array object:
    C++
    // p7(out) - set pointer array
    Int32 _nos_p7 = nos_p7;
    array<String^>^ _p7 = gcnew array<String^>(_nos_p7);
    
  2. Initialize it with the addresses of the pointers:
    C++
    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:

C++
// call
work.CORE_CLI_WRAPPER(_p1, _p2, _p3, _p4, ptr_p5, ptr_p6, _p7, _p8);

The signature of this method is:

C++
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:

C#
// CSharpCore: static call
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:

C#
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.

C#
// p3(out) - copy from managed to unmanaged
Marshal::Copy(_p3, 0, IntPtr(p3), _p3->Length);

To handle copying strings 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.

C#
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 strings _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

  1. https://gcc.gnu.org/wiki/GFortranStandards
  2. http://stackoverflow.com/questions/9686532/arrays-of-strings-in-fortran-c-bridges-using-iso-c-binding
  3. https://en.wikipedia.org/wiki/Row-_and_column-major_order
  4. Al Kelley and Ira Pohl, A Book on C. The Benjamin/Cummings Publishing Co., 1990.
  5. Nishant Sivakumar, C++/ CLI in Action. Manning Publications Co., 2007.
  6. https://msdn.microsoft.com/en-us/library/system.runtime.interopservices.marshal(v=vs.110).aspx

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Member 1895422
davidgore
Germany Germany
No Biography provided

Comments and Discussions

 
GeneralMy vote of 5 Pin
KarstenK5-Mar-17 23:35
mveKarstenK5-Mar-17 23:35 
QuestionMy vote 5 Pin
Gwamoniak23-Feb-17 4:04
MemberGwamoniak23-Feb-17 4:04 
QuestionLegacy Pin
D. Infuehr22-Feb-17 5:24
MemberD. Infuehr22-Feb-17 5:24 
GeneralNice article Pin
Daniel Pfeffer22-Feb-17 1:02
professionalDaniel Pfeffer22-Feb-17 1:02 
GeneralRe: Nice article Pin
D. Infuehr22-Feb-17 5:30
MemberD. Infuehr22-Feb-17 5:30 
Daniel Pfeffer. German Name, Israeli origin.
Your parents were germans or lived in austria before 1938?
GeneralRe: Nice article Pin
Daniel Pfeffer22-Feb-17 5:41
professionalDaniel Pfeffer22-Feb-17 5:41 
GeneralMy vote of 5 Pin
MargauxVillain21-Feb-17 23:35
MemberMargauxVillain21-Feb-17 23:35 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.