Интероперабельность: Фортран и C#

от автора

Как известно, в мире миллионы и миллионы строк легаси-кода. Первое место в легаси, разумеется, принадлежит Коболу, но и на долю Фортрана досталось немало. Причём, в основном, вычислительных модулей.

Не так давно мне принесли небольшую программку (менее 1000 строк, более четверти — комментарии и пустые строки) с задачей «сделать что-нибудь красивое, например, графики и интерфейс». Хоть программа и небольшая, а переделывать её не хотелось — дядька её ещё два месяца будет старательно обкатывать и вносить коррективы.

Результаты работы в виде нескольких кусков кода и вагона текста старательно изложены под катом.

Постановка задачи

Есть программа на фортране, которая что-то считает. Задача: минимально её скорректировать, желательно — не залезая в логику работы, — и вынести в отдельный модуль задание входных параметров, а также вывод результатов.

Для этого нам потребуется научиться делать следующие вещи:

  • компилировать dll на фортране;
  • находить экспортируемые из dll методы;
  • передавать в них параметры следующих типов:
    • атомарные (int, double);
    • строки (string);
    • колбэки (Action<>);
    • массивы (double[]);
  • вызывать методы из управляемого окружения (в нашем случае — C#).

Фронт-энд будем делать на C# — в первую очередь, по причине WPF, ну и кроссплатформенности не надо.

Окружение

Для начала подготовим окружение.

В качестве компилятора я использовал gfortran из пакета GCC (взять можно отсюда). Также нам пригодится GNU make (это лежит неподалёку). В качестве редактора исходного кода можно использовать что угодно; я поставил эклипс с плагином Photran.

Установка плагина на эклипс производится из стандартных репозиториев через пункт меню «Help»/«Install New Software…» из базового репозитория Juno (в фильтре ввести Photran).

После установки всего софта требуется прописать пути к бинарникам gfortran и make в стандартный path.

Программы все написаны на старом диалекте фортрана, то есть требуют обязательный отступ в 6 пробелов в начале каждой строки. Строки ограничены 72 знакоместами. Расширение файла — for. Не то чтобы я настолько олдскулен и хардкорен, но что есть, с тем и работаем.

С C# всё понятно — студия. Я работал в VS2010.

Первая программа

Фортран

Для начала соберём простую программу на фортране.

      module test       contains         subroutine hello()           print *, "Hello, world"         end subroutine       end module test        program test_main         use test         call hello()       end program 

Деталей разбирать не будем, мы тут не фортран всё-таки учим, но кратко освещу моменты, с которыми нам придётся столкнуться.

Во-первых, модули. Их можно делать, можно не делать. В тестовом проекте я использовал модули, это сказалось на именах экспортируемых методов. В боевой задаче всё написано сплошняком, и там модулей нет. Короче, зависит от того, что вам пришло в виде наследства.

Во-вторых, синтаксис фортрана таков, что пробелы в нём необязательны. Можно писать endif, можно — end if. Можно do1i=1,10, а можно по-человечески — do 1 i = 1, 10. Так что это просто кладезь ошибок. Я полчаса искал, почему строчка

        callback() 

давала ошибку «не найден символ _back()», пока не сообразил, что надо написать

        call callback() 

Так что будьте внимательны.

В-третьих, диалекты f90 и f95 не требуют отступов в начале строк. Тут всё опять-таки зависит от того, что к вам пришло.

Но ладно, вернёмся к программе. Компилируется она или из эклипса (если правильно настроен makefile), или из командной строки. Для начала поработаем из командной строки:

> gfortran -o bin\test.exe src\test.for 

Запущенный exe-файл будет а) требовать run-time dll от фортрана, и б) выводить строку «Hello, world».

Чтобы получился exe, не требующий рантайма, компиляцию надо проводить с ключом -static:

> gfortran -static -o bin\test.exe src\test.for 

Для получения же dll требуется добавить ещё ключик -shared:

> gfortran -static -shared -o bin\test.exe src\test.for 

На этом с фортраном пока что закончим, и перейдём в C#.

C#

Создадим полностью стандартное консольное приложение. Сразу добавим ещё один класс — TestWrapper и напишем немного кода:

    public class TestWrapper {         [DllImport("test.dll", EntryPoint = "__test_MOD_hello", CallingConvention = CallingConvention.Cdecl)]         public static extern void hello();     } 

Входная точка в процедуру определяется при помощи стандартной VS-утилиты dumpbin:

> dumpbin /exports test.dll 

Эта команда даёт длинный дамп, в котором можно найти интересующие нас строчки:

          3    2 000018CC __test_MOD_hello 

Искать можно или grep-ом, или сбросить вывод dumpbin в файл, и пройтись поиском по нему. Главное — мы увидели символьное название точки входа, которое можно поместить в наш вызов.

Дальше — проще. В основном модуле Program.cs делаем вызов:

        static void Main(string[] args) {             TestWrapper.hello();         } 

Запустив консольное приложение, можно видеть нашу строчку «Hello, world», выводимую средствами фортрана. Разумеется, надо не забыть подкинуть скомпилированный в фортране test.dll в папку bin/Debug (или bin/Release).

Атомарные параметры

Но это всё неинтересно, интересно — передать данные туда и получить что-то обратно. С этой целью проведём вторую итерацию. Пусть это будет, например, процедура, добавляющая число 1 к первому параметру, и передающая результат во второй параметр.

Фортран

Процедура проста до безобразия:

        subroutine add_one(inVal, retVal)           integer, intent(in) :: inVal           integer, intent(out) :: retVal            retVal = inVal + 1         end subroutine 

В фортране вызов выглядит как-то так:

        integer :: inVal, retVal          inVal = 10         call add_one(inVal, retVal)         print *, inVal, ' + 1 equals ', retVal 

Теперь нам надо данный код скомпилировать и протестировать. В общем-то можно так и продолжать компилировать из консоли, но у нас же есть makefile. Давайте его пристроим к делу.

Так как мы делаем exe (для тестирования) и dll (для «продакшн-варианта»), то имеет смысл сначала компилировать в объектный код, после чего из него собирать dll/exe. Для этого открываем в эклипсе makefile и пишем что-то в духе:

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 

Теперь мы можем по-человечески компилировать и очищать проект по кнопке из эклипса. Но для этого требуется, чтобы путь к make был установлен в переменных окружения.

C#

Следующее на очереди — доработка нашей оболочки в C#. Для начала импортируем ещё один метод из dll в проект:

        [DllImport("test.dll", EntryPoint = "__test_MOD_add_one", CallingConvention = CallingConvention.Cdecl)]         public static extern void add_one(ref int i, out int r); 

Точку входа определяем как и раньше, через dumpbin. Так как у нас появляются переменные, требуется указать соглашение по вызову (в данном случае cdecl). Переменные передаются по ссылке, так что ref обязателен. Если опустить ref, то при вызове получим AV: «Необработанное исключение: System.AccessViolationException: Попытка чтения или записи в защищенную память. Это часто свидетельствует о том, что другая память повреждена.»

В основной программе пишем примерно следующее:

            int inVal = 10;             int outVal;             TestWrapper.add_one(ref inVal, out outVal);             Console.WriteLine("{0} add_one equals {1}", inVal, outVal); 

В общем-то всё, задача решена. Если бы не одно «но» — опять требуется копировать test.dll из папки фортрана. Процедура механическая, надо бы её автоматизировать. Для этого нажимаем правой кнопкой на проект, «Свойства», выбираем вкладку «События построения», и пишем в окне «Командная строка события перед построением» что-то в духе

make -C $(SolutionDir)..\Test.for clean make -C $(SolutionDir)..\Test.for all copy $(SolutionDir)..\Test.for\bin\test.dll $(TargetDir)\test.dll 

Пути, понятное дело, надо бы свои подставить.

Итого, после компиляции и запуска, если всё прошло нормально, получаем работающую программу второй версии.

Строки

