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.


    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[]);
    • 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 - TestWrapperand 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 dumpbinto 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 refrequired. 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.dllfrom 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” window
    make -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 interfacedescription 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 TestWrapperdelegate 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.progressdescribed 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 realdouble 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 TestWrappernothing 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.

    Also popular now: