На языке Фортран написано огромное количество кода, отлаженного и работающего многие годы. Я не собираюсь поднимать вопрос «что лучше — Фортран или С?». У каждого языка есть свои сильные и слабые стороны. Но, учитывая большое распространение языка С, всё более популярными в определенных кругах становятся случаи «гибридных» приложений, когда часть кода пишется (скорее, уже написана) на языке Фортран, а другая – на С. Вот только у этих языков есть определенная специфика, про которую я частично уже говорил, и чтобы написанное нами приложение работало корректно, нужно учитывать много нюансов. Различия в типах данных, соглашениях о вызове (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 | int signed int |
C_SHORT | short int signed short int |
|
C_LONG | long int signed long int |
|
C_LONG_LONG | long long int signed long long int |
|
C_SIGNED_CHAR | signed char unsigned 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(C) :: 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 (аналог в Фортране):
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(C)
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
<type_spec>, POINTER, INTENT(OUT) :: FPTR
INTEGER, INTENT(IN), OPTIONAL :: SHAPE
В качестве входного аргумента CPTR передаем указатель на объект в С, на выходе имеем Фортрановский указатель FPTR на этот объект.
C_LOC возвращает адрес объекта С или Фортран:
C_ADDRESS = C_LOC(OBJECT)
C_ASSOCIATED проверяет, выставлен наш указатель в null или нет, а так же ассоциирован ли он с объектом С.
Производные типы тоже не остались в стороне. Например, такая структура ctype
typedef struct
{
int a, b;
float c;
} ctype;
будет работать с типом FTYPE:
TYPE, BIND(C) :: 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(C)
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(C) :: 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);
Вот и всё. Думаю, что использование этой фичи нового стандарта позволит многим разработчикам избежать большого количества проблем, а Фортран и С будут дружны как никогда.
Автор: ivorobts