Skip to content

Latest commit

 

History

6 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

C++ ↔ Fortran BFGS Interoperability Demo

CI License: MIT

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.


Architecture

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

File Layout

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

Call Flow

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

Name Mangling

The Problem

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'

The Solution: Two-Sided Demangling

C++ sideextern "C" disables C++ name mangling:

extern "C" {
    void bfgs_minimize(int n, double* x0, /* ... */);
}

Fortran sidebind(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 subroutine

Both sides agree: the symbol is bfgs_minimize — no underscores, no prefixes, no parameter encoding.

Verifying with nm

$ nm build/bfgs_demo | grep bfgs_minimize
0000000000004a80 T bfgs_minimize

Key observations:

  • T — symbol is in the .text (code) section
  • No underscore prefixbind(c) suppresses Fortran's default _ suffix
  • No C++ type encodingextern "C" suppresses mangling

What Happens Without It

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 Compatibility

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.

Type Size Table

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.

Calling Convention

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 xmm0xmm7
  • Remaining arguments → stack (right-to-left)
  • Return value → rax (integer) or xmm0 (float)
  • Stack aligned to 16 bytes at call site
  • Red zone: 128 bytes below rsp usable without adjustment

This is the C calling convention. Both extern "C" and bind(c) use it.

Why ABI Mismatches Cause Silent Corruption

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.


Pass-by-Reference

Classic Fortran: Everything Is Passed by Reference

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 &temporary

ISO_C_BINDING: The value Attribute Changes This

With 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
);

Memory Diagram: Pass-by-Reference

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.

Arrays

std::vector<double> and .data()

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.0

Fortran expects a contiguous block of memory for its array arguments. A double* to a std::vector's data buffer is exactly that.

Why Contiguous Memory Matters

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.

Common Mistakes

// 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 contiguous

Row-Major vs Column-Major

2×3 Matrix Example

Consider this matrix:

[ 1  2  3 ]
[ 4  5  6 ]

C++ (Row-Major)

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]

Fortran (Column-Major)

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]

Why This Causes Bugs

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:

  1. Pass 1D arrays only (this project's approach) — vectors have no layout ambiguity.
  2. Transpose before passing — swap rows and columns in C++ before calling Fortran.
  3. Use TRANSPOSE in Fortran — accept row-major and transpose upon receiving.

This Project: No Matrix at the Boundary

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.


Indexing

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.


ISO_C_BINDING

Old-School Interoperability (F77 Style)

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 uppercases

Problems:

  • Non-portable — breaks when switching compilers
  • String arguments need hidden length parameters
  • No type checking across languages
  • Requires trial-and-error with nm

Modern ISO_C_BINDING (F2003+)

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 subroutine

Advantages:

  • Portable — works with gfortran, ifort, flang, PGI, NAG
  • Explicit typesc_int, c_double have guaranteed sizes
  • Explicit linkagebind(c) specifies the exact symbol name
  • Standard-compliant — part of ISO Fortran 2003, refined in 2008/2018
  • C function pointerstype(c_funptr) + c_f_procpointer() enable callbacks

Disadvantages:

  • Slightly more verbose
  • Requires Fortran 2003+ compiler (all modern compilers support this)
  • The value attribute semantics can be surprising to Fortran programmers

Building

CMake Approach

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:

  1. Detects both compilers (g++ and gfortran)
  2. Handles Fortran module dependency ordering (.mod files)
  3. Uses the Fortran linker driver when Fortran is enabled, which pulls in libgfortran

Manual Linking

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_demo

Why -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.

Platform-Specific Setup

Ubuntu / Debian:

sudo apt install g++ gfortran cmake

Fedora / RHEL:

sudo dnf install gcc-c++ gcc-gfortran cmake

Arch Linux:

sudo pacman -S gcc gcc-fortran cmake

Then build:

cd cpp-fortran-bfgs
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
cmake --build . -j$(nproc)
./bfgs_demo

Or use the one-liner:

./scripts/build.sh

Debugging

Inspecting Symbols

# 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

Diagnosing Undefined Reference Errors

/usr/bin/ld: undefined reference to `bfgs_minimize'

Root causes:

  1. Missing extern "C" — C++ mangles the name; Fortran emits plain bfgs_minimize
  2. Missing bind(c) — Fortran emits __bfgs_MOD_bfgs_minimize or bfgs_minimize_
  3. Name mismatchbind(c, name="bfgs_minimize") doesn't match the C declaration
  4. Fortran .o file not linked — check CMake target_link_libraries
  5. 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 Debugging

$ 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.)

Runtime Library Issues

$ ldd ./bfgs_demo | grep "not found"
  libgfortran.so.5 => not found

Fix: 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"

Performance

Benchmark Results (2D Rosenbrock, 500 trials, Release build, g++/gfortran 15.2)

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%

Why Overhead Is Negligible

  1. Memory is shared — no array copies at the language boundary
  2. Same calling convention — both sides use the C ABI; no thunks or adapters
  3. Objective evaluation dominates — each BFGS iteration evaluates the Rosenbrock function (O(n) floating-point operations), which dwarfs any call overhead
  4. Small number of boundary crossings — one C++→Fortran entry call, plus ~22 callback invocations (Fortran→C++) over the entire optimization

When Overhead Might Matter

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.


Memory Layout Diagram

 ╔══════════════════════════════════════════════════════════════╗
 ║                     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.                                     ║
 ║                                                              ║
 ╚══════════════════════════════════════════════════════════════╝

Common Mistakes

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

Diagnostic Workflow

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

Expected Output

╔══════════════════════════════════════════════════════════════════╗
║  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.

References

  • ISO/IEC 1539-1:2010 — Fortran 2008 standard (ISO_C_BINDING in Chapter 15)
  • Metcalf, Reid, CohenModern Fortran Explained (Oxford, 2011)
  • Nocedal & WrightNumerical Optimization (Springer, 2006) — BFGS algorithm
  • GCC Fortran ManualInteroperability with C
  • System V AMD64 ABICalling convention reference

License

MIT — use freely in your own HPC and scientific computing projects.

About

Production-quality C++20 ↔ Fortran BFGS optimizer. Zero-copy ABI interop via ISO_C_BINDING, benchmark-backed performance analysis.

Topics

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages