Fortran 2003: учим Фортран и С дружить
На языке Фортран написано огромное количество кода, отлаженного и работающего многие годы. Я не собираюсь поднимать вопрос «что лучше — Фортран или С?». У каждого языка есть свои сильные и слабые стороны. Но, учитывая большое распространение языка С, всё более популярными в определенных кругах становятся случаи «гибридных» приложений, когда часть кода пишется (скорее, уже написана) на языке Фортран, а другая — на С. Вот только у этих языков есть определенная специфика, про которую я частично уже говорил, и чтобы написанное нами приложение работало корректно, нужно учитывать много нюансов. Различия в типах данных, соглашениях о вызове (calling convention), именах (naming convention) делают задачу создания mixed language приложения далеко нетривиальной. Хорошо, что в стандарте Fortran 2003 появился целый набор средств, специально разработанный для решения задачи интероперабельности C и Фортрана. Кстати, не помню других языков, которые бы стандартизировали подобную работу — ещё один «плюсик» Фортрану за протянутую «руку дружбы».Что же такое эта интер-опер- и так далее? Под термином «интероперабельность» имеется в виду возможность вызова в коде на языке Фортран функции С и наоборот. Кроме этого, можно использовать глобальные переменные, а так же объявлять локальные переменные, структуры данных и перечисления, имеющие соответствие в С. Основная идея — всё должно работать одинаково как в С, так и в Фортране. Стоит отметить, что под С подразумевается стандарт С99 (ISO/IEC 9899:1999). Кстати, конкретная реализация компилятора Фортран вправе выбирать, с кем из компиляторов C дружить. В случае Intel Fortan, это Microsoft Visual C++ на Windows и gcc на Linux и OS X. А что с Intel C++? Так как он совместим с Visual C++ и gcc — никаких проблем с ним нет (что и следовало ожидать).Фортран реализует поддержку той самой интероперабельности через следующие средства:
Ограничения на типы данных, которые могут быть интероперабельны Конструкция BIND (C) Модуль ISO_C_BINDING Аттрибут VALUE Одна из основных сложностей при разработке «смешанных» приложений состоит в том, что типы в С и Фортране разные: понятия указателей, работа со строками, с указателями на функции и так далее. Да и с базовыми типами не всё так просто. Скажем, в С мы имеем типы от short int до long long int. Они могут иметь, а могу и не иметь аналогов в Фортране.Чтобы всё это работало хорошо, в Фортране и появился модуль, отвечающий за «дружбу» этих языков — ISO_C_BINDING. Что там есть? Набор средств, который позволяет гарантировать, что типы данных в Фортране будут «правильными» для работы с кодом на С.Например, для типа int мы можем использовать тип INTEGER (C_INT) в Фортране, а C_INT определяется в модуле ISO_C_BINDING. В случае с Intel Fortran, длина INTEGER составляет 4 байта, но в других реализациях — не факт. Использование именованной константы гарантирует портируемость.Вот те объекты С, которые могут быть доступны для приложения на Фортране:
Числовые типы: целые, с плавающей точкой, комплексные Логические типы (в Фортране есть типа LOGICAL) Строки Структуры Указатели Массивы Глобальные переменные Функции При этом Фортрановские типы остаются старыми, но мы их модифицируем с помощью интероперабельного параметра KIND и константы из модуля. Это будет служить своего рода «связующим звеном» между типами С и Фортран. Соответствие между типами можно найти в следующей таблице: Тип Фортрана Параметр KIND Тип С INTEGER C_INT intsigned int C_SHORT short intsigned short int C_LONG long intsigned long int C_LONG_LONG long long intsigned long long int C_SIGNED_CHAR signed charunsigned char C_SIZE_T size_t REAL C_FLOAT float C_DOUBLE double C_LONG_DOUBLE long double COMPLEX C_COMPLEX _Complex C_DOUBLE_COMPLEX double _Complex C_LONG_DOUBLE_COMPLEX long double _Complex LOGICAL C_BOOL _Bool CHARACTER C_CHAR char Хочу отметить, что в Фортране не поддерживается тип unsigned int.Ещё одна особенность — логические типы выставляются в true/false по-разному в С и Фортране.Если в С используется 0 для false и любое число, отличное от 0 для true, то в Фортране четные числа — false, нечетные — true.Не забываем использовать опцию -fpscomp logicals (/fpscomp: logicals на Windows), которая меняет правила как в С.Кстати, она подключается неявно, если использовать опцию -standard-semantics (/standard-semantics на Windows) — весьма рекомендуемая опция при работе с стандартом Fortran 2003.Теперь давайте посмотрим, как всё работает на примере. Если мы в Фортране напишем
INTEGER (KIND=C_LONG) :: I То использование KIND=C_LONG гарантирует нам, что у переменной I не возникнет проблем с типом при её использовании в коде C, и там она будет типа long int (в соответствии с табличкой). С встроенными типам всё просто — ищем соответствующую константу для KIND в табличке и дело в шляпе. Кстати, раз уж весь этот функционал доступен в виде модуля, то нам стоит его подключить с помощью ключевого слова USE: USE, INTRINSIC: ISO_C_BINDING Таким образом нам будут доступны все константы для типов из модуля. Чтобы не засорять пространство имен, рекомендуется ограничивать область видимости только теми типами, которые мы реально собираемся использовать, например так: USE, INTRINSIC: ISO_C_BINDING, ONLY C_LONG Кроме самих типов, существует так же конструкция BIND (не является частью модуля — часть стандарта Fortran 2003), который говорит компилятору языка Фортран о том, что соответствующее имя — объект С. Причем делать это можно явно и неявно. Например, у нас имеются такие глобальные переменные в С: int a_int; long b_long; И мы хотим правильно использовать их в нашем коде на Фортране, например, в модуле: MODULE TEST_BINDING USE ISO_C_BINDING ! неявный binding A_INT и a_int INTEGER (C_INT), BIND© :: A_INT ! явный binding B и b_long INTEGER (C_LONG) :: B BIND (C, NAME=' b_long ') :: B END MODULE TEST_BINDING Такая «связка» нужна для объектов. В данном примере мы сможем работать в коде на Фортране с теми самыми глобальными переменными, созданными в С. Если же у нас имеется некоторая функция, например Cfunc (float a1, double a2); То в качестве аргументов можно использовать такие данные, и BIND делать не нужно: REAL (C_FLOAT) :: A1 COMPLEX (C_DOUBLE) :: A2 Одна из проблем Фортрана и С заключалась в различиях при работе со строками. Поэтому для того, чтобы в такую функцию можно было передать строку: void copy (char in[], char out[]); Нужно использовать KIND=C_CHAR и символ окончания строки C_NULL_CHAR (аналог \0 в Фортране): CHARACTER (LEN=10, KIND=C_CHAR) :: DIGIT_STRING = C_CHAR '123456789' // C_NULL_CHAR CHARACTER (KIND=C_CHAR) :: DIGIT_ARR (10) И наша строка из Фортрана будет здорово «дружить» с С — можно смело передавать в функцию! Но функцию тоже нужно как-то связать с её аналогом из С. Делается это в Фортране с помощью интерфейсов: INTERFACE SUBROUTINE COPY (IN, OUT), BIND© USE ISO_C_BINDING CHAR (KIND=C_CHAR), DIMENSION (*) :: IN, OUT END SUBROUTINE COPY END INTERFACE И вот теперь мы можем смело писать CALL COPY (DIGIT_STRING, DIGIT_ARR) Самое интересное — это работа с указателями. Для функции с указателями в качестве аргументов short func (double *a; int *b; int c[10]; void *d) можно использовать следующие переменные в Фортране: REAL (C_DOUBLE) :: A! A соответствует *а, потому как в Фортране аргументы передаются по ссылке INTEGER (C_INT) :: B, V (10) ! B и С соответствуют *b и c[] TYPE (C_PTR), VALUE: D! D соответствует *d, так как тип у указателя void* Кроме того, что для указателей есть свой параметр KIND, имеется ещё и ряд дополнительных возможностей, например есть «нулевой» указатель C_NULL_PTR — аналог null из С. В распоряжении так же специальные функции.
C_F_POINTER ассоциирует указатель Фортрана с объектом С. Синтаксис у данной функции следующий:
CALL C_F_POINTER (CPTR, FPTR [, SHAPE])
TYPE (C_PTR), INTENT (IN) :: CPTR
C_ADDRESS = C_LOC (OBJECT) C_ASSOCIATED проверяет, выставлен наш указатель в null или нет, а так же ассоциирован ли он с объектом С.
Производные типы тоже не остались в стороне. Например, такая структура ctype
typedef struct { int a, b; float c; } ctype; будет работать с типом FTYPE:
TYPE, BIND© :: FTYPE INTEGER (C_INT) :: A, B REAL (C_FLOAT) :: C END TYPE FTYPE Конечно, подробно описать все детали стандарта в рамках поста не получится, да этой цели я и не ставил, но думаю, что пролил свет на то, как всё это дело работает. Ну и заключительный примерчик, приближенный к реальности, который показывает, как модуль ISO_C_BINDING может быть использован для вызова функций как из Фортрана, так и из С.Начнем с примера Фортрана, вызывающего С функцию:
int C_Library_Function (void* sendbuf, int sendcount, int *recvcounts); Итак, создаем интерфейс с нужными параметрами KIND: MODULE FTN_C_2 INTERFACE INTEGER (C_INT) FUNCTION C_LIBRARY_FUNCTION (SENDBUF, SENDCOUNT, RECVCOUNTS) BIND (C, NAME='C_LIBRARY_FUNCTION») USE, INTRINSIC: ISO_C_BINDING IMPLICIT NONE TYPE (C_PTR), VALUE: SENDBUF INTEGER (C_INT), VALUE: SENDCOUNT TYPE (C_PTR), VALUE: RECVCOUNTS END FUNCTION C_LIBRARY_FUNCTION END INTERFACE END MODULE FTN_C_2 А теперь непосредственно вызов функции: USE, INTRINSIC: ISO_C_BINDING, ONLY: C_INT, C_FLOAT, C_LOC USE FTN_C_2 … REAL (C_FLOAT), TARGET: SEND (100) INTEGER (C_INT) :: SENDCOUNT INTEGER (C_INT), ALLOCATABLE, TARGET: RECVCOUNTS (100) … ALLOCATE (RECVCOUNTS (100)) … CALL C_LIBRARY_FUNCTION (C_LOC (SEND), SENDCOUNT, C_LOC (RECVCOUNTS)) … И никаких проблем ни с именами, ни с типами данных.Наоборот, если у нас стоит задача вызова некоторой Фортрановской функции (весьма частая задача), то данный модуль и здесь поможет в решении проблем. Если у нас имеется функция Simulation: SUBROUTINE SIMULATION (ALPHA, BETA, GAMMA, DELTA, ARRAYS) BIND© USE, INTRINSIC: ISO_C_BINDING IMPLICIT NONE INTEGER (C_LONG), VALUE: ALPHA REAL (C_DOUBLE), INTENT (INOUT) :: BETA INTEGER (C_LONG), INTENT (OUT) :: GAMMA REAL (C_DOUBLE), DIMENSION (*), INTENT (IN) :: DELTA TYPE, BIND© :: PASS INTEGER (C_INT) :: LENC, LENF TYPE (C_PTR) :: C, F END TYPE PASS TYPE (PASS), INTENT (INOUT) :: ARRAYS REAL (C_FLOAT), ALLOCATABLE, TARGET, SAVE: ETA (:) REAL (C_FLOAT), POINTER: C_ARRAY (:) … ! ассоциируем C_ARRAY с массивом, выделенным в C CALL C_F_POINTER (ARRAYS%C, C_ARRAY, (/ARRAYS%LENC/)) … ! выделяем память под массив и делаем его доступным в С ARRAYS%LENF = 100 ALLOCATE (ETA (ARRAYS%LENF)) ARRAYS%F = C_LOC (ETA) … END SUBROUTINE SIMULATION Декларируем в С структуру: struct pass {int lenc, lenf; float *c, *f;}; И функцию: void simulation (long alpha, double *beta, long *gamma, double delta[], struct pass *arrays); И можем её смело вызывать: simulation (alpha, &beta, &gamma, delta, &arrays); Вот и всё. Думаю, что использование этой фичи нового стандарта позволит многим разработчикам избежать большого количества проблем, а Фортран и С будут дружны как никогда.