Интероперабельность с C: iso_c_binding и bind(c)

Стандартный механизм iso_c_binding позволяет Fortran и C вызывать функции друг друга с гарантией совместимости типов и соглашений — без хрупких хаков.

iso_c_binding — встроенный модуль Fortran, дающий типы и константы, совместимые с C; bind(c) — атрибут, заставляющий процедуру или тип следовать соглашениям C (имя символа, передача аргументов), что и обеспечивает межъязыковой вызов.

Зачем Fortran дружить с C

Fortran силён в численном ядре, но реальные приложения нуждаются в том, что лучше делать на C или через C-библиотеки: системные вызовы, графика, сеть, базы данных, GPU (CUDA), огромная экосистема C-библиотек. И наоборот — C-программам часто нужна численная мощь Fortran (BLAS, LAPACK исторически на Fortran). Долгое время связь была хрупкой: программисты угадывали, как компилятор «коверкает» имена (name mangling добавляет подчёркивания), как передаёт аргументы, какие размеры у типов — и любое расхождение давало сбой. Fortran 2003 положил этому конец, добавив стандартизованную интероперабельность с C: модуль iso_c_binding и атрибут bind(c). Теперь связка Fortran↔C — это не хак, а гарантированный стандартом контракт.

Совместимые типы

Размеры типов в C формально не фиксированы (int может быть 16, 32 или 64 бита). Чтобы Fortran точно знал, какой kind соответствует C-типу на данной платформе, iso_c_binding предоставляет именованные константы kind: c_int для int, c_double для double, c_float для float, c_char для char, c_ptr для указателя void* и другие. Объявляя переменные с этими kind, вы получаете типы, побитово совпадающие с C.

use iso_c_binding
implicit none
integer(c_int)    :: n          ! совместимо с C int
real(c_double)    :: x          ! совместимо с C double
real(c_float)     :: f          ! совместимо с C float
type(c_ptr)       :: handle     ! совместимо с C void*

Вызов C-функции из Fortran

Чтобы вызвать C-функцию, описывают её интерфейс с атрибутом bind(c), указывая C-имя через name=. Аргументы объявляют совместимыми типами. Важная тонкость: C передаёт скаляры по значению, а Fortran по умолчанию — по ссылке; поэтому скалярные аргументы помечают атрибутом value.

! C-функция: double c_hypot(double a, double b);
module c_math
  use iso_c_binding
  implicit none
  interface
    function c_hypot(a, b) bind(c, name="c_hypot") result(r)
      import :: c_double
      real(c_double), value :: a, b      ! value: передача по значению, как в C
      real(c_double) :: r
    end function c_hypot
  end interface
end module c_math

program use_c
  use c_math
  implicit none
  print *, "hypot(3,4) =", c_hypot(3.0d0, 4.0d0)
end program use_c

Разберём контракт. Атрибут bind(c, name="c_hypot") сообщает: «это C-функция с символом c_hypot, используй C-соглашение о вызове». Без name= символ берётся из имени процедуры в нижнем регистре. Атрибут value у a, b критичен: в C аргументы-скаляры идут по значению, и без value Fortran передал бы адреса, что сломало бы вызов. Оператор import втягивает c_double в область видимости интерфейса. Этот блок — типовой шаблон импорта C-функции в Fortran.

Экспорт Fortran-процедуры в C

Обратное направление столь же важно: сделать Fortran-процедуру вызываемой из C. Для этого её саму помечают bind(c). Тогда компилятор сгенерирует символ с предсказуемым именем и C-совместимым интерфейсом, и C-код сможет её вызвать, объявив соответствующий прототип.

! Fortran-процедура, вызываемая из C:
subroutine compute_sum(arr, n, total) bind(c, name="compute_sum")
  use iso_c_binding
  implicit none
  integer(c_int), value      :: n          ! по значению
  real(c_double), intent(in) :: arr(n)      ! массив по ссылке (как C-указатель)
  real(c_double), intent(out):: total
  total = sum(arr)
end subroutine compute_sum

! Соответствующий прототип в C:
! void compute_sum(const double *arr, int n, double *total);

