Как известно, в мире миллионы и миллионы строк легаси-кода. Первое место в легаси, разумеется, принадлежит Коболу, но и на долю Фортрана досталось немало. Причём, в основном, вычислительных модулей.
Не так давно мне принесли небольшую программку (менее 1000 строк, более четверти — комментарии и пустые строки) с задачей «сделать что-нибудь красивое, например, графики и интерфейс». Хоть программа и небольшая, а переделывать её не хотелось — дядька её ещё два месяца будет старательно обкатывать и вносить коррективы.
Результаты работы в виде нескольких кусков кода и вагона текста старательно изложены под катом.
Есть программа на фортране, которая что-то считает. Задача: минимально её скорректировать, желательно — не залезая в логику работы, — и вынести в отдельный модуль задание входных параметров, а также вывод результатов.
Для этого нам потребуется научиться делать следующие вещи:
Фронт-энд будем делать на C# — в первую очередь, по причине WPF, ну и кроссплатформенности не надо.
Для начала подготовим окружение.
В качестве компилятора я использовал gfortran из пакета GCC (взять можно отсюда). Также нам пригодится GNU make (это лежит неподалёку). В качестве редактора исходного кода можно использовать что угодно; я поставил эклипс с плагином Photran.
Установка плагина на эклипс производится из стандартных репозиториев через пункт меню «Help»/«Install New Software...» из базового репозитория Juno (в фильтре ввести Photran).
После установки всего софта требуется прописать пути к бинарникам gfortran и make в стандартный path.
Программы все написаны на старом диалекте фортрана, то есть требуют обязательный отступ в 6 пробелов в начале каждой строки. Строки ограничены 72 знакоместами. Расширение файла — for. Не то чтобы я настолько олдскулен и хардкорен, но что есть, с тем и работаем.
С C# всё понятно — студия. Я работал в VS2010.
Для начала соберём простую программу на фортране.
Во-первых, модули. Их можно делать, можно не делать. В тестовом проекте я использовал модули, это сказалось на именах экспортируемых методов. В боевой задаче всё написано сплошняком, и там модулей нет. Короче, зависит от того, что вам пришло в виде наследства.
Во-вторых, синтаксис фортрана таков, что пробелы в нём необязательны. Можно писать
В-третьих, диалекты f90 и f95 не требуют отступов в начале строк. Тут всё опять-таки зависит от того, что к вам пришло.
Но ладно, вернёмся к программе. Компилируется она или из эклипса (если правильно настроен makefile), или из командной строки. Для начала поработаем из командной строки:
Запущенный exe-файл будет а) требовать run-time dll от фортрана, и б) выводить строку «Hello, world».
Чтобы получился exe, не требующий рантайма, компиляцию надо проводить с ключом
Для получения же dll требуется добавить ещё ключик
На этом с фортраном пока что закончим, и перейдём в C#.
Создадим полностью стандартное консольное приложение. Сразу добавим ещё один класс —
Входная точка в процедуру определяется при помощи стандартной VS-утилиты
Эта команда даёт длинный дамп, в котором можно найти интересующие нас строчки:
Искать можно или
Дальше — проще. В основном модуле Program.cs делаем вызов:
Запустив консольное приложение, можно видеть нашу строчку «Hello, world», выводимую средствами фортрана. Разумеется, надо не забыть подкинуть скомпилированный в фортране test.dll в папку
Но это всё неинтересно, интересно — передать данные туда и получить что-то обратно. С этой целью проведём вторую итерацию. Пусть это будет, например, процедура, добавляющая число 1 к первому параметру, и передающая результат во второй параметр.
Процедура проста до безобразия:
В фортране вызов выглядит как-то так:
Теперь нам надо данный код скомпилировать и протестировать. В общем-то можно так и продолжать компилировать из консоли, но у нас же есть makefile. Давайте его пристроим к делу.
Так как мы делаем exe (для тестирования) и dll (для «продакшн-варианта»), то имеет смысл сначала компилировать в объектный код, после чего из него собирать dll/exe. Для этого открываем в эклипсе makefile и пишем что-то в духе:
Теперь мы можем по-человечески компилировать и очищать проект по кнопке из эклипса. Но для этого требуется, чтобы путь к make был установлен в переменных окружения.
Следующее на очереди — доработка нашей оболочки в C#. Для начала импортируем ещё один метод из dll в проект:
Точку входа определяем как и раньше, через
В основной программе пишем примерно следующее:
В общем-то всё, задача решена. Если бы не одно «но» — опять требуется копировать
Итого, после компиляции и запуска, если всё прошло нормально, получаем работающую программу второй версии.
Положим, для передачи начальных параметров в вызываемый dll-модуль написанного кода нам будет довольно. Но зачастую требуется так или иначе закинуть внутрь строку. Тут есть одна засада, с которой я не разбирался — кодировки. Потому все мои примеры приведены для латиницы.
Тут всё просто (ну, для хардкорщиков):
Если бы мы писали внутрифортрановский метод, без dll и прочей интероперабельности, то длину можно было бы и не передавать. А так как нам надо передавать данные между модулями, придётся работать с двумя переменными, указателем на строку и её длиной.
Вызов метода тоже не составляет сложностей:
Теперь надо вызвать этот метод из C#. С этой целью доработаем
Здесь добавляется ещё один параметр импорта — используемый
Вызов при этом выглядит банально, за исключением многословности, вызванной требованием все параметры передавать по ссылке (
Мы подошли к самому интересному — колбэкам, или передаче методов внутрь dll для отслеживания происходящего.
Для начала напишем собственно метод, принимающий функцию как параметр. В фортране это выглядит примерно так:
Тут нам следует обратить внимание на новую секцию
Вызов же данного метода абсолютно банален:
В результате 10 раз будет вызван метод
Переходим в C#. Тут нам требуется провести дополнительную работу — объявить в классе
После этого можно определить прототип вызываемого метода
Точку входа традиционно определяем из выдачи
Вызов этого метода тоже не составляет затруднений. Передавать туда можно как нативный фортрановский метод (типа
Итак, мы уже обладаем достаточным инструментарием для того, чтобы переделать код таким образом, чтобы передавать внутрь метода колбэк для вывода прогресса исполнения ёмких операций. Единственное, чего мы пока не умеем — передавать массивы.
С ними чуть сложнее, чем со строками. Если для строк достаточно написать пару атрибутов, то для массивов придётся поработать немного ручками.
Для начала напишем процедуру печати массива, с небольшим заделом на будущее в виде передачи строки:
Добавляется объявление массива из
Вызов из фортрана тоже банален:
На выходе получаем отпечатанную строку и массив.
В
А вот внутри программы придётся немного поработать и задействовать сборку
Это связано с тем, что внутрь фортрановской программы должен передаваться указатель на массив, то есть требуется копирование данных из управляемой области в неуправляемую, и, соответственно, выделение памяти в ней. В связи с этим имеет смысл написание оболочек типа такой:
Полные исходные коды всех итераций (и ещё немного бонуса в виде передачи массива в колбэк-функцию) лежат в репозитории на битбакете (hg). Если у кого-то есть дополнения — милости прошу в комменты.
Традиционно благодарю всех, кто дочитал до конца, ибо что-то очень уж много текста получилось.
Не так давно мне принесли небольшую программку (менее 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). Если у кого-то есть дополнения — милости прошу в комменты.
Традиционно благодарю всех, кто дочитал до конца, ибо что-то очень уж много текста получилось.