📜 ⬆️ ⬇️

Interoperability: Fortran and C #

As you know, in the world there are millions and millions of lines of Legacy code. The first place in the Legacy, of course, belongs to Kobol, but also Fortran got a lot. And, 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 “to make something beautiful, for example, graphics and interface”. Although the program is small, but I did not want to redo it - for two more months it will diligently run it in and make adjustments.

The results of the work in the form of several pieces of code and car text carefully set out under the cut.

')

Formulation of the problem


There is a program on FORTRAN that counts something. The task: to correct it as little as possible, preferably - without getting into the logic of work - and to put the input parameters and output the results into a separate module.

To do this, we need to learn how to do the following things:

We will do the front-end on C # - first of all, due to WPF, and cross-platform functionality is not necessary.

Environment


To begin, prepare the environment.

As a compiler, I used gfortran from the GCC package (you can take it from here ). Also GNU make is useful to us (it lies nearby ). Anything can be used as a source editor; I put the Eclipse with the plugin Photran.

Eclipse plug-in is installed from standard repositories via the “Help” / “Install New Software ...” menu item from the Juno base repository (enter Photran in the filter).

After installing all the software, you need to register the paths to the gfortran and make binaries to the standard path.

The programs are all written in the old dialect of Fortran, that is, they require a mandatory indent of 6 spaces at the beginning of each line. Lines are limited to 72 familiarity. File extension - for. Not that I'm so old-school and hardcore, but that is, so we work.

With C #, everything is clear - the studio. I worked in VS2010.

First program


Fortran


To begin with we will collect the simple program on a 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 analyze the details, we are still not learning Fortran, but we will briefly highlight the moments that we will have to face.

First, the modules. They can do, you can not do. In the test project, I used modules, this affected the names of the exported methods. In the combat mission, everything is written sploshnyakom, and there are no modules. In short, it depends on what came to you as an inheritance.

Secondly, the FORTRAN syntax is such that the spaces in it are optional. You can write endif , you can - end if . You can do1i=1,10 , but you can humanly - do 1 i = 1, 10 . So this is just a storehouse of errors. I spent half an hour looking for a line
  callback() 
gave the error “the _back() character was not found” until it realized what to write
  call callback() 
So be careful.

Thirdly, the f90 and f95 dialects do not require padding at the beginning of lines. Here everything again depends on what has come to you.

But okay, back to the program. It is compiled either from eclipse (if the makefile is configured correctly), or from the command line. First, 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".

In order to get an exe that does not require runtime, compilation must be done with the -static key:
 > gfortran -static -o bin\test.exe src\test.for 

To get the same dll, you need to add the -shared key:
 > gfortran -static -shared -o bin\test.dll src\test.for 

On this with the Fortran, let's finish it for now, and move on to C #.

C #


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 dumpbin VS-utility:
 > 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 search or grep ohm, or dump dumpbin output to a file, and go search for it. The main thing - we saw the symbolic name of the entry point, which can be placed in our call.

Further - easier. In the main module of Program.cs we make the call:
  static void Main(string[] args) { TestWrapper.hello(); } 

By running the console application, you can see our line “Hello, world”, displayed by means of Fortran. Of course, we must not forget to throw the test.dll compiled in Fortran into the bin/Debug (or bin/Release ) bin/Release .

Atomic parameters


But all this is not interesting, it is interesting - to transfer the data there and get something back. To this end, we will conduct 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 ugliness:
  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 also have a makefile. Let's get it attached to the case.

Since we are doing exe (for testing) and dll (for the “production option”), it makes sense to first compile into object code, then assemble dll / exe from it. To do this, open the makefile in the eclipse and write something in the spirit of:
 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 clear the project using the eclipse button. But this requires that the path to make be set in 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 defined as before, through dumpbin . Since we have variables, we need to specify a calling cdecl (in this case, cdecl ). Variables are passed by reference, so ref required. If we omit the ref , then when we call, we get AV: “Unhandled exception: System.AccessViolationException : Attempt to read or write to protected memory. This often indicates that the other memory is damaged. ”

In the main program we write about the following:
  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 “Construction Events” tab, and write something in the spirit in the “Command line of the event before construction” window
 make -C $(SolutionDir)..\Test.for clean make -C $(SolutionDir)..\Test.for all copy $(SolutionDir)..\Test.for\bin\test.dll $(TargetDir)\test.dll 
Ways, of course, it would be necessary to substitute.

So, after compiling and running, if everything went well, we get a working program of the second version.

Strings


Let us assume that to transfer the initial parameters to the called dll-module of the written code, we will be satisfied. But it is often necessary to throw a string in one way or another. There is one ambush with which I did not understand - encodings. Therefore all my examples are given for Latin.

Fortran


It's simple (well, for hardcore):
  subroutine progress(text, l) character*(l), intent(in) :: text integer, intent(in) :: l print *, 'progress: ', text end subroutine 

If we wrote an 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 will have to work with two variables, a pointer to a string and its length.

Calling the method is also not difficult:
  character(50) :: strVal strVal = "hello, world" call progress(strVal, len(trim(strVal))) 

len(trim()) is specified for the purpose of truncating spaces at the end (as 50 characters are allocated per line, and 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); 

This is where another import parameter is added - the CharSet used. Also there is an instruction to the compiler on line passing - MarshalAs .

The call looks trite, with the exception of the 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); 

Kolbeki


We come to the most interesting thing - callbacks, or passing methods inside 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 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 describing the prototype of the transmitted method. Pretty verbose, but, in general, nothing new.

The call of this method is absolutely banal:
  call run(progress, 10) 

As a result, the progress method written on the previous iteration will be called 10 times.

C #


Moving to C #. Here we need to do more work - declare a delegate with the correct attribute in the TestWrapper class:
  [UnmanagedFunctionPointer(CallingConvention.Cdecl)] public delegate void Progress(string txt, ref int strl); 

After that, you can define a prototype of the called run method:
  [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 dumpbin issue; the rest is familiar to us too.

Calling this method is also not difficult. You can send there both a native Fortran method (such as TestWrapper.progress , described at the last iteration), and 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 remake the code in such a way as to pass into the callback method to display the progress of the execution of capacious operations. The only thing we do not know how to do is pass the arrays.

Arrays


With them a little harder than with strings. If for strings it is enough to write a couple of attributes, then for arrays you will have to work a little with pens.

Fortran


To begin with, we will write the procedure for printing an array, with a small margin for the future in the form of a line feed:
  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 from double (or real double precision) is added, and its size is also passed.
The call from FORTRAN is also trivial:
  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)) 

At the output we get a printed string and an array.

C #


There TestWrapper nothing special about TestWrapper :
  [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 will have to work a little and use the System.Runtime.InteropServices assembly:
  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 an array must be passed into the Fortran program, that is, it is necessary to copy data from the managed area to the unmanaged, and, accordingly, memory allocation in it. In this regard, it makes sense to write shells of the following type:
  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 complete 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 bit package (hg). If someone has additions - welcome in comments.

Traditionally, I thank everyone who read to the end, for something very much text came out.

Source: https://habr.com/ru/post/178717/


All Articles