A production-quality example demonstrating how Modern C++20 and Fortran interoperate in HPC and scientific computing.
The Fortran BFGS optimizer minimizes the N-dimensional Rosenbrock function while the C++ layer controls the application, passes data, receives results, and benchmarks performance.
main.cpp (C++)
│
├── rosenbrock() ← C-linkage callback, called FROM Fortran
│
├── run_bfgs() ← C++ wrapper (std::vector ↔ raw double*)
│ │
│ └── bfgs_minimize() ← Fortran entry point via ISO_C_BINDING
│ │
│ ├── line_search() ← Armijo backtracking
│ │ └── callback ──→ rosenbrock() [back to C++]
│ │
│ ├── linalg operations ← dot, norm, matvec, BFGS update
│ │
│ └── objective.f90 ← Fortran-native Rosenbrock (benchmark only)
│
└── print stats, verify x* ≈ [1,1,...,1], benchmark timings
cpp-fortran-bfgs/
├── CMakeLists.txt CMake build (CXX + Fortran)
├── README.md
├── fortran/
│ ├── linalg.f90 BLAS-style vector ops (dot, norm, axpy, matvec)
│ ├── line_search.f90 Armijo backtracking line search
│ ├── bfgs.f90 BFGS optimizer (ISO_C_BINDING entry point)
│ └── objective.f90 Fortran-native Rosenbrock (benchmarking)
├── include/
│ └── bfgs.hpp C++ header (extern "C" declarations, BFGSResult)
├── src/
│ ├── main.cpp Driver (CLI parsing, verification, benchmark)
│ ├── wrapper.cpp C++ wrapper + native C++ BFGS for benchmarking
│ └── objective.cpp C-linkage Rosenbrock callback
└── scripts/
└── build.sh One-command build + run
C++ stack (main.cpp)
│
│ std::vector<double> x0 = {-1.2, 0.7};
│ x0.data() ───────────► points to contiguous heap memory
│ ┌─────────────────────┐
├── run_bfgs(x0) │ double[2] at 0x... │
│ │ │ [-1.2, 0.7] │ ← SHARED memory
│ │ └──────┬──────────────┘
│ │ │
│ └── bfgs_minimize( │
│ n=2, │
│ x0.data(), ◄───────────┘ same pointer, zero-copy
│ &f_out, │
│ grad_tol=1e-6, │
│ max_iter=1000, │
│ &iter, │
│ &converged, │
│ verbose=1, │
│ &rosenbrock ◄────────── C function pointer
│ )
│ │
│ │ Inside Fortran (bfgs.f90):
│ │ x(1:n) maps to SAME memory as x0.data()
│ │ x(1) = -1.2, x(2) = 0.7
│ │
│ │ DO iteration loop:
│ │ p = -H * gradient
│ │ α = line_search(callback, ...)
│ │ │
│ │ └── callback(n, x, &f, &grad) ───▶ rosenbrock() [C++]
│ │ (x is the SAME pointer, no copy)
│ │ x = x + α * p
│ │ BFGS update: H ← updated inverse Hessian
│ │ END DO
│ │
│ │ x0(:) = x(:) ← copy minimizer back to C++ buffer
│ │
│ └── return
│
│ result.x = x0 ← now [1.0, 1.0, ...]
│ result.f = ~0
│ result.converged = true
│
└── print stats, verify, benchmark
C++ compilers mangle function names to encode the full signature (namespace, class, parameter types). This enables function overloading but produces symbols like:
_Z15bfgs_minimizeiPdS_diPiS_PFviPKdS1_S1_E
Fortran compilers also mangle names — historically by appending underscores or using uppercase. Without bind(c), gfortran emits symbols like __bfgs_MOD_bfgs_minimize (module prefix) or bfgs_minimize_ (trailing underscore).
If both sides mangle differently, the linker cannot find the symbol:
/usr/bin/ld: undefined reference to `bfgs_minimize'
C++ side — extern "C" disables C++ name mangling:
extern "C" {
void bfgs_minimize(int n, double* x0, /* ... */);
}Fortran side — bind(c, name="bfgs_minimize") sets the exact linker symbol:
subroutine bfgs_minimize(n, x0, f, ...) bind(c, name="bfgs_minimize")
use iso_c_binding
integer(c_int), value :: n
real(c_double), intent(inout) :: x0(n)
...
end subroutineBoth sides agree: the symbol is bfgs_minimize — no underscores, no prefixes, no parameter encoding.
$ nm build/bfgs_demo | grep bfgs_minimize
0000000000004a80 T bfgs_minimizeKey observations:
T— symbol is in the.text(code) section- No underscore prefix —
bind(c)suppresses Fortran's default_suffix - No C++ type encoding —
extern "C"suppresses mangling
| Scenario | Fortran emits | C++ expects | Result |
|---|---|---|---|
| Both sides correct | bfgs_minimize |
bfgs_minimize |
✅ Links |
Missing extern "C" |
bfgs_minimize |
_Z15bfgs_minimize... |
❌ Undefined reference |
Missing bind(c) |
__bfgs_MOD_bfgs_minimize |
bfgs_minimize |
❌ Undefined reference |
| Both missing | __bfgs_MOD_bfgs_minimize |
_Z15bfgs_minimize... |
❌ Undefined reference |
ABI (Application Binary Interface) defines how data is laid out in memory and how functions are called. C++ and Fortran must agree on every detail.
| Fortran ISO_C_BINDING | C/C++ type | Size (bytes) | Notes |
|---|---|---|---|
integer(c_int) |
int |
4 | Signed 32-bit |
integer(c_long) |
long |
8 (LP64) | Platform-dependent |
real(c_float) |
float |
4 | IEEE 754 single |
real(c_double) |
double |
8 | IEEE 754 double |
type(c_funptr) |
void(*)() |
8 | Function pointer |
type(c_ptr) |
void* |
8 | Opaque pointer |
Critical rule: Never use Fortran's default integer or real types for interop. Default integer can be 4 or 8 bytes depending on compiler flags. Default real can be 4 or 8 bytes. Always use integer(c_int), real(c_double), etc.
On x86-64 Linux (System V AMD64 ABI):
- First 6 integer/pointer arguments → registers
rdi, rsi, rdx, rcx, r8, r9 - First 8 floating-point arguments → registers
xmm0–xmm7 - Remaining arguments → stack (right-to-left)
- Return value →
rax(integer) orxmm0(float) - Stack aligned to 16 bytes at call site
- Red zone: 128 bytes below
rspusable without adjustment
This is the C calling convention. Both extern "C" and bind(c) use it.
If Fortran expects a 4-byte integer but C++ passes an 8-byte long, the Fortran side reads only the first 4 bytes. The remaining 4 bytes become the next parameter, corrupting every subsequent argument. This does not segfault — it silently produces wrong results.
In traditional Fortran, all arguments are passed by reference — the callee receives a pointer to the caller's data. This applies even to scalar literals:
call foo(42) ! Fortran creates a temporary, passes &temporaryWith bind(c), the value attribute makes Fortran accept pass-by-value for scalars:
subroutine bfgs_minimize(n, x0, f, grad_tol, max_iter, iter, converged, fcn) bind(c)
integer(c_int), value :: n ! ← VALUE: from C: `int n` (not `int* n`)
real(c_double), intent(inout) :: x0(n) ! ← NO value: from C: `double* x0`
real(c_double), intent(out) :: f ! ← NO value: from C: `double* f`
real(c_double), value :: grad_tol ! ← VALUE: from C: `double grad_tol`
integer(c_int), value :: max_iter ! ← VALUE: from C: `int max_iter`
integer(c_int), intent(out) :: iter ! ← NO value: from C: `int* iter`
integer(c_int), intent(out) :: converged ! ← NO value: from C: `int* converged`
type(c_funptr), value :: fcn ! ← VALUE: from C: `void(*fcn)(...)`The corresponding C declaration:
void bfgs_minimize(
int n, // value
double* x0, // reference
double* f, // reference (intent(out))
double grad_tol, // value
int max_iter, // value
int* iter, // reference (intent(out))
int* converged, // reference (intent(out))
void (*fcn)(int, const double*, double*, double*) // value
);C++ side Fortran side
───────── ────────────
std::vector<double> x0
.data() = 0x7fff1234 ────────────► x(1:n) at 0x7fff1234
[ -1.2, 0.7 ] x(1) = -1.2
x(2) = 0.7
The Fortran array x occupies the EXACT SAME memory as the
C++ vector's heap allocation. When Fortran writes x(1) = 1.0,
C++ sees x0[0] = 1.0.
This is zero-copy interoperability.
std::vector<double> stores its elements in a contiguous heap allocation. The .data() member returns a double* pointing to the first element.
std::vector<double> v = {1.0, 2.0, 3.0};
double* ptr = v.data();
// ptr[0] = 1.0, ptr[1] = 2.0, ptr[2] = 3.0Fortran expects a contiguous block of memory for its array arguments. A double* to a std::vector's data buffer is exactly that.
Fortran arrays are always contiguous. When Fortran receives x(n), it assumes that x(1), x(2), ..., x(n) are laid out sequentially in memory with stride 1.
If you pass a pointer to non-contiguous data (e.g., a column of a row-major matrix, or a std::deque), Fortran will read garbage from the gaps between elements.
// WRONG — &vec is the vector object, not the data
bfgs_minimize(n, &vec, ...); // passes pointer to std::vector internals
// WRONG — data() is fine, but Fortran modifies in-place
bfgs_minimize(n, x0.data(), ...); // OK if const isn't needed, U.B. if x0 is const
// CORRECT — mutable copy
std::vector<double> x(x0); // copy
bfgs_minimize(n, x.data(), ...); // safe: x is mutable and contiguousConsider this matrix:
[ 1 2 3 ]
[ 4 5 6 ]
Elements of each row are contiguous in memory:
Memory address: 0 1 2 3 4 5
Value: 1 2 3 4 5 6
└─ row 0 ─┘ └─ row 1 ─┘
double A[2][3] = {{1,2,3}, {4,5,6}};
// A[0][0]=1, A[0][1]=2, A[0][2]=3, A[1][0]=4, A[1][1]=5, A[1][2]=6
// Memory: [1, 2, 3, 4, 5, 6]Elements of each column are contiguous in memory:
Memory address: 0 1 2 3 4 5
Value: 1 4 2 5 3 6
└ col 1 ┘ └ col 2 ┘ └ col 3 ┘
integer :: A(2, 3)
! A(1,1)=1, A(2,1)=4, A(1,2)=2, A(2,2)=5, A(1,3)=3, A(2,3)=6
! Memory: [1, 4, 2, 5, 3, 6]If C++ passes a row-major matrix to Fortran expecting column-major:
C++ passes: [1, 2, 3, 4, 5, 6] (row-major)
Fortran reads:
A(1,1) = 1 ← correct
A(2,1) = 2 ← WRONG! should be 4
A(1,2) = 3 ← WRONG! should be 2
...
Every element appears transposed. The solution:
- Pass 1D arrays only (this project's approach) — vectors have no layout ambiguity.
- Transpose before passing — swap rows and columns in C++ before calling Fortran.
- Use
TRANSPOSEin Fortran — accept row-major and transpose upon receiving.
The BFGS inverse Hessian H(n,n) is allocated inside Fortran and never crosses the language boundary. C++ only passes 1D vectors (x, grad). This completely avoids the row/column-major problem.
| Aspect | C++ | Fortran |
|---|---|---|
| First element | x[0] |
x(1) |
| Last element | x[n-1] |
x(n) |
| Loop typical | for (i=0; i<n; ++i) |
do i = 1, n |
When passing a double* from C++ to Fortran, the memory is shared but the indexing is different:
C++ view: Fortran view:
x[0] x[1] x[2] x(1) x(2) x(3)
┌────┬────┬────┐ ┌────┬────┬────┐
│ a │ b │ c │ │ a │ b │ c │
└────┴────┴────┘ └────┴────┴────┘
0x1000 same memory
Both access the same bytes at 0x1000. The index offset differs, but ISO_C_BINDING handles this correctly — Fortran's x(1:n) descriptor maps to n elements starting at the pointer.
Before Fortran 2003, interop was compiler-specific and fragile:
! gfortran-specific hack
subroutine foo(n, x)
integer :: n
real*8 :: x(n)
! No bind(c) — relies on compiler flags:
! -fno-underscoring (remove trailing _)
! -fno-second-underscore
end subroutine// C side — must guess the Fortran compiler's mangling
extern void foo_(int* n, double* x); // gfortran adds _
extern void FOO(int* n, double* x); // ifort uppercasesProblems:
- Non-portable — breaks when switching compilers
- String arguments need hidden length parameters
- No type checking across languages
- Requires trial-and-error with
nm
use iso_c_binding
subroutine foo(n, x) bind(c, name="foo")
integer(c_int), value :: n
real(c_double), intent(in) :: x(n)
end subroutineAdvantages:
- Portable — works with gfortran, ifort, flang, PGI, NAG
- Explicit types —
c_int,c_doublehave guaranteed sizes - Explicit linkage —
bind(c)specifies the exact symbol name - Standard-compliant — part of ISO Fortran 2003, refined in 2008/2018
- C function pointers —
type(c_funptr)+c_f_procpointer()enable callbacks
Disadvantages:
- Slightly more verbose
- Requires Fortran 2003+ compiler (all modern compilers support this)
- The
valueattribute semantics can be surprising to Fortran programmers
project(cpp-fortran-bfgs LANGUAGES CXX Fortran)
add_library(fortran_bfgs STATIC fortran/linalg.f90 fortran/line_search.f90
fortran/bfgs.f90 fortran/objective.f90)
add_executable(bfgs_demo src/main.cpp src/wrapper.cpp src/objective.cpp)
target_link_libraries(bfgs_demo PRIVATE fortran_bfgs)CMake automatically:
- Detects both compilers (
g++andgfortran) - Handles Fortran module dependency ordering (
.modfiles) - Uses the Fortran linker driver when Fortran is enabled, which pulls in
libgfortran
If building without CMake, the linker order matters:
# Compile Fortran
gfortran -std=f2008 -c fortran/linalg.f90 -o linalg.o
gfortran -std=f2008 -c fortran/line_search.f90 -o line_search.o
gfortran -std=f2008 -c fortran/bfgs.f90 -o bfgs.o
gfortran -std=f2008 -c fortran/objective.f90 -o objective.o
# Compile C++
g++ -std=c++20 -c src/objective.cpp -o objective_cpp.o
g++ -std=c++20 -c src/wrapper.cpp -o wrapper.o
g++ -std=c++20 -c src/main.cpp -o main.o
# Link — Fortran .o files MUST come before -lgfortran
g++ main.o wrapper.o objective_cpp.o linalg.o line_search.o bfgs.o objective.o \
-lgfortran -o bfgs_demoWhy -lgfortran? The Fortran .o files reference symbols like _gfortran_st_write, _gfortran_transfer_real, _gfortran_st_write_done (for I/O) and array intrinsic helpers. -lgfortran provides the Fortran runtime library that resolves these.
Ubuntu / Debian:
sudo apt install g++ gfortran cmakeFedora / RHEL:
sudo dnf install gcc-c++ gcc-gfortran cmakeArch Linux:
sudo pacman -S gcc gcc-fortran cmakeThen build:
cd cpp-fortran-bfgs
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
cmake --build . -j$(nproc)
./bfgs_demoOr use the one-liner:
./scripts/build.sh# List all symbols containing "bfgs"
$ nm build/bfgs_demo | grep bfgs
# Detailed symbol table (with section info)
$ objdump -t build/bfgs_demo | grep bfgs
# ELF-level symbol inspection
$ readelf -s build/bfgs_demo | grep bfgs
# Check runtime library dependencies
$ ldd build/bfgs_demo
# → Look for libgfortran.so.5 — must be present/usr/bin/ld: undefined reference to `bfgs_minimize'
Root causes:
- Missing
extern "C"— C++ mangles the name; Fortran emits plainbfgs_minimize - Missing
bind(c)— Fortran emits__bfgs_MOD_bfgs_minimizeorbfgs_minimize_ - Name mismatch —
bind(c, name="bfgs_minimize")doesn't match the C declaration - Fortran
.ofile not linked — check CMaketarget_link_libraries - Link order — C++ objects must come before Fortran objects when using
-lgfortran
Fix: check nm on both the C++ object and the Fortran object to see what symbols each provides and expects.
$ gdb ./build/bfgs_demo
(gdb) break bfgs_minimize # break on Fortran entry point
(gdb) run
(gdb) print n # print Fortran variable (value)
(gdb) print x[0]@n # print n elements starting at x (1-based Fortran = C x[0])
(gdb) backtrace # see C++ → Fortran → C++ call stack
(gdb) info registers # check calling convention (rdi, rsi, xmm0, etc.)$ ldd ./bfgs_demo | grep "not found"
libgfortran.so.5 => not foundFix: install gfortran or set LD_LIBRARY_PATH:
export LD_LIBRARY_PATH=/usr/lib/gcc/x86_64-linux-gnu/15:$LD_LIBRARY_PATH
# or link statically
cmake .. -DCMAKE_EXE_LINKER_FLAGS="-static-libgfortran"Fortran BFGS (via C++ interop)
Trials: 500 | Total: 2.90 ms | Avg/call: 5.790 µs
Native C++ BFGS
Trials: 500 | Total: 3.01 ms | Avg/call: 6.022 µs
Interop overhead: -3.9%
- Memory is shared — no array copies at the language boundary
- Same calling convention — both sides use the C ABI; no thunks or adapters
- Objective evaluation dominates — each BFGS iteration evaluates the Rosenbrock function (O(n) floating-point operations), which dwarfs any call overhead
- Small number of boundary crossings — one C++→Fortran entry call, plus ~22 callback invocations (Fortran→C++) over the entire optimization
Interop overhead becomes measurable only when:
- The objective function is trivial (e.g., returning a constant)
- You cross the boundary thousands of times per second
- You're using debug builds with no inlining
For real HPC workloads (PDE solvers, molecular dynamics, CFD), the cost of the boundary is less than 0.1% of total runtime.
╔══════════════════════════════════════════════════════════════╗
║ PROCESS ADDRESS SPACE ║
╠══════════════════════════════════════════════════════════════╣
║ ║
║ C++ HEAP ║
║ ┌──────────────────────────────────────────────────┐ ║
║ │ std::vector<double> x0 (24 bytes on stack) │ ║
║ │ .data() = 0x5555000100 ──┐ │ ║
║ │ .size() = 2 │ │ ║
║ └────────────────────────────┼─────────────────────┘ ║
║ │ ║
║ ┌────────────────────────────▼─────────────────────┐ ║
║ │ Contiguous double[2] at 0x5555000100 │ ║
║ │ [ -1.2 | 0.7 ] │ ║
║ │ ↑ 0x00 ↑ 0x08 │ ║
║ └──────────────────────────────────────────────────┘ ║
║ │ ║
║ ═══════════════════════════ │ ═══════════════════════ ║
║ C++ STACK (main.cpp) │ C++ STACK (wrapper.cpp) ║
║ ─────────────────────────────┼────────────────── ║
║ double* x0_ptr ──────────────┘ ║
║ double f_out = 0.0 ║
║ int iter = 0 ║
║ int converged = 0 ║
║ ║
║ ════════════════════════════════════════════════════════ ║
║ FORTRAN STACK (bfgs.f90) ║
║ ──────────────────────────────────────────────────── ║
║ x(1:2) → 0x5555000100 (aliases C++ heap) ║
║ x(1) = -1.2 x(2) = 0.7 ║
║ ║
║ grad(1:2) → Fortran stack (local) ║
║ H(2,2) → Fortran stack (column-major!) ║
║ H(1,1) H(2,1) H(1,2) H(2,2) ← column-major layout ║
║ 1.0 0.0 0.0 1.0 ║
║ ║
║ p(1:2) → Fortran stack (local) ║
║ s(1:2), y(1:2)→ Fortran stack (local) ║
║ ║
║ ════════════════════════════════════════════════════════ ║
║ CALLBACK STACK (rosenbrock() in objective.cpp) ║
║ ──────────────────────────────────────────────────── ║
║ const double* x → 0x5555000100 (SAME memory!) ║
║ double* f → &f_out (C++ stack lower) ║
║ double* grad → Fortran stack (caller's local) ║
║ ║
║ *** KEY INSIGHT *** ║
║ x at 0x5555000100 is shared across all three layers: ║
║ C++ main.cpp → x0.data() ║
║ Fortran bfgs → x(1:n) ║
║ C++ callback → const double* x ║
║ ║
║ When Fortran writes x(1) = 1.0: ║
║ → C++ sees x0[0] = 1.0 (same byte at 0x5555000100) ║
║ → Callback sees x[0] = 1.0 (same pointer) ║
║ ZERO COPIES THROUGHOUT. ║
║ ║
╚══════════════════════════════════════════════════════════════╝
| Mistake | Symptom | How to Identify | Fix |
|---|---|---|---|
Wrong integer size (Fortran integer vs C long) |
Segfault or corrupted iter, converged |
Check sizes with sizeof() and STORAGE_SIZE() |
Use integer(c_int) ↔ int (both 4 bytes) |
real(4) vs double |
NaN or silently wrong results | Fortran selected_real_kind mismatch |
Use real(c_double) ↔ double |
Missing extern "C" |
Undefined reference to mangled name | nm shows _Z15bfgs_minimize... |
Wrap declaration in extern "C" { } |
Missing bind(c) |
Undefined reference with _ suffix |
nm shows bfgs_minimize_ or __bfgs_MOD_... |
Add bind(c, name="bfgs_minimize") |
| Row-major vs column-major | Wrong matrix values (transposed) | Output appears transposed | Use 1D arrays at boundary, or transpose before passing |
Passing &vec instead of vec.data() |
Garbage data, deterministic crash | &vec is address of std::vector object, not data |
Use vec.data() |
Missing -lgfortran |
Undefined _gfortran_* symbols |
ldd shows missing libgfortran.so |
Add -lgfortran to link flags; CMake handles this automatically |
| Compiler ABI mismatch (gfortran vs ifort) | Segfault on call | Different convention for array descriptors | Use same compiler family; bind(c) helps but .mod files are compiler-specific |
Fortran modifies const memory |
Undefined behavior, crash | C++ passes const double*, Fortran has intent(inout) |
Copy the vector before passing; Fortran needs mutable memory |
Forgetting value attribute |
All scalars passed as pointers | Fortran expects int n but C++ passes int directly |
Add value to scalar parameters in Fortran |
Your program crashes or produces wrong results.
│
▼
1. Verify symbols match
$ nm bfgs_demo | grep bfgs_minimize
→ Must show plain "bfgs_minimize" (no underscores, no mangling)
2. Check type sizes
Fortran: print *, c_sizeof(1_c_int), c_sizeof(1.0_c_double)
C++: std::cout << sizeof(int) << " " << sizeof(double)
→ Must both output "4 8"
3. Check library dependencies
$ ldd bfgs_demo
→ libgfortran must be present
4. Debug with GDB
(gdb) break bfgs_minimize
(gdb) run
(gdb) print x[0]@n ← check array values at entry
(gdb) info args ← see argument values from Fortran's perspective
╔══════════════════════════════════════════════════════════════════╗
║ C++ ↔ Fortran BFGS Interoperability Demo ║
╚══════════════════════════════════════════════════════════════════╝
Configuration:
Dimension : 2
grad_tol : 1.000000e-06
max_iter : 200
Initial guess:
x0 = [-1.20000000, 0.70000000]
f(x0) = 5.96000000e+01
─── Running Fortran BFGS ───
=========================================================
BFGS (Fortran) — Rosenbrock Minimisation
Dimension: 2
=========================================================
Iter f(x) ||grad||
0 5.96000E+01 3.88865E+02
...
22 1.41068E-18 5.24203E-08
=========================================================
Converged in 22 iterations.
Final ||grad|| = 5.2420E-08
Final f(x) = 1.410679E-18
─── Fortran BFGS Result ───
x* = [1.00000000, 1.00000000]
f(x*) = 1.41067934e-18
Iterations = 22
Converged = YES
Verification: PASSED ✓
─── Performance Benchmark ───
Fortran BFGS (via C++ interop) : ~5.8 µs/call
Native C++ BFGS : ~6.0 µs/call
Interop overhead: < 5%
✓ Overhead is negligible.
- ISO/IEC 1539-1:2010 — Fortran 2008 standard (ISO_C_BINDING in Chapter 15)
- Metcalf, Reid, Cohen — Modern Fortran Explained (Oxford, 2011)
- Nocedal & Wright — Numerical Optimization (Springer, 2006) — BFGS algorithm
- GCC Fortran Manual — Interoperability with C
- System V AMD64 ABI — Calling convention reference
MIT — use freely in your own HPC and scientific computing projects.