html текст
All interests
  • All interests
  • Design
  • Food
  • Gadgets
  • Humor
  • News
  • Photo
  • Travel
  • Video
Click to see the next recommended page
Like it
Don't like
Add to Favorites

Интероперабельность: Фортран и 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.dll 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). Если у кого-то есть дополнения — милости прошу в комменты.

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

материал с habrahabr.ru

11

      Add

      You can create thematic collections and keep, for instance, all recipes in one place so you will never lose them.

      No images found
      Previous Next 0 / 0
      500
      • Advertisement
      • Animals
      • Architecture
      • Art
      • Auto
      • Aviation
      • Books
      • Cartoons
      • Celebrities
      • Children
      • Culture
      • Design
      • Economics
      • Education
      • Entertainment
      • Fashion
      • Fitness
      • Food
      • Gadgets
      • Games
      • Health
      • History
      • Hobby
      • Humor
      • Interior
      • Moto
      • Movies
      • Music
      • Nature
      • News
      • Photo
      • Pictures
      • Politics
      • Psychology
      • Science
      • Society
      • Sport
      • Technology
      • Travel
      • Video
      • Weapons
      • Web
      • Work
        Submit
        Valid formats are JPG, PNG, GIF.
        Not more than 5 Мb, please.
        30
        surfingbird.ru/site/
        RSS format guidelines
        500
        • Advertisement
        • Animals
        • Architecture
        • Art
        • Auto
        • Aviation
        • Books
        • Cartoons
        • Celebrities
        • Children
        • Culture
        • Design
        • Economics
        • Education
        • Entertainment
        • Fashion
        • Fitness
        • Food
        • Gadgets
        • Games
        • Health
        • History
        • Hobby
        • Humor
        • Interior
        • Moto
        • Movies
        • Music
        • Nature
        • News
        • Photo
        • Pictures
        • Politics
        • Psychology
        • Science
        • Society
        • Sport
        • Technology
        • Travel
        • Video
        • Weapons
        • Web
        • Work

          Submit

          Thank you! Wait for moderation.

          Тебе это не нравится?

          You can block the domain, tag, user or channel, and we'll stop recommend it to you. You can always unblock them in your settings.

          • habrahabr.ru
          • домен habrahabr.ru

          Get a link

          Спасибо, твоя жалоба принята.

          Log on to Surfingbird

          Recover
          Sign up

          or

          Welcome to Surfingbird.com!

          You'll find thousands of interesting pages, photos, and videos inside.
          Join!

          • Personal
            recommendations

          • Stash
            interesting and useful stuff

          • Anywhere,
            anytime

          Do we already know you? Login or restore the password.

          Close

          Add to collection

             

            Facebook

            Ваш профиль на рассмотрении, обновите страницу через несколько секунд

            Facebook

            К сожалению, вы не попадаете под условия акции