Положим, для передачи начальных параметров в вызываемый dll-модуль написанного кода нам будет довольно. Но зачастую требуется так или иначе закинуть внутрь строку. Тут есть одна засада, с которой я не разбирался — кодировки. Потому все мои примеры приведены для латиницы.

Фортран

Тут всё просто (ну, для хардкорщиков):

        subroutine progress(text, l)           character*(l), intent(in) :: text           integer, intent(in) :: l            print *, 'progress: ', text         end subroutine 

Если бы мы писали внутрифортрановский метод, без dll и прочей интероперабельности, то длину можно было бы и не передавать. А так как нам надо передавать данные между модулями, придётся работать с двумя переменными, указателем на строку и её длиной.

Вызов метода тоже не составляет сложностей:

        character(50) :: strVal         strVal = "hello, world"         call progress(strVal, len(trim(strVal))) 

len(trim()) указан с целью обрезания пробелов в конце (т.к. выделено на строку 50 символов, а используется только 12).

C#

Теперь надо вызвать этот метод из C#. С этой целью доработаем 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); 

Здесь добавляется ещё один параметр импорта — используемый CharSet. Также появляется указание компилятору по передаче строки — MarshalAs.

Вызов при этом выглядит банально, за исключением многословности, вызванной требованием все параметры передавать по ссылке (ref):

            var str = "hello from c#";             var strLen = str.Length;             TestWrapper.progress(str, ref strLen); 

Колбэки

Мы подошли к самому интересному — колбэкам, или передаче методов внутрь dll для отслеживания происходящего.

Фортран

Для начала напишем собственно метод, принимающий функцию как параметр. В фортране это выглядит примерно так:

        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 

Тут нам следует обратить внимание на новую секцию interface описания прототипа передаваемого метода. Изрядно многословно, но, в общем-то, ничего нового.

Вызов же данного метода абсолютно банален:

        call run(progress, 10) 

В результате 10 раз будет вызван метод progress, написанный на предыдущей итерации.

C#

Переходим в C#. Тут нам требуется провести дополнительную работу — объявить в классе TestWrapper делегат с правильным атрибутом:

        [UnmanagedFunctionPointer(CallingConvention.Cdecl)]         public delegate void Progress(string txt, ref int strl); 

После этого можно определить прототип вызываемого метода run:

        [DllImport("test.dll", EntryPoint = "__test_MOD_run", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]         public static extern void run(Progress w, ref int times); 

Точку входа традиционно определяем из выдачи dumpbin; остальное нам тоже знакомо.

Вызов этого метода тоже не составляет затруднений. Передавать туда можно как нативный фортрановский метод (типа TestWrapper.progress, описанного на прошлой итерации), так и лямбду C#:

            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); 

Итак, мы уже обладаем достаточным инструментарием для того, чтобы переделать код таким образом, чтобы передавать внутрь метода колбэк для вывода прогресса исполнения ёмких операций. Единственное, чего мы пока не умеем — передавать массивы.

Массивы

С ними чуть сложнее, чем со строками. Если для строк достаточно написать пару атрибутов, то для массивов придётся поработать немного ручками.

Фортран

Для начала напишем процедуру печати массива, с небольшим заделом на будущее в виде передачи строки:

        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 

Добавляется объявление массива из double (или real двойной точности), а также передаём его размер.
Вызов из фортрана тоже банален:

        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)) 

На выходе получаем отпечатанную строку и массив.

C#

В 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); 

А вот внутри программы придётся немного поработать и задействовать сборку 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); 

Это связано с тем, что внутрь фортрановской программы должен передаваться указатель на массив, то есть требуется копирование данных из управляемой области в неуправляемую, и, соответственно, выделение памяти в ней. В связи с этим имеет смысл написание оболочек типа такой:

        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);         } 

Собираем всё вместе

Полные исходные коды всех итераций (и ещё немного бонуса в виде передачи массива в колбэк-функцию) лежат в репозитории на битбакете (hg). Если у кого-то есть дополнения — милости прошу в комменты.

Традиционно благодарю всех, кто дочитал до конца, ибо что-то очень уж много текста получилось.

ссылка на оригинал статьи http://habrahabr.ru/post/178717/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *