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:
- compile dll on FORTRAN;
- find exported from dll methods;
- pass in the parameters of the following types:
- atomic (
int
, double
); - strings;
- callbacks (
Action<>
); - arrays (
double[]
);
- call methods from a managed environment (in our case, C #).
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.