Массивы передаются между языками как указатели на непрерывную память, и здесь снова всплывает column-major: многомерный массив Fortran и многомерный массив C при одной и той же памяти «видят» индексы транспонированными. Для одномерных массивов проблемы нет, а для матриц нужно либо транспонировать, либо договариваться о трактовке. Скаляр n помечен value (C передаёт его по значению), а arr и total идут по ссылке — это соответствует C-указателям в прототипе.

Строки и указатели

Строки — тонкое место: C-строки нуль-терминированы (заканчиваются байтом \0), а Fortran хранит длину отдельно и нулём не завершает. iso_c_binding даёт константу c_null_char для явного добавления терминатора при передаче строки в C. Для указателей модуль предоставляет тип c_ptr и функции преобразования: c_loc(x) возвращает C-указатель на Fortran-объект, c_f_pointer(cptr, fptr) связывает Fortran-указатель с адресом из C. Это позволяет передавать туда-обратно дескрипторы, буферы, структуры данных.

use iso_c_binding
implicit none
character(len=20) :: fstr
character(len=:), allocatable :: cstr

fstr = "Привет из Fortran"
! добавить нуль-терминатор для передачи в C:
cstr = trim(fstr) // c_null_char
! теперь cstr можно передать в C-функцию, ожидающую char*
Fortran (iso_c_binding)C-тип
integer(c_int)int
real(c_double)double
real(c_float)float
type(c_ptr)void*
character(c_char) + c_null_charchar* (нуль-терм.)
скаляр с атрибутом valueаргумент по значению

Как работает под капотом

На уровне машинного кода Fortran и C — это просто функции с соглашением о вызовах (calling convention): как передаются аргументы (в регистрах/на стеке), кто чистит стек, как именуется символ. Исторически Fortran-компиляторы добавляли к именам подчёркивания (compute_sumcompute_sum_) и передавали всё по ссылке, плюс скрыто слали длины строк — и эти детали различались между компиляторами, отчего ручная связь с C была ненадёжной. Атрибут bind(c) заставляет компилятор отказаться от своих соглашений и строго следовать C ABI: символ именуется ровно как указано (name=), аргументы передаются по C-правилам (с value — по значению), дескрипторы массивов сводятся к простым указателям. Константы из iso_c_binding (c_int, c_double) на этапе компиляции раскрываются в kind, чьи размеры гарантированно совпадают с C-типами целевой платформы. В сумме это даёт переносимый, не зависящий от компилятора контракт — поэтому современная связка Fortran↔C надёжна и широко используется (обёртки к CUDA, MPI, графическим и системным библиотекам).

Совместимые производные типы и структуры C

Интероперабельность простирается дальше скаляров и массивов — Fortran умеет обмениваться с C целыми структурами. Производный тип, помеченный bind(c), гарантированно имеет ту же раскладку памяти, что соответствующая C-структура (struct): те же поля в том же порядке с тем же выравниванием. Это позволяет передавать составные данные между языками единым объектом, а не разбирая на отдельные аргументы.

use iso_c_binding
! Соответствует C-структуре:
!   struct Particle { double x, y, z; double mass; int id; };
type, bind(c) :: particle_t
  real(c_double) :: x, y, z
  real(c_double) :: mass
  integer(c_int) :: id
end type particle_t

interface
  subroutine process_particles(arr, n) bind(c, name="process_particles")
    import :: particle_t, c_int
    type(particle_t), intent(inout) :: arr(*)
    integer(c_int), value :: n
  end subroutine
end interface

Ограничения bind(c)-типа отражают то, что C умеет: только interoperable-поля (типы из iso_c_binding), без аллоцируемых компонентов, указателей Fortran, type-bound procedures или наследования — ведь у C таких понятий нет. Это «общий знаменатель» двух систем типов. Зато в этих рамках обмен абсолютно надёжен: Fortran-процедура и C-функция видят буквально одну и ту же структуру в памяти. На таких совместимых типах строят, например, привязки к графическим библиотекам (передача вершин), к физическим движкам, к форматам данных. Возможность обмениваться структурами, а не только примитивами, делает связку Fortran↔C по-настоящему полноценной: можно строить богатые интерфейсы, а не сводить всё к спискам скалярных аргументов.

Зачем это нужно: место Fortran в гетерогенном мире

Интероперабельность с C — не побочная мелочь, а стратегически важная возможность, определяющая место Fortran в современной разработке. Реальные научные приложения почти никогда не пишутся на одном языке. Типичная архитектура такова: вычислительное ядро — на Fortran (за скорость и удобство численных операций), а обвязка — на других языках: Python для скриптов, анализа и визуализации, C/C++ для системной интеграции и графических интерфейсов, специализированные библиотеки для всего остального. C служит универсальным «лингва франка» между ними: почти любой язык умеет вызывать C-функции и быть вызванным из C. Поэтому, обеспечив связь Fortran↔C, вы фактически связываете Fortran со всей экосистемой.

Самый яркий пример — связка Fortran и Python. Учёные обожают Python за интерактивность, богатство библиотек (matplotlib, pandas, scikit-learn) и простоту, но чистый Python слишком медленен для тяжёлых вычислений. Идеальное разделение: ядро расчёта на Fortran, обёрнутое для вызова из Python (через инструменты вроде f2py, которые под капотом используют именно C-интероп), а вся «оркестровка» — подготовка данных, запуск, анализ результатов, графики — на Python. Так получают и скорость Fortran, и удобство Python. Аналогично через iso_c_binding пишут привязки к GPU (CUDA-ядра на C, вызываемые из Fortran), к параллельным библиотекам, к форматам данных вроде HDF5. Без стандартной интероперабельности Fortran оказался бы изолированным «островом», пригодным лишь для самодостаточных программ. С ней он становится специализированным вычислительным движком в большой гетерогенной системе — ровно той ролью, которая ему лучше всего подходит: пусть Fortran делает то, в чём он непревзойдён (быстрые точные численные расчёты), а интеграцию, ввод-вывод высокого уровня, визуализацию и логику приложения берут на себя языки, сильные в этом. Именно так Fortran остаётся живым и востребованным в 2020-е: не вопреки другим языкам, а в продуктивном симбиозе с ними, и мост bind(c)/iso_c_binding — несущая конструкция этого симбиоза.

Частые ошибки

  • Забыть атрибут value у скаляров. C передаёт скаляры по значению; без value Fortran шлёт адрес, и C прочитает мусор. Скалярные аргументы C-интерфейса почти всегда value.
  • Не добавить c_null_char к строке для C. C-функции ждут нуль-терминированную строку; без терминатора они прочитают за её пределами.
  • Игнорировать column-major при передаче матриц. Fortran и C видят многомерный массив транспонированным; одномерные передаются без проблем, матрицы — с учётом порядка.
  • Использовать обычные kind вместо c_int/c_double. Размер обычного integer может не совпасть с C int. Для интеропа берите типы из iso_c_binding.
  • Полагаться на ручное name mangling. Угадывать подчёркивания не нужно и опасно — bind(c, name=...) задаёт символ явно и переносимо.

Итоги

  • Стандартная интероперабельность Fortran↔C строится на модуле iso_c_binding и атрибуте bind(c).
  • Совместимые типы (c_int, c_double, c_ptr) гарантируют побитовое соответствие C-типам платформы.
  • Скаляры в C идут по значению — помечайте их атрибутом value; массивы передаются как указатели.
  • Строки для C завершают c_null_char; указатели конвертируют через c_loc/c_f_pointer.
  • bind(c, name=...) задаёт символ явно, устраняя проблему name mangling; помните про column-major для матриц.
Проверьте себя
1. Почему скалярный аргумент C-функции в Fortran-интерфейсе обычно помечают атрибутом value?
AЧтобы сделать его константой
BПотому что C передаёт скаляры по значению, а Fortran по умолчанию по ссылке
CЧтобы ускорить вызов
DЭто требование только для строк
2. Что обеспечивает модуль iso_c_binding с типами c_int и c_double?
AАвтоматический перевод кода Fortran в C
BKind-параметры, размеры которых гарантированно совпадают с соответствующими C-типами платформы
CУскорение численных расчётов
DЗащиту от ошибок памяти