На языке Фортран написано огромное количество кода, отлаженного и работающего многие годы. Я не собираюсь поднимать вопрос «что лучше — Фортран или С?». У каждого языка есть свои сильные и слабые стороны. Но, учитывая большое распространение языка С, всё более популярными в определенных кругах становятся случаи «гибридных» приложений, когда часть кода пишется (скорее, уже написана) на языке Фортран, а другая – на С. Вот только у этих языков есть определенная специфика, про которую я частично уже говорил, и чтобы написанное нами приложение работало корректно, нужно учитывать много нюансов. Различия в типах данных, соглашениях о вызове (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 (аналог \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(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);


Вот и всё. Думаю, что использование этой фичи нового стандарта позволит многим разработчикам избежать большого количества проблем, а Фортран и С будут дружны как никогда.

Комментарии (4)


  1. BalinTomsk
    16.04.2015 18:23
    +1

    Самое главное автор забыл — матрицы на C и Fortran по разному адресуются.


  1. ivorobts Автор
    16.04.2015 18:25

    Не забыл, я об этом в предыдущем посте писал, здесь на него ссылку дал. Но спасибо за бдительность!


  1. icc
    17.04.2015 01:23

    Можно было бы добавить пример, как из dll вызывать сишные функции, а из объектных файлов вызывать фортрановские. Хотя код тоже несложнее будет, чем в статье.


  1. DancingOnWater
    17.04.2015 09:59

    Тоже регулярно порываюсь подружить фортрнаовский код с сишным, но также регулярно нахожу адекватную замену, на на C\C++.

    Сейчас связка boost+eigen+gsl покрывает все мои потребности