Interoperability: Fortran and C #
As you know, there are millions and millions of lines of legacy code in the world. The first place in Legacy, of course, belongs to Kobol, but Fortran also got a lot. Moreover, mainly computing modules.
Not so long ago, they brought me a small program (less than 1000 lines, more than a quarter - comments and blank lines) with the task of "doing something beautiful, for example, graphics and interface". Although the program is small, I didn’t want to redo it - my uncle will carefully run around it and make adjustments for two more months.
The results of the work in the form of several pieces of code and a text car are carefully set out under the cut.
There is a Fortran program that counts something. The task: to correct it minimally, preferably - without getting into the logic of work - and put the input parameters as well as the output of the results in a separate module.
To do this, we need to learn how to do the following things:
We will do the front-end in C # - first of all, because of WPF, well, cross-platform is not necessary.
To begin, prepare the environment.
As a compiler, I used gfortran from the GCC package (you can get it from here ). GNU make is also useful to us (this lies nearby ). You can use anything as the source code editor; I put the eclipse with the Photran plugin.
Installing the plugin on the eclipse is done from standard repositories through the menu item "Help" / "Install New Software ..." from the base Juno repository (enter Photran in the filter).
After installing all the software, you need to register the paths to the gfortran and make binaries in the standard path.
All programs are written in the old Fortran dialect, that is, they require a mandatory indent of 6 spaces at the beginning of each line. Lines are limited to 72 familiarities. The file extension is for. It's not that I'm so old school and hardcore, but what we have is working with that.
With C #, everything is clear - the studio. I worked in VS2010.
To get started, let's collect a simple program on Fortran.
First of all, modules. You can do them, you can not do them. In the test project I used modules, this affected the names of the exported methods. In the combat mission, everything is written in one piece, and there are no modules there. In short, it depends on what came to you as an inheritance.
Secondly, Fortran syntax is such that spaces in it are optional. You can write
Thirdly, the dialects f90 and f95 do not require indentation at the beginning of lines. Here, again, everything depends on what has come to you.
But okay, back to the program. It compiles either from the eclipse (if the makefile is configured correctly), or from the command line. To get started, let's work from the command line:
The launched exe-file will a) require a run-time dll from Fortran, and b) display the string “Hello, world”.
To get an exe that does not require runtime, compilation must be performed with the key
To get the dll, you need to add another key
For now, let's finish with fortran and move on to C #.
Let's create a completely standard console application. Immediately add another class -
The entry point to the procedure is determined using the standard VS-utility
This command gives a long dump in which you can find the lines of interest to us:
You can either search for or
Further it is easier. In the main module Program.cs we make a call:
By launching the console application, you can see our line “Hello, world”, displayed by means of fortran. Of course, we must not forget to drop the test.dll compiled in Fortran into the folder
But all this is uninteresting, interesting - to transfer data there and get something back. To this end, we will carry out the second iteration. Let it be, for example, a procedure that adds the number 1 to the first parameter, and passes the result to the second parameter.
The procedure is simple to disgrace:
In Fortran, the call looks something like this:
Now we need to compile and test this code. In general, you can continue to compile from the console, but we have a makefile. Let's attach it to business.
Since we do exe (for testing) and dll (for the “production version”), it makes sense to first compile into object code, and then build dll / exe from it. To do this, open the makefile in the eclipse and write something in the spirit:
Now we can humanly compile and clean the project using the button from the eclipse. But this requires that the path to make be set in the environment variables.
Next in line is the refinement of our shell in C #. First, we import another method from the dll into the project:
The entry point is determined as before, through
In the main program, we write something like this:
In general, everything, the problem is solved. If it were not for one “but” - again, you need to copy
Total, after compilation and launch, if everything went well, we get a working program of the second version.
Suppose to pass the initial parameters to the called dll module of the written code we will be enough. But often you need to throw a line inside one way or another. There is one ambush, which I did not understand - encodings. Therefore, all my examples are given for the Latin alphabet.
Everything is simple here (well, for hardcore workers):
If we wrote the intra-Fortran method, without dll and other interoperability, then the length could not be transmitted. And since we need to transfer data between modules, we have to work with two variables, a pointer to a string and its length.
Calling a method is also straightforward:
Now you need to call this method from C #. To this end, we finalize
Here one more import parameter is added - used
The call at the same time looks trite, with the exception of verbosity caused by the requirement to pass all parameters by reference (
We came to the most interesting thing - callbacks, or passing methods inside a dll to track what is happening.
To begin with, we will write the actual method that takes a function as a parameter. In Fortran, it looks something like this:
Here we should pay attention to the new section of the
The call of this method is absolutely banal:
As a result
Go to C #. Here we need to do additional work - declare a
After that, you can determine the prototype of the called method
The entry point is traditionally determined from the issuance
Calling this method is also straightforward. You can pass there either the native Fortran method (of the type
So, we already have sufficient tools to redo the code in such a way as to pass a callback inside the method to display the progress of the execution of capacious operations. The only thing we don’t know yet is to pass arrays.
With them a little more complicated than with strings. If for strings it’s enough to write a couple of attributes, then for arrays you have to work a little with pens.
To begin with, we will write the procedure for printing an array, with a small reserve for the future in the form of a string transfer:
An array declaration of
A call from Fortran is also commonplace:
The output is a printed string and an array.
There is
But inside the program you have to work a little and use the assembly
This is due to the fact that a pointer to the array must be passed inside the Fortran program, that is, copying data from the managed area to the unmanaged area is required, and, accordingly, memory allocation in it. In this regard, it makes sense to write shells like this:
The full source codes of all iterations (and a little more bonus in the form of transferring an array to a callback function) are in the repository on the bitpacket (hg). If someone has additions - you are welcome to comment.
Traditionally, I thank everyone who read to the end, because something very much text turned out.
Not so long ago, they brought me a small program (less than 1000 lines, more than a quarter - comments and blank lines) with the task of "doing something beautiful, for example, graphics and interface". Although the program is small, I didn’t want to redo it - my uncle will carefully run around it and make adjustments for two more months.
The results of the work in the form of several pieces of code and a text car are carefully set out under the cut.
Formulation of the problem
There is a Fortran program that counts something. The task: to correct it minimally, preferably - without getting into the logic of work - and put the input parameters as well as the output of the results in a separate module.
To do this, we need to learn how to do the following things:
- compile dll on fortran;
- find exported from dll methods;
- pass parameters of the following types to them:
- atomic (
int
,double
); - strings (
string
); - callbacks (
Action<>
); - arrays (
double[]
);
- atomic (
- call methods from a managed environment (in our case, C #).
We will do the front-end in C # - first of all, because of WPF, well, cross-platform is not necessary.
Environment
To begin, prepare the environment.
As a compiler, I used gfortran from the GCC package (you can get it from here ). GNU make is also useful to us (this lies nearby ). You can use anything as the source code editor; I put the eclipse with the Photran plugin.
Installing the plugin on the eclipse is done from standard repositories through the menu item "Help" / "Install New Software ..." from the base Juno repository (enter Photran in the filter).
After installing all the software, you need to register the paths to the gfortran and make binaries in the standard path.
All programs are written in the old Fortran dialect, that is, they require a mandatory indent of 6 spaces at the beginning of each line. Lines are limited to 72 familiarities. The file extension is for. It's not that I'm so old school and hardcore, but what we have is working with that.
With C #, everything is clear - the studio. I worked in VS2010.
First program
Fortran
To get started, let's collect a simple program on Fortran.
module test
contains
subroutine hello()
print *, "Hello, world"
end subroutine
end module test
program test_main
use test
call hello()
end program
We will not disassemble the details, we are not teaching Fortran here, but I will briefly cover the moments that we will encounter. First of all, modules. You can do them, you can not do them. In the test project I used modules, this affected the names of the exported methods. In the combat mission, everything is written in one piece, and there are no modules there. In short, it depends on what came to you as an inheritance.
Secondly, Fortran syntax is such that spaces in it are optional. You can write
endif
, you can - end if
. It is possible do1i=1,10
, but it is possible humanly - do 1 i = 1, 10
. So this is just a storehouse of mistakes. I searched for half an hour why the line callback()
gave the error "symbol not found _back()
" until he realized what to write call callback()
So be careful. Thirdly, the dialects f90 and f95 do not require indentation at the beginning of lines. Here, again, everything depends on what has come to you.
But okay, back to the program. It compiles either from the eclipse (if the makefile is configured correctly), or from the command line. To get started, let's work from the command line:
> gfortran -o bin\test.exe src\test.for
The launched exe-file will a) require a run-time dll from Fortran, and b) display the string “Hello, world”.
To get an exe that does not require runtime, compilation must be performed with the key
-static
:> gfortran -static -o bin\test.exe src\test.for
To get the dll, you need to add another key
-shared
:> gfortran -static -shared -o bin\test.dll src\test.for
For now, let's finish with fortran and move on to C #.
C #
Let's create a completely standard console application. Immediately add another class -
TestWrapper
and write some code: public class TestWrapper {
[DllImport("test.dll", EntryPoint = "__test_MOD_hello", CallingConvention = CallingConvention.Cdecl)]
public static extern void hello();
}
The entry point to the procedure is determined using the standard VS-utility
dumpbin
:> dumpbin /exports test.dll
This command gives a long dump in which you can find the lines of interest to us:
3 2 000018CC __test_MOD_hello
You can either search for or
grep
, or dump the output dumpbin
to a file, and go through the search for it. The main thing - we saw the symbolic name of the entry point, which can be placed in our call. Further it is easier. In the main module Program.cs we make a call:
static void Main(string[] args) {
TestWrapper.hello();
}
By launching the console application, you can see our line “Hello, world”, displayed by means of fortran. Of course, we must not forget to drop the test.dll compiled in Fortran into the folder
bin/Debug
(or bin/Release
).Atomic parameters
But all this is uninteresting, interesting - to transfer data there and get something back. To this end, we will carry out the second iteration. Let it be, for example, a procedure that adds the number 1 to the first parameter, and passes the result to the second parameter.
Fortran
The procedure is simple to disgrace:
subroutine add_one(inVal, retVal)
integer, intent(in) :: inVal
integer, intent(out) :: retVal
retVal = inVal + 1
end subroutine
In Fortran, the call looks something like this:
integer :: inVal, retVal
inVal = 10
call add_one(inVal, retVal)
print *, inVal, ' + 1 equals ', retVal
Now we need to compile and test this code. In general, you can continue to compile from the console, but we have a makefile. Let's attach it to business.
Since we do exe (for testing) and dll (for the “production version”), it makes sense to first compile into object code, and then build dll / exe from it. To do this, open the makefile in the eclipse and write something in the spirit:
FORTRAN_COMPILER = gfortran
all: src\test.for
$(FORTRAN_COMPILER) -O2 \
-c -o obj\test.obj \
src\test.for
$(FORTRAN_COMPILER) -static \
-o bin\test.exe \
obj\test.obj
$(FORTRAN_COMPILER) -static -shared \
-o bin\test.dll \
obj\test.obj
clean:
del /Q bin\*.* obj\*.* *.mod
Now we can humanly compile and clean the project using the button from the eclipse. But this requires that the path to make be set in the environment variables.
C #
Next in line is the refinement of our shell in C #. First, we import another method from the dll into the project:
[DllImport("test.dll", EntryPoint = "__test_MOD_add_one", CallingConvention = CallingConvention.Cdecl)]
public static extern void add_one(ref int i, out int r);
The entry point is determined as before, through
dumpbin
. Since we have variables, we need to specify a calling convention (in this case cdecl
). Variables are passed by reference, so it is ref
required. If we omit ref
, then when we call, we get AV: “Unhandled exception System.AccessViolationException
:: Attempted to read or write to protected memory. This often indicates that another memory is damaged. ” In the main program, we write something like this:
int inVal = 10;
int outVal;
TestWrapper.add_one(ref inVal, out outVal);
Console.WriteLine("{0} add_one equals {1}", inVal, outVal);
In general, everything, the problem is solved. If it were not for one “but” - again, you need to copy
test.dll
from the Fortran folder. The procedure is mechanical, it would be necessary to automate it. To do this, right-click on the project, “Properties”, select the “Build Events” tab, and write something in the spirit in the “Command line of the event before build” windowmake -C $(SolutionDir)..\Test.for clean
make -C $(SolutionDir)..\Test.for all
copy $(SolutionDir)..\Test.for\bin\test.dll $(TargetDir)\test.dll
Of course, we should substitute our own paths. Total, after compilation and launch, if everything went well, we get a working program of the second version.
Lines
Suppose to pass the initial parameters to the called dll module of the written code we will be enough. But often you need to throw a line inside one way or another. There is one ambush, which I did not understand - encodings. Therefore, all my examples are given for the Latin alphabet.
Fortran
Everything is simple here (well, for hardcore workers):
subroutine progress(text, l)
character*(l), intent(in) :: text
integer, intent(in) :: l
print *, 'progress: ', text
end subroutine
If we wrote the intra-Fortran method, without dll and other interoperability, then the length could not be transmitted. And since we need to transfer data between modules, we have to work with two variables, a pointer to a string and its length.
Calling a method is also straightforward:
character(50) :: strVal
strVal = "hello, world"
call progress(strVal, len(trim(strVal)))
len(trim())
is specified with the aim of trimming spaces at the end (as 50 characters are allocated per line, but only 12 are used).C #
Now you need to call this method from C #. To this end, we finalize
TestWrapper
: [DllImport("test.dll", EntryPoint = "__test_MOD_progress", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
public static extern void progress([MarshalAs(UnmanagedType.LPStr)]string txt, ref int strl);
Here one more import parameter is added - used
CharSet
. There is also an instruction to the compiler to pass the string - MarshalAs
. The call at the same time looks trite, with the exception of verbosity caused by the requirement to pass all parameters by reference (
ref
): var str = "hello from c#";
var strLen = str.Length;
TestWrapper.progress(str, ref strLen);
Callbacks
We came to the most interesting thing - callbacks, or passing methods inside a dll to track what is happening.
Fortran
To begin with, we will write the actual method that takes a function as a parameter. In Fortran, it looks something like this:
subroutine run(fnc, times)
integer, intent(in) :: times
integer :: i
character(20) :: str, temp, cs
interface
subroutine fnc(text, l)
character(l), intent(in) :: text
integer, intent(in) :: l
end subroutine
end interface
temp = 'iter: '
do i = 1, times
write(str, '(i10)') i
call fnc(trim(temp)//trim(str), len(trim(temp)//trim(str)))
end do
end subroutine
end module test
Here we should pay attention to the new section of the
interface
description of the prototype of the transferred method. Fairly verbose, but, in general, nothing new. The call of this method is absolutely banal:
call run(progress, 10)
As a result
progress
, the method written in the previous iteration will be called 10 times .C #
Go to C #. Here we need to do additional work - declare a
TestWrapper
delegate in the class with the correct attribute: [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void Progress(string txt, ref int strl);
After that, you can determine the prototype of the called method
run
: [DllImport("test.dll", EntryPoint = "__test_MOD_run", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
public static extern void run(Progress w, ref int times);
The entry point is traditionally determined from the issuance
dumpbin
; the rest is also familiar to us. Calling this method is also straightforward. You can pass there either the native Fortran method (of the type
TestWrapper.progress
described in the previous iteration) or the C # lambda: int rpt = 5;
TestWrapper.run(TestWrapper.progress, ref rpt);
TestWrapper.run((string _txt, ref int _strl) => {
var inner = _txt.Substring(0, _strl);
Console.WriteLine("Hello from c#: {0}", inner);
}, ref rpt);
So, we already have sufficient tools to redo the code in such a way as to pass a callback inside the method to display the progress of the execution of capacious operations. The only thing we don’t know yet is to pass arrays.
Arrays
With them a little more complicated than with strings. If for strings it’s enough to write a couple of attributes, then for arrays you have to work a little with pens.
Fortran
To begin with, we will write the procedure for printing an array, with a small reserve for the future in the form of a string transfer:
subroutine print_arr(str, strL, arr, arrL)
integer, intent(in) :: strL, arrL
character(strL), intent(in) :: str
real*8, intent(in) :: arr(arrL)
integer :: i
print *, str
do i = 1, arrL
print *, i, " elem: ", arr(i)
end do
end subroutine
An array declaration of
double
(or real
double precision) is added , and also its size is transmitted. A call from Fortran is also commonplace:
character(50) :: strVal
real*8 :: arr(4)
strVal = "hello, world"
arr = (/1.0, 3.14159265, 2.718281828, 8.539734222/)
call print_arr(strVal, len(trim(strVal)), arr, size(arr))
The output is a printed string and an array.
C #
There is
TestWrapper
nothing special: [DllImport("test.dll", EntryPoint = "__test_MOD_print_arr", CallingConvention = CallingConvention.Cdecl)]
public static extern void print_arr(string titles, ref int titlesl, IntPtr values, ref int qnt);
But inside the program you have to work a little and use the assembly
System.Runtime.InteropServices
: var s = "abcd";
var sLen = s.Length;
var arr = new double[] { 1.01, 2.12, 3.23, 4.34 };
var arrLen = arr.Length;
var size = Marshal.SizeOf(arr[0]) * arrLen;
var pntr = Marshal.AllocHGlobal(size);
Marshal.Copy(arr, 0, pntr, arr.Length);
TestWrapper.print_arr(s, ref sLen, pntr, ref arrLen);
This is due to the fact that a pointer to the array must be passed inside the Fortran program, that is, copying data from the managed area to the unmanaged area is required, and, accordingly, memory allocation in it. In this regard, it makes sense to write shells like this:
public static void PrintArr(string _titles, double[] _values) {
var titlesLen = _titles.Length;
var arrLen = _values.Length;
var size = Marshal.SizeOf(_values[0]) * arrLen;
var pntr = Marshal.AllocHGlobal(size);
Marshal.Copy(_values, 0, pntr, _values.Length);
TestWrapper.print_arr(_titles, ref titlesLen, pntr, ref arrLen);
}
Putting it all together
The full source codes of all iterations (and a little more bonus in the form of transferring an array to a callback function) are in the repository on the bitpacket (hg). If someone has additions - you are welcome to comment.
Traditionally, I thank everyone who read to the end, because something very much text turned out.