Не так давно мне принесли небольшую программку (менее 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/
Добавить комментарий