Интероперабельность с 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_char | char* (нуль-терм.) |
скаляр с атрибутом value | аргумент по значению |
Как работает под капотом
На уровне машинного кода Fortran и C — это просто функции с соглашением о вызовах (calling convention): как передаются аргументы (в регистрах/на стеке), кто чистит стек, как именуется символ. Исторически Fortran-компиляторы добавляли к именам подчёркивания (compute_sum → compute_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 передаёт скаляры по значению; безvalueFortran шлёт адрес, и C прочитает мусор. Скалярные аргументы C-интерфейса почти всегдаvalue. - Не добавить
c_null_charк строке для C. C-функции ждут нуль-терминированную строку; без терминатора они прочитают за её пределами. - Игнорировать column-major при передаче матриц. Fortran и C видят многомерный массив транспонированным; одномерные передаются без проблем, матрицы — с учётом порядка.
- Использовать обычные kind вместо
c_int/c_double. Размер обычногоintegerможет не совпасть с Cint. Для интеропа берите типы из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 для матриц.