From b166e8bd8f9c352b4f22bbcbefcccc9d9f1c2a09 Mon Sep 17 00:00:00 2001 From: Jae Hoon Seo Date: Fri, 2 Oct 2026 15:08:35 +0900 Subject: [PATCH 1/6] Keep large test arrays off the stack With OpenMP enabled, gfortran places fixed-size local arrays on the stack (-fopenmp implies -frecursive). The taut-string case of the modal test kept 1.7 MB of dense matrices and mode shapes there, within 0.25 MB of the 2 MB MinGW default reserve, and stopped with a stack overflow on some Windows CI runners. Allocate it, and the other test arrays larger than 64 KB (modal, snap-load, and torsion tests), on the heap. --- tests/test_modal.f90 | 12 +++++++----- tests/test_snap_load.f90 | 5 ++++- tests/test_torsion_deck.f90 | 3 ++- tests/test_torsion_orcaflex.f90 | 3 ++- tests/test_torsion_validation.f90 | 4 +++- 5 files changed, 18 insertions(+), 9 deletions(-) diff --git a/tests/test_modal.f90 b/tests/test_modal.f90 index 01f91dd..0079076 100644 --- a/tests/test_modal.f90 +++ b/tests/test_modal.f90 @@ -52,12 +52,14 @@ END SUBROUTINE require SUBROUTINE case_taut_string() INTEGER, PARAMETER :: NE = 100, NN = NE + 1, NM = 80 REAL(wp), PARAMETER :: L = 100.0_wp, EA = 1.0e8_wp, RHO = 100.0_wp, EPS = 1.0e-3_wp - REAL(wp) :: q(3*NN), l0(NE), ea_(NE), rho_a(NE), k(3*NN, 3*NN), m(3*NN, 3*NN), om2(NM), sh(3*NN, NM) + REAL(wp) :: q(3*NN), l0(NE), ea_(NE), rho_a(NE), om2(NM) REAL(wp) :: t, mc, f_exp, f_got, l0tot, ax, tr - REAL(wp), ALLOCATABLE :: kb(:, :), mb(:, :) + ! The dense matrices and the mode shapes (about 1.7 MB) live on the heap, not the stack. + REAL(wp), ALLOCATABLE :: kb(:, :), mb(:, :), k(:, :), m(:, :), sh(:, :) INTEGER :: conn(2, NE), i, j, es, nf, nmode, kd LOGICAL :: free(3*NN) CHARACTER(256) :: em + ALLOCATE (k(3*NN, 3*NN), m(3*NN, 3*NN), sh(3*NN, NM)) l0tot = L/(1.0_wp + EPS) DO i = 1, NE conn(:, i) = [i, i + 1] @@ -211,12 +213,12 @@ SUBROUTINE case_slack_string() !! A slack tension-only string has no stiffness (K = 0): every frequency is zero and the !! banded solver still returns M-orthonormal shapes for the repeated zero eigenvalue. INTEGER, PARAMETER :: NE = 20, NN = NE + 1, NM = 6 - REAL(wp) :: q(3*NN), l0(NE), ea_(NE), rho_a(NE), om2(NM), sh(3*NN, NM), gram(NM, NM) - REAL(wp) :: k(3*NN, 3*NN), m(3*NN, 3*NN) - REAL(wp), ALLOCATABLE :: kb(:, :), mb(:, :) + REAL(wp) :: q(3*NN), l0(NE), ea_(NE), rho_a(NE), om2(NM), gram(NM, NM) + REAL(wp), ALLOCATABLE :: kb(:, :), mb(:, :), k(:, :), m(:, :), sh(:, :) INTEGER :: conn(2, NE), i, es, nf, kd LOGICAL :: free(3*NN) CHARACTER(256) :: em + ALLOCATE (k(3*NN, 3*NN), m(3*NN, 3*NN), sh(3*NN, NM)) DO i = 1, NE conn(:, i) = [i, i + 1] END DO diff --git a/tests/test_snap_load.f90 b/tests/test_snap_load.f90 index 6ee1900..6a86d16 100644 --- a/tests/test_snap_load.f90 +++ b/tests/test_snap_load.f90 @@ -287,7 +287,10 @@ SUBROUTINE ba_load(q, v, force, jac_q, jac_v, ErrStat, ErrMsg) REAL(wp), INTENT(OUT) :: force(:), jac_q(:, :), jac_v(:, :) INTEGER, INTENT(OUT) :: ErrStat CHARACTER(*), INTENT(OUT) :: ErrMsg - REAL(wp) :: f_ba(NDOF), jq_ba(NDOF, NDOF), jv_ba(NDOF, NDOF), f_sb(NDOF), kr_sb(NDOF, NDOF) + REAL(wp) :: f_ba(NDOF), f_sb(NDOF) + ! The three Jacobian blocks (about 95 KB) live on the heap, not the stack. + REAL(wp), ALLOCATABLE :: jq_ba(:, :), jv_ba(:, :), kr_sb(:, :) + ALLOCATE (jq_ba(NDOF, NDOF), jv_ba(NDOF, NDOF), kr_sb(NDOF, NDOF)) CALL CD_Cable_Axial_Damping_Load(q, v, conn_h, l0_h, ba_h, f_ba, jq_ba, jv_ba, ErrStat, ErrMsg) IF (ErrStat /= CD_DAMP_OK) RETURN CALL CD_Seabed_Penalty_Load(RESHAPE(q, [3, NN]), kn_h, SEABED_Z, f_sb, kr_sb, ErrStat, ErrMsg) diff --git a/tests/test_torsion_deck.f90 b/tests/test_torsion_deck.f90 index ada5465..c478120 100644 --- a/tests/test_torsion_deck.f90 +++ b/tests/test_torsion_deck.f90 @@ -603,8 +603,9 @@ END SUBROUTINE check_roll_range SUBROUTINE insert_option(path, row) !! Insert an OPTIONS row (just after the section header) of a deck written by write_deck. CHARACTER(*), INTENT(IN) :: path, row - CHARACTER(512) :: lines(400) + CHARACTER(512), ALLOCATABLE :: lines(:) ! the deck text on the heap, not the stack INTEGER :: u, n, ios, k + ALLOCATE (lines(400)) OPEN (NEWUNIT=u, FILE=path, STATUS='OLD', ACTION='READ') n = 0 DO diff --git a/tests/test_torsion_orcaflex.f90 b/tests/test_torsion_orcaflex.f90 index 059b4de..a261e96 100644 --- a/tests/test_torsion_orcaflex.f90 +++ b/tests/test_torsion_orcaflex.f90 @@ -396,8 +396,9 @@ END SUBROUTINE lazy_wave_deck SUBROUTINE add_outputs(path) !! Append the torsion channels to the OUTPUTS of a deck written by write_deck. CHARACTER(*), INTENT(IN) :: path - CHARACTER(512) :: lines(200) + CHARACTER(512), ALLOCATABLE :: lines(:) ! the deck text on the heap, not the stack INTEGER :: u, n, ios, k + ALLOCATE (lines(200)) OPEN (NEWUNIT=u, FILE=path, STATUS='OLD', ACTION='READ') n = 0 DO diff --git a/tests/test_torsion_validation.f90 b/tests/test_torsion_validation.f90 index a8c2a39..40607aa 100644 --- a/tests/test_torsion_validation.f90 +++ b/tests/test_torsion_validation.f90 @@ -598,11 +598,13 @@ SUBROUTINE check_bordered_term() INTEGER, PARAMETER :: NE = 16, ND = 6*(NE + 1), MAXIT = 30 TYPE(CD_HermiteTorsionType) :: tors REAL(wp) :: seed(ND), qs(ND), l0(NE), curv(NE + 1), res, ends(3, 4), q0(ND), hist(MAXIT, 2), sk, fdead(ND) - REAL(wp) :: ab(LDAB, ND), abc(LDAB, ND), g(ND), z(ND), e, th, mt, qfin(ND), dfin, rho, amp, rate + REAL(wp) :: g(ND), z(ND), e, th, mt, qfin(ND), dfin, rho, amp, rate + REAL(wp), ALLOCATABLE :: ab(:, :), abc(:, :) ! band matrices on the heap, not the stack INTEGER :: k, it, es, ic, nit(2), nneg_b, nneg_k, info, ipiv(ND), nr LOGICAL :: conv(2), ok, mask(ND) CHARACTER(512) :: em EXTERNAL :: dgbsv + ALLOCATE (ab(LDAB, ND), abc(LDAB, ND)) l0 = 1.0_wp/NE fdead = 0.0_wp fdead(ND - 5) = 10.0_wp From a4be6949900d45fcbb62bfa9e648a6d98e03d3f2 Mon Sep 17 00:00:00 2001 From: Jae Hoon Seo Date: Fri, 2 Oct 2026 15:08:35 +0900 Subject: [PATCH 2/6] Build the aggregate initialisation bundles on the heap CD_AGG_Init_From_Deck, the C API deck initialisation, and the driver's mixed route each held one or two aggregate bundles (about 37 KB and 33 KB) as stack locals: frames of 108 KB, 74 KB, and 83 KB. Initialisation runs on the thread of the calling host, whose stack size CableDyn does not choose, so allocate them instead. This lowers the deepest stack of a C API run from about 345 KB to 250 KB. The per-step path is unchanged and still does not allocate. --- app/cabledyn.f90 | 10 ++++++++-- src/CableDyn_CAPI.f90 | 8 +++++++- src/CableDyn_OpenFAST_Aggregate.f90 | 12 ++++++++++-- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/app/cabledyn.f90 b/app/cabledyn.f90 index d03dec1..dc3fe3b 100644 --- a/app/cabledyn.f90 +++ b/app/cabledyn.f90 @@ -395,7 +395,7 @@ SUBROUTINE run_mixed_held_deck(deck, root, dt, has_dt, run_time, has_run_time, h INTEGER, INTENT(OUT) :: run_stat CHARACTER(*), INTENT(OUT) :: run_msg - TYPE(CD_AGG_ModuleType) :: aggregate + TYPE(CD_AGG_ModuleType), ALLOCATABLE :: aggregate ! about 37 KB: on the heap, not the stack REAL(wp), ALLOCATABLE :: position(:, :), velocity(:, :), acceleration(:, :), load(:, :) REAL(wp) :: time, first_miss_time, last_miss_time, step_dt INTEGER :: es, n_moving, nstep, step, n_iter, miss_count, consecutive_miss, max_consecutive_miss @@ -447,6 +447,12 @@ SUBROUTINE run_mixed_held_deck(deck, root, dt, has_dt, run_time, has_run_time, h END IF mixed_run: BLOCK + ALLOCATE (aggregate, STAT=ios) + IF (ios /= 0) THEN + run_stat = CD_DECKDRV_SOLVEFAIL + run_msg = 'CableDyn_driver: cannot allocate the mixed aggregate' + EXIT mixed_run + END IF ! The aggregate is caller-driven and cannot refresh standalone wave/current ! OPTIONS. Reject such decks by name instead of silently holding zero fluid. CALL CD_AGG_Init_From_Deck(aggregate, deck, step_dt, es, em, forbid_deck_ambient=.TRUE., & @@ -574,7 +580,7 @@ SUBROUTINE run_mixed_held_deck(deck, root, dt, has_dt, run_time, has_run_time, h END BLOCK mixed_run IF (file_open) CLOSE (unit) - CALL CD_AGG_End(aggregate, es, em) + IF (ALLOCATED(aggregate)) CALL CD_AGG_End(aggregate, es, em) END SUBROUTINE run_mixed_held_deck LOGICAL FUNCTION deck_requests_modes(deck) RESULT(requested) diff --git a/src/CableDyn_CAPI.f90 b/src/CableDyn_CAPI.f90 index 3078ce7..731a6a4 100644 --- a/src/CableDyn_CAPI.f90 +++ b/src/CableDyn_CAPI.f90 @@ -304,7 +304,8 @@ SUBROUTINE init_deck_aggregate(h, deck_path, has_dtm, dtm, err_stat) LOGICAL, INTENT(IN) :: has_dtm REAL(wp), INTENT(IN) :: dtm INTEGER(C_INT), INTENT(OUT) :: err_stat - TYPE(CD_AGG_ModuleType) :: new_agg + ! On the heap: InitDeck runs on the host's thread, whose stack size the host chooses. + TYPE(CD_AGG_ModuleType), ALLOCATABLE :: new_agg INTEGER :: es, nm, stat REAL(wp), ALLOCATABLE :: pos(:, :), vel(:, :), acc(:, :), load(:, :) CHARACTER(CD_C_MSG_LEN) :: em @@ -314,6 +315,11 @@ SUBROUTINE init_deck_aggregate(h, deck_path, has_dtm, dtm, err_stat) 'needs dtM, the fixed step of CableDyn_Step', err_stat) RETURN END IF + ALLOCATE (new_agg, STAT=stat) + IF (stat /= 0) THEN + CALL set_status(h, CD_C_ALLOC_FAIL, 'CableDyn_InitDeck: aggregate allocation failed', err_stat) + RETURN + END IF CALL CD_AGG_Init_From_Deck(new_agg, deck_path, dtm, es, em) IF (es /= CD_AGG_OK) THEN CALL map_agg_status(es, err_stat) diff --git a/src/CableDyn_OpenFAST_Aggregate.f90 b/src/CableDyn_OpenFAST_Aggregate.f90 index 3d6430f..3ec7744 100644 --- a/src/CableDyn_OpenFAST_Aggregate.f90 +++ b/src/CableDyn_OpenFAST_Aggregate.f90 @@ -257,8 +257,10 @@ SUBROUTINE CD_AGG_Init_From_Deck(self, deck_path, dt, ErrStat, ErrMsg, env_gravi ! range_files: the caller writes the range graphs of the LINES flag r (CD_AGG_Range_*) LOGICAL, INTENT(IN), OPTIONAL :: range_files - TYPE(CD_AGG_ModuleType) :: candidate - TYPE(CD_DeckAggregateType) :: parts + ! Both bundles (about 70 KB together) are built on the heap: initialisation runs on the + ! thread of the calling host, whose stack size CableDyn does not choose. + TYPE(CD_AGG_ModuleType), ALLOCATABLE :: candidate + TYPE(CD_DeckAggregateType), ALLOCATABLE :: parts INTEGER :: es, ncp_total, istat, ich ! Static mesh-sequencing diagnostics can report several independently attempted ! hierarchies. Preserve that context through the aggregate boundary so a user sees @@ -280,6 +282,12 @@ SUBROUTINE CD_AGG_Init_From_Deck(self, deck_path, dt, ErrStat, ErrMsg, env_gravi ErrMsg = 'CableDyn_OpenFAST_Aggregate: dt must be positive' RETURN END IF + ALLOCATE (candidate, parts, STAT=istat) + IF (istat /= 0) THEN + ErrStat = CD_AGG_ALLOCFAIL + ErrMsg = 'CableDyn_OpenFAST_Aggregate: cannot allocate the initialisation workspace' + RETURN + END IF CALL CD_Init_Deck_Aggregate(deck_path, dt, parts, es, em, env_gravity=env_gravity, & env_rho_water=env_rho_water, env_wtrdpth=env_wtrdpth, & ptfm_init=ptfm_init, external_fluid=external_fluid, & From ea0f3c92c7f3048ea1a2d57a3c68f8ed5fdb15e7 Mon Sep 17 00:00:00 2001 From: Jae Hoon Seo Date: Fri, 2 Oct 2026 15:08:35 +0900 Subject: [PATCH 3/6] Reserve a 64 MiB stack for GNU Windows executables and test a 1 MiB stack Link every executable of a MinGW-w64 gfortran build with a 64 MiB stack reserve instead of the 2 MiB default, since the stack depth of the OpenBLAS kernels differs between CPUs. A shared library loaded by a host such as python.exe cannot rely on this reserve, so new tests bound the stack the library needs: - stack_c_api: a 2048-element finite-EI cable (12294 DOFs), an EI=0 + finite-EI aggregate, and an EI=0 mooring are initialised, stepped, and evaluated through the C API on a thread with a 1 MiB stack (every platform). - stack_modal and stack_driver_large_deck (GNU Windows): the modal test and the driver relinked with a 1 MiB reserve; the driver runs the 2048-element cable. stack_modal fails with the former modal test. - stack_reserve: the reserve each of these links received. Document the reserve and the stack rule in DEVELOPMENT.md. --- CHANGELOG.md | 5 + CMakeLists.txt | 76 ++++++ DEVELOPMENT.md | 13 + tests/check_stack_reserve.cmake | 50 ++++ .../data/stack_humboldt800_2048_elements.dat | 50 ++++ tests/test_c_api_stack.c | 231 ++++++++++++++++++ 6 files changed, 425 insertions(+) create mode 100644 tests/check_stack_reserve.cmake create mode 100644 tests/data/stack_humboldt800_2048_elements.dat create mode 100644 tests/test_c_api_stack.c diff --git a/CHANGELOG.md b/CHANGELOG.md index c0696ac..16c3eb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,11 @@ All notable changes to CableDyn are recorded in this file. The format follows the displaced mass) no longer depends on the sign of the round-off weight: a line whose total weight is below 1e-12 of its axial stiffness is seeded as a straight, uniformly stretched line. Previously the catenary seed could fail to close on some platforms. +- Windows builds with gfortran: the `modal` test could stop with a stack overflow on some + CPUs, because it kept 1.7 MB of dense matrices on the stack. Large test arrays and the + initialisation workspaces of the coupled aggregate and the C API are now allocated on the + heap, every executable of such a build reserves a 64 MiB stack, and the new `stack` tests + run the library on a 1 MiB thread stack. Results are unchanged. ## [0.1.0] - 2026-10-01 diff --git a/CMakeLists.txt b/CMakeLists.txt index ac8b809..8645280 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -99,6 +99,22 @@ if(CMAKE_Fortran_COMPILER_ID STREQUAL "GNU") $<$:-Wall> $<$:-Wextra>) endif() +# GNU builds on Windows (MinGW-w64): every executable gets a 64 MiB stack reserve instead of +# the 2 MiB MinGW default. With OpenMP, gfortran places fixed-size local arrays on the stack +# (-fopenmp implies -frecursive), and the stack depth of the OpenBLAS kernels varies with the +# CPU, so a small reserve can overflow on one machine and pass on another. A reserve is address +# space, not committed memory. The library itself must run within a small stack, since a host +# such as python.exe sets the stack of the threads that call it: the stack_* tests check that. +# The static IFX release sets its own reserve on the driver. +set(CABLEDYN_GNU_WINDOWS_STACK OFF) +if(MINGW AND CMAKE_Fortran_COMPILER_ID STREQUAL "GNU") + set(CABLEDYN_GNU_WINDOWS_STACK ON) + set(CABLEDYN_EXE_STACK_RESERVE 67108864) + set(CABLEDYN_SMALL_STACK_RESERVE 1048576) + add_link_options( + "$<$,EXECUTABLE>:-Wl,--stack,${CABLEDYN_EXE_STACK_RESERVE}>") +endif() + if(CABLEDYN_ENABLE_COVERAGE AND NOT CMAKE_Fortran_COMPILER_ID STREQUAL "GNU") message(FATAL_ERROR "CABLEDYN_ENABLE_COVERAGE requires GNU Fortran/gcov instrumentation") endif() @@ -1074,6 +1090,25 @@ else() "the portable C API lifecycle gate remains enabled") endif() +# Portable (Win32 or POSIX threads) stack bound of the shared library: each deck is +# initialised, stepped, and evaluated on a thread with a 1 MiB stack (a host program, not +# CableDyn, chooses the stack of the threads that call the library). The decks are a +# 2048-element finite-EI cable (12294 DOFs), an EI=0 + finite-EI aggregate, and an EI=0 +# mooring; a stack need that grew with the model would overflow here. +add_executable(test_c_api_stack tests/test_c_api_stack.c) +target_include_directories(test_c_api_stack PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src") +target_link_libraries(test_c_api_stack PRIVATE $) +add_dependencies(test_c_api_stack cabledyn_shared) +if(NOT WIN32) + target_link_libraries(test_c_api_stack PRIVATE Threads::Threads m) +endif() +add_test(NAME stack_c_api + COMMAND test_c_api_stack 1024 + 0.05 ${CMAKE_CURRENT_SOURCE_DIR}/tests/data/stack_humboldt800_2048_elements.dat + 0.025 ${CMAKE_CURRENT_SOURCE_DIR}/examples/iea15mw_umaine_mixed_cabledyn.dat + 0.1 ${CMAKE_CURRENT_SOURCE_DIR}/examples/iea15mw_volturnus_mooring.dat) +set_tests_properties(stack_c_api PROPERTIES LABELS "c;unit;capi;stack" TIMEOUT 900) + # Portable (Win32 or POSIX threads) gate: concurrent InitDeck of one deck and of decks # sharing a Syrope OWC table in the user's locale, concurrent Step on distinct handles, # the missing-file diagnostic, and the BLAS thread/memory policy of CableDyn_Create. @@ -1630,6 +1665,47 @@ target_link_libraries(test_modal PRIVATE cabledyn_core) add_test(NAME modal COMMAND test_modal) set_tests_properties(modal PROPERTIES LABELS "fortran;unit;driver;static;validation") +# Stack bound (GNU Windows builds): the modal test and the driver relinked with a 1 MiB stack +# reserve, half the MinGW default, instead of the 64 MiB reserve of the other executables. A +# large local array (as the dense matrices test_modal once kept on the stack) or a library +# frame that grows with the model overflows here on every machine. The driver runs a +# 2048-element finite-EI cable. Linux and macOS executables take their main-thread stack +# from the environment (ulimit -s), so these links are Windows-only; test_c_api_stack covers +# the library on every platform. +if(CABLEDYN_GNU_WINDOWS_STACK) + file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/stack_small") + add_executable(test_modal_small_stack tests/test_modal.f90) + target_link_libraries(test_modal_small_stack PRIVATE cabledyn_core) + add_executable(cabledyn_small_stack app/cabledyn.f90) + target_link_libraries(cabledyn_small_stack PRIVATE cabledyn_core) + foreach(_cabledyn_small IN ITEMS test_modal_small_stack cabledyn_small_stack) + # Appended after the 64 MiB directory-wide option; the last --stack wins (checked below). + target_link_options(${_cabledyn_small} PRIVATE "-Wl,--stack,${CABLEDYN_SMALL_STACK_RESERVE}") + endforeach() + add_test(NAME stack_modal COMMAND test_modal_small_stack + WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/stack_small") + set_tests_properties(stack_modal PROPERTIES LABELS "fortran;unit;driver;static;stack") + add_test(NAME stack_driver_large_deck + COMMAND cabledyn_small_stack + "${CMAKE_CURRENT_SOURCE_DIR}/tests/data/stack_humboldt800_2048_elements.dat" + "${CMAKE_BINARY_DIR}/stack_small/stack_driver_large_deck") + set_tests_properties(stack_driver_large_deck PROPERTIES + LABELS "fortran;driver;integration;dynamic;stack" TIMEOUT 900) + if(CMAKE_OBJDUMP) + # The reserve each link actually received: =, '|'-separated. + string(JOIN "|" _cabledyn_stack_checks + "$=${CABLEDYN_EXE_STACK_RESERVE}" + "$=${CABLEDYN_EXE_STACK_RESERVE}" + "$=${CABLEDYN_EXE_STACK_RESERVE}" + "$=${CABLEDYN_SMALL_STACK_RESERVE}" + "$=${CABLEDYN_SMALL_STACK_RESERVE}") + add_test(NAME stack_reserve + COMMAND ${CMAKE_COMMAND} -DOBJDUMP=${CMAKE_OBJDUMP} "-DCHECKS=${_cabledyn_stack_checks}" + -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/check_stack_reserve.cmake) + set_tests_properties(stack_reserve PROPERTIES LABELS "unit;driver;stack") + endif() +endif() + add_executable(test_seabed_friction_aniso tests/test_seabed_friction_aniso.f90) target_link_libraries(test_seabed_friction_aniso PRIVATE cabledyn_core) add_test(NAME seabed_friction_aniso COMMAND test_seabed_friction_aniso) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index db8cc41..1699374 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -44,6 +44,15 @@ The Debug configuration (`-DCMAKE_BUILD_TYPE=Debug`) adds `-fcheck=all conda OpenBLAS through `CONDA_PREFIX`. `cmake --install build --prefix stage` installs the driver, the shared library, and `CableDyn_CAPI.h`. +A Windows build with gfortran links every executable with a 64 MiB stack reserve +(the MinGW default is 2 MiB). With OpenMP enabled, gfortran keeps fixed-size local +arrays on the stack, and the stack depth of the OpenBLAS kernels differs between +CPUs, so a small reserve can pass on one machine and overflow on another. The +shared library cannot rely on this reserve: the host program (for example +`python.exe`, which reserves about 2 MB) sets the stack of the threads that call +it. The `-L stack` tests check the library on a thread with a 1 MiB stack and, on +Windows, the driver and the modal test relinked with a 1 MiB reserve. + | Selection | Tests | |---|---| | `-L fortran` | All Fortran tests | @@ -56,6 +65,7 @@ installs the driver, the shared library, and `CableDyn_CAPI.h`. | `-L external_ref` | IEA-15MW VolturnUS-S mooring comparisons | | `-L cosserat` | Secondary Cosserat path | | `-L python` | Python package, deck-style, and documentation checks | +| `-L stack` | Library, driver, and modal test on a 1 MiB stack | A test program can also be run directly from the build tree (`build/`, or `build\bin\` with the conda toolchain on Windows); it prints `PASS:` or @@ -125,6 +135,9 @@ Fortran lines longer than 120 columns must be split with `&` before formatting. - LAPACK: banded solvers (`DGBSV`, and `DPBTRF`/`DPBTRS` for symmetric positive definite bands); a dense assembly is packed into band storage before the solve. - Warning-clean under `-std=f2018 -Wall -Wextra -fimplicit-none`. +- Keep large data off the stack: a local array or derived-type variable that can + exceed about 64 KB is `ALLOCATABLE`, except on the per-step path, which uses the + preallocated workspaces and does not allocate. ### SPDX headers diff --git a/tests/check_stack_reserve.cmake b/tests/check_stack_reserve.cmake new file mode 100644 index 0000000..885d5aa --- /dev/null +++ b/tests/check_stack_reserve.cmake @@ -0,0 +1,50 @@ +# SPDX-License-Identifier: Apache-2.0 +# Verify the stack reserve written into Windows executables by the GNU link: the 64 MiB +# reserve of the CableDyn executables, and the 1 MiB reserve of the small-stack builds +# that the stack_* tests run (a later --stack option must override the directory-wide one). +# +# CHECKS: '|'-separated = entries. + +foreach(_required OBJDUMP CHECKS) + if(NOT DEFINED ${_required} OR "${${_required}}" STREQUAL "") + message(FATAL_ERROR "check_stack_reserve: missing ${_required}") + endif() +endforeach() + +string(REPLACE "|" ";" _checks "${CHECKS}") +set(_failures "") +foreach(_check IN LISTS _checks) + string(FIND "${_check}" "=" _eq REVERSE) + if(_eq LESS 1) + message(FATAL_ERROR "check_stack_reserve: malformed entry '${_check}'") + endif() + string(SUBSTRING "${_check}" 0 ${_eq} _binary) + math(EXPR _value_at "${_eq} + 1") + string(SUBSTRING "${_check}" ${_value_at} -1 _expected) + if(NOT EXISTS "${_binary}") + message(FATAL_ERROR "check_stack_reserve: missing ${_binary}") + endif() + execute_process( + COMMAND "${OBJDUMP}" -p "${_binary}" + RESULT_VARIABLE _rc + OUTPUT_VARIABLE _out + ERROR_VARIABLE _err) + if(NOT _rc EQUAL 0) + message(FATAL_ERROR "check_stack_reserve: ${OBJDUMP} -p ${_binary} failed (${_rc})\n${_err}") + endif() + if(NOT _out MATCHES "SizeOfStackReserve[ \t]+([0-9a-fA-F]+)") + message(FATAL_ERROR "check_stack_reserve: no SizeOfStackReserve read from ${_binary}") + endif() + math(EXPR _reserve "0x${CMAKE_MATCH_1}") + get_filename_component(_name "${_binary}" NAME) + if(NOT _reserve EQUAL _expected) + string(APPEND _failures " ${_name}: stack reserve ${_reserve} bytes, expected ${_expected}\n") + else() + message(STATUS "${_name}: stack reserve ${_reserve} bytes") + endif() +endforeach() + +if(_failures) + message(FATAL_ERROR "check_stack_reserve: unexpected stack reserve\n${_failures}") +endif() +message(STATUS "PASS: every checked executable has the expected stack reserve") diff --git a/tests/data/stack_humboldt800_2048_elements.dat b/tests/data/stack_humboldt800_2048_elements.dat new file mode 100644 index 0000000..cf343cf --- /dev/null +++ b/tests/data/stack_humboldt800_2048_elements.dat @@ -0,0 +1,50 @@ +--------------------- CableDyn Input File ------------------------------------ +Stack-bound deck: the Lozon humboldt800 lazy-wave cable on 2048 Hermite elements (12294 DOFs) +-- A large finite-EI model in still water. The small-stack tests run it on a 1 MiB stack +-- to show that the library stack need does not grow with the model size. +--------------------- LINE TYPES --------------------------------------------- +TypeName Diam MassDenInAir EA BA EI Cd_n Cd_t Ca_n Ca_t +(-) (m) (kg/m) (N) (-) (N-m2) (-) (-) (-) (-) +bare 0.160 36.70 4.69e8 0.0 1.99e4 1.2 0.1 1.0 0.0 +buoy 0.290 59.17 4.69e8 0.0 1.99e4 1.2 0.1 1.0 0.0 +--------------------- POINTS ------------------------------------------------ +ID Type X Y Z +(-) (-) (m) (m) (m) +1 Coupled 5.0 0.0 -14.0 +2 Fixed 805.0 0.0 -800.0 +--------------------- LINES ------------------------------------------------- +ID NodeA NodeB Outputs +(-) (-) (-) (-) +1 1 2 - +--------------------- SECTIONS ---------------------------------------------- +LineID LineType Length NumSegs +(-) (-) (m) (-) +1 bare 372.449 512 +1 buoy 400.000 512 +1 bare 597.981 1024 +--------------------- OPTIONS ----------------------------------------------- +9.80665 g - Gravitational acceleration (m/s^2) [default: 9.80665] +1025.0 rhoW - Water density (kg/m^3) [default: 1025] +800.0 WtrDpth - Water depth for the flat seabed (m) [default: absent standalone; host-owned OpenFAST] +1.0e5 kBot - Seabed penalty stiffness base (Pa/m) [default: 1.0e5] +1.0e4 cBot - Seabed normal damping base (Pa-s/m) [default: 1.0e4] +0.4 rhoInf - Generalised-alpha spectral radius {0<=rhoInf<=1} [default: 0.4] +False modified_newton - Tangent-reuse mode {False=full Newton; True=guarded reuse} [default: False] +0.05 dtM - CableDyn internal time step (s) [default: omitted standalone; 0.1 s target OpenFAST] +0.2 TMax - Standalone simulation duration (s); 0 selects static initialisation only +False adaptive_mesh - Automatic finite-EI refinement {False; True} [default: False] +True tensile_safety - Refine and reject local axial compression in this tensile cable +dynamic_solver 1.0e-8 1.0e-14 30 12 - Relative/absolute tolerance, max iterations, backtracks [defaults] +none frictionMu - Seabed friction coefficient {none; value>=0} [default: none] +none current - Current model and velocity (m/s) {none; uniform; profile} [default: none] +none waves - Wave model, height, period, direction (m, s, deg) {none; airy; jonswap} +0 WaterKin - Water-kinematics source {0/none; file; SEASTATE coupled only} [default: no policy] +-- Complete option/default reference: cabledyn_options_reference.dat and doc/options.rst. +--------------------- OUTPUTS ----------------------------------------------- +"FairTen1" +"AnchTen1" +"FairIncl1" +"AnchIncl1" +"Curv1N250" +"BendMom1N250" +--------------------- need this line ---------------------------------------- diff --git a/tests/test_c_api_stack.c b/tests/test_c_api_stack.c new file mode 100644 index 0000000..d31e8c3 --- /dev/null +++ b/tests/test_c_api_stack.c @@ -0,0 +1,231 @@ +/* File: tests/test_c_api_stack.c + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) 2026 Jae Hoon Seo, SMI Lab, Inha University + * + * Bounded-stack gate for the shared library (Win32 threads or POSIX threads). + * + * Usage: test_c_api_stack
[
...] + * + * A host such as python.exe calls the library on its own threads, with the stack + * size the host chose (python.exe reserves about 2 MB; the Windows default for an + * MSVC-linked program is 1 MB), so the library must not rely on the stack reserve of + * a CableDyn executable. For every deck this program starts one thread whose stack + * is KiB and, on that thread, creates a handle, initialises the deck, + * steps it with the coupled points held (coupling step
s), evaluates the coupled + * loads, and closes the handle. A stack overflow ends the process (SIGSEGV on POSIX + * systems, STATUS_STACK_OVERFLOW on Windows), which fails the test. On Windows the + * deepest stack page the thread touched is also reported. + */ +#include "CableDyn_CAPI.h" + +#include +#include +#include +#include + +#if defined(_WIN32) +#include +#include +#else +#include +#include +#endif + +enum { N_STEP = 3 }; + +struct deck_task { + const char *deck; + double dt; + int status; /* 0 = pass */ + int ndof; + size_t peak_bytes; /* deepest committed stack (Windows), 0 elsewhere */ + char message[1100]; +}; + +static size_t stack_in_use(void) +{ +#if defined(_WIN32) + /* The stack is committed one guard page at a time from the top of its reservation + * down, and never decommitted: the pages still only reserved at the bottom of the + * reservation were never touched, and the rest is the thread's high-water mark. */ + MEMORY_BASIC_INFORMATION here, bottom; + volatile char marker = 0; + char *top; + + if (VirtualQuery((const void *)&marker, &here, sizeof here) == 0) return 0; + if (VirtualQuery(here.AllocationBase, &bottom, sizeof bottom) == 0) return 0; + top = (char *)here.BaseAddress + here.RegionSize; + if (bottom.State != MEM_RESERVE) return (size_t)(top - (char *)here.AllocationBase); + return (size_t)(top - ((char *)bottom.BaseAddress + bottom.RegionSize)); +#else + return 0; +#endif +} + +static void fail(struct deck_task *t, void *m, const char *what) +{ + char last[1025] = ""; + if (m != NULL) CableDyn_GetLastError(m, last, (int)sizeof last); + snprintf(t->message, sizeof t->message, "%s%s%s", what, last[0] ? ": " : "", last); + t->status = 1; +} + +static void run_deck(struct deck_task *t) +{ + void *m = NULL; + int err = -1, ndof, n_iter = 0, i, step; + bool converged = false, stalled = true; + double *q = NULL, *v = NULL, *a = NULL, *loads = NULL; + + t->status = 0; + CableDyn_Create(&m, &err); + if (err != CD_C_OK || m == NULL) { + fail(t, NULL, "create failed"); + return; + } + CableDyn_InitDeck(m, t->deck, (int)strlen(t->deck), &err); + if (err != CD_C_OK) { + fail(t, m, "deck initialisation failed"); + goto done; + } + ndof = CableDyn_NCoupledDOF(m, &err); + t->ndof = ndof; + if (err != CD_C_OK || ndof <= 0) { + fail(t, m, "coupled DOF query failed"); + goto done; + } + q = calloc((size_t)ndof, sizeof *q); + v = calloc((size_t)ndof, sizeof *v); + a = calloc((size_t)ndof, sizeof *a); + loads = calloc((size_t)ndof, sizeof *loads); + if (!q || !v || !a || !loads) { + fail(t, NULL, "allocation failed"); + goto done; + } + CableDyn_GetCoupledMotion(m, q, v, a, ndof, &err); + if (err != CD_C_OK) { + fail(t, m, "coupled motion query failed"); + goto done; + } + for (step = 0; step < N_STEP; ++step) { + CableDyn_Step(m, t->dt, q, v, a, ndof, &converged, &stalled, &n_iter, &err); + if (err != CD_C_OK || !converged || stalled) { + fail(t, m, "held-point step did not converge"); + goto done; + } + } + CableDyn_CalcOutput(m, loads, ndof, &err); + if (err != CD_C_OK) { + fail(t, m, "output failed"); + goto done; + } + for (i = 0; i < ndof; ++i) { + if (!isfinite(loads[i])) { + fail(t, NULL, "non-finite coupled load"); + goto done; + } + } + +done: + free(q); + free(v); + free(a); + free(loads); + CableDyn_Close(&m, &err); + if (t->status == 0 && err != CD_C_OK) fail(t, NULL, "close failed"); + t->peak_bytes = stack_in_use(); +} + +#if defined(_WIN32) +static unsigned __stdcall deck_thread(void *arg) +{ + run_deck((struct deck_task *)arg); + return 0; +} + +static int run_on_small_stack(struct deck_task *t, size_t stack_bytes) +{ + /* A reservation, as the linker's stack reserve of an executable, not a commit. */ + HANDLE th = (HANDLE)_beginthreadex(NULL, (unsigned)stack_bytes, deck_thread, t, + STACK_SIZE_PARAM_IS_A_RESERVATION, NULL); + if (th == NULL) return 1; + WaitForSingleObject(th, INFINITE); + CloseHandle(th); + return 0; +} +#else +static void *deck_thread(void *arg) +{ + run_deck((struct deck_task *)arg); + return NULL; +} + +static int run_on_small_stack(struct deck_task *t, size_t stack_bytes) +{ + pthread_attr_t attr; + pthread_t th; + int rc; + +#if defined(PTHREAD_STACK_MIN) + if (stack_bytes < (size_t)PTHREAD_STACK_MIN) stack_bytes = (size_t)PTHREAD_STACK_MIN; +#endif + if (pthread_attr_init(&attr) != 0) return 1; + rc = pthread_attr_setstacksize(&attr, stack_bytes); + if (rc == 0) rc = pthread_create(&th, &attr, deck_thread, t); + pthread_attr_destroy(&attr); + if (rc != 0) return 1; + return pthread_join(th, NULL) != 0; +} +#endif + +int main(int argc, char **argv) +{ + long stack_kib; + int i, failures = 0; + struct deck_task task; + + if (argc < 4 || (argc - 2) % 2 != 0) { + fputs("usage: test_c_api_stack
[
...]\n", + stderr); + return 2; + } + stack_kib = strtol(argv[1], NULL, 10); + if (stack_kib <= 0) { + fputs("FAIL: stack size must be a positive number of KiB\n", stderr); + return 2; + } + for (i = 3; i < argc; i += 2) { + memset(&task, 0, sizeof task); + task.deck = argv[i]; + task.dt = strtod(argv[i - 1], NULL); + if (!(task.dt > 0.0)) { + fprintf(stderr, "FAIL: %s: the coupling step must be positive\n", argv[i]); + ++failures; + continue; + } + if (run_on_small_stack(&task, (size_t)stack_kib * 1024u) != 0) { + fprintf(stderr, "FAIL: %s: could not start a thread with a %ld KiB stack\n", + argv[i], stack_kib); + ++failures; + continue; + } + if (task.status != 0) { + fprintf(stderr, "FAIL: %s: %s\n", argv[i], task.message); + ++failures; + continue; + } + if (task.peak_bytes > 0) { + printf("%s: %d coupled DOFs, %d steps, deepest stack %zu of %ld KiB\n", argv[i], + task.ndof, N_STEP, task.peak_bytes / 1024u, stack_kib); + } else { + printf("%s: %d coupled DOFs, %d steps on a %ld KiB stack\n", argv[i], task.ndof, + N_STEP, stack_kib); + } + } + if (failures != 0) { + fprintf(stderr, "test_c_api_stack: %d deck(s) failed\n", failures); + return 1; + } + puts("PASS: every deck ran within the thread stack"); + return 0; +} From f3f7b088a6cfbde923a0b784423eb98b7a3c22f2 Mon Sep 17 00:00:00 2001 From: Jae Hoon Seo Date: Fri, 2 Oct 2026 16:18:28 +0900 Subject: [PATCH 4/6] Read the stack reserve from the PE header and validate the stack test inputs The stack_reserve test needed objdump and was silently not registered when CMake did not find it. It now reads SizeOfStackReserve from the PE optional header with CMake alone, and stack_reserve_mismatch checks that the reader reports a reserve that differs from the expected one. test_c_api_stack rejects malformed stack sizes and coupling steps, rounds a POSIX thread stack up to whole pages, and fails when the wait for the Win32 thread fails. --- CMakeLists.txt | 32 ++++++++++------- DEVELOPMENT.md | 5 +-- tests/check_stack_reserve.cmake | 64 +++++++++++++++++++++++++-------- tests/test_c_api_stack.c | 21 +++++++---- 4 files changed, 85 insertions(+), 37 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8645280..c084f07 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1691,19 +1691,25 @@ if(CABLEDYN_GNU_WINDOWS_STACK) "${CMAKE_BINARY_DIR}/stack_small/stack_driver_large_deck") set_tests_properties(stack_driver_large_deck PROPERTIES LABELS "fortran;driver;integration;dynamic;stack" TIMEOUT 900) - if(CMAKE_OBJDUMP) - # The reserve each link actually received: =, '|'-separated. - string(JOIN "|" _cabledyn_stack_checks - "$=${CABLEDYN_EXE_STACK_RESERVE}" - "$=${CABLEDYN_EXE_STACK_RESERVE}" - "$=${CABLEDYN_EXE_STACK_RESERVE}" - "$=${CABLEDYN_SMALL_STACK_RESERVE}" - "$=${CABLEDYN_SMALL_STACK_RESERVE}") - add_test(NAME stack_reserve - COMMAND ${CMAKE_COMMAND} -DOBJDUMP=${CMAKE_OBJDUMP} "-DCHECKS=${_cabledyn_stack_checks}" - -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/check_stack_reserve.cmake) - set_tests_properties(stack_reserve PROPERTIES LABELS "unit;driver;stack") - endif() + # The reserve each link actually received, read from the PE headers: =. + string(JOIN "|" _cabledyn_stack_checks + "$=${CABLEDYN_EXE_STACK_RESERVE}" + "$=${CABLEDYN_EXE_STACK_RESERVE}" + "$=${CABLEDYN_EXE_STACK_RESERVE}" + "$=${CABLEDYN_SMALL_STACK_RESERVE}" + "$=${CABLEDYN_SMALL_STACK_RESERVE}") + add_test(NAME stack_reserve + COMMAND ${CMAKE_COMMAND} "-DCHECKS=${_cabledyn_stack_checks}" + -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/check_stack_reserve.cmake) + set_tests_properties(stack_reserve PROPERTIES LABELS "unit;driver;stack") + # The check itself must see a reserve that differs from the expected one. + add_test(NAME stack_reserve_mismatch + COMMAND ${CMAKE_COMMAND} + "-DCHECKS=$=${CABLEDYN_EXE_STACK_RESERVE}" + -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/check_stack_reserve.cmake) + set_tests_properties(stack_reserve_mismatch PROPERTIES LABELS "unit;driver;stack" + PASS_REGULAR_EXPRESSION + "stack reserve ${CABLEDYN_SMALL_STACK_RESERVE} bytes, expected ${CABLEDYN_EXE_STACK_RESERVE}") endif() add_executable(test_seabed_friction_aniso tests/test_seabed_friction_aniso.f90) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 1699374..498ab6f 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -50,8 +50,9 @@ arrays on the stack, and the stack depth of the OpenBLAS kernels differs between CPUs, so a small reserve can pass on one machine and overflow on another. The shared library cannot rely on this reserve: the host program (for example `python.exe`, which reserves about 2 MB) sets the stack of the threads that call -it. The `-L stack` tests check the library on a thread with a 1 MiB stack and, on -Windows, the driver and the modal test relinked with a 1 MiB reserve. +it. The `-L stack` tests check the library on a thread with a 1 MiB stack and, in +a Windows gfortran build, the driver and the modal test relinked with a 1 MiB +reserve and the reserve written into each executable. | Selection | Tests | |---|---| diff --git a/tests/check_stack_reserve.cmake b/tests/check_stack_reserve.cmake index 885d5aa..6ad5d82 100644 --- a/tests/check_stack_reserve.cmake +++ b/tests/check_stack_reserve.cmake @@ -2,14 +2,32 @@ # Verify the stack reserve written into Windows executables by the GNU link: the 64 MiB # reserve of the CableDyn executables, and the 1 MiB reserve of the small-stack builds # that the stack_* tests run (a later --stack option must override the directory-wide one). +# The reserve is read from the PE optional header itself, so no binutils are needed. # # CHECKS: '|'-separated = entries. -foreach(_required OBJDUMP CHECKS) - if(NOT DEFINED ${_required} OR "${${_required}}" STREQUAL "") - message(FATAL_ERROR "check_stack_reserve: missing ${_required}") +if(NOT DEFINED CHECKS OR "${CHECKS}" STREQUAL "") + message(FATAL_ERROR "check_stack_reserve: missing CHECKS") +endif() + +# Little-endian unsigned integer of bytes at of . +function(_read_le file offset nbytes out) + file(READ "${file}" _hex OFFSET ${offset} LIMIT ${nbytes} HEX) + string(LENGTH "${_hex}" _len) + math(EXPR _want "2 * ${nbytes}") + if(NOT _len EQUAL _want) + message(FATAL_ERROR "check_stack_reserve: ${file} is too short for a PE header") endif() -endforeach() + set(_value "") + math(EXPR _last "${nbytes} - 1") + foreach(_byte RANGE ${_last} 0 -1) + math(EXPR _at "2 * ${_byte}") + string(SUBSTRING "${_hex}" ${_at} 2 _pair) + string(APPEND _value "${_pair}") + endforeach() + math(EXPR _value "0x${_value}") + set(${out} ${_value} PARENT_SCOPE) +endfunction() string(REPLACE "|" ";" _checks "${CHECKS}") set(_failures "") @@ -21,22 +39,38 @@ foreach(_check IN LISTS _checks) string(SUBSTRING "${_check}" 0 ${_eq} _binary) math(EXPR _value_at "${_eq} + 1") string(SUBSTRING "${_check}" ${_value_at} -1 _expected) + if(NOT _expected MATCHES "^[0-9]+$") + message(FATAL_ERROR "check_stack_reserve: malformed expected reserve in '${_check}'") + endif() if(NOT EXISTS "${_binary}") message(FATAL_ERROR "check_stack_reserve: missing ${_binary}") endif() - execute_process( - COMMAND "${OBJDUMP}" -p "${_binary}" - RESULT_VARIABLE _rc - OUTPUT_VARIABLE _out - ERROR_VARIABLE _err) - if(NOT _rc EQUAL 0) - message(FATAL_ERROR "check_stack_reserve: ${OBJDUMP} -p ${_binary} failed (${_rc})\n${_err}") + get_filename_component(_name "${_binary}" NAME) + + # DOS header 'MZ', e_lfanew at 0x3C, 'PE\0\0' signature, 20-byte COFF header, then the + # optional header: magic 0x10B (PE32, 4-byte reserve) or 0x20B (PE32+, 8-byte reserve), + # SizeOfStackReserve at offset 72 in both. + _read_le("${_binary}" 0 2 _mz) + if(NOT _mz EQUAL 23117) + message(FATAL_ERROR "check_stack_reserve: ${_name} is not a PE executable") + endif() + _read_le("${_binary}" 60 4 _pe) + _read_le("${_binary}" ${_pe} 4 _signature) + if(NOT _signature EQUAL 17744) + message(FATAL_ERROR "check_stack_reserve: ${_name} has no PE signature") endif() - if(NOT _out MATCHES "SizeOfStackReserve[ \t]+([0-9a-fA-F]+)") - message(FATAL_ERROR "check_stack_reserve: no SizeOfStackReserve read from ${_binary}") + math(EXPR _optional "${_pe} + 24") + _read_le("${_binary}" ${_optional} 2 _magic) + if(_magic EQUAL 523) + set(_width 8) + elseif(_magic EQUAL 267) + set(_width 4) + else() + message(FATAL_ERROR "check_stack_reserve: ${_name} has an unknown optional header (${_magic})") endif() - math(EXPR _reserve "0x${CMAKE_MATCH_1}") - get_filename_component(_name "${_binary}" NAME) + math(EXPR _at "${_optional} + 72") + _read_le("${_binary}" ${_at} ${_width} _reserve) + if(NOT _reserve EQUAL _expected) string(APPEND _failures " ${_name}: stack reserve ${_reserve} bytes, expected ${_expected}\n") else() diff --git a/tests/test_c_api_stack.c b/tests/test_c_api_stack.c index d31e8c3..65184a1 100644 --- a/tests/test_c_api_stack.c +++ b/tests/test_c_api_stack.c @@ -29,6 +29,7 @@ #else #include #include +#include #endif enum { N_STEP = 3 }; @@ -148,10 +149,12 @@ static int run_on_small_stack(struct deck_task *t, size_t stack_bytes) /* A reservation, as the linker's stack reserve of an executable, not a commit. */ HANDLE th = (HANDLE)_beginthreadex(NULL, (unsigned)stack_bytes, deck_thread, t, STACK_SIZE_PARAM_IS_A_RESERVATION, NULL); + DWORD waited; + if (th == NULL) return 1; - WaitForSingleObject(th, INFINITE); + waited = WaitForSingleObject(th, INFINITE); CloseHandle(th); - return 0; + return waited != WAIT_OBJECT_0; } #else static void *deck_thread(void *arg) @@ -164,11 +167,14 @@ static int run_on_small_stack(struct deck_task *t, size_t stack_bytes) { pthread_attr_t attr; pthread_t th; + long page = sysconf(_SC_PAGESIZE); int rc; #if defined(PTHREAD_STACK_MIN) if (stack_bytes < (size_t)PTHREAD_STACK_MIN) stack_bytes = (size_t)PTHREAD_STACK_MIN; #endif + /* Some systems (macOS) accept only a whole number of pages. */ + if (page > 0) stack_bytes = (stack_bytes + (size_t)page - 1) / (size_t)page * (size_t)page; if (pthread_attr_init(&attr) != 0) return 1; rc = pthread_attr_setstacksize(&attr, stack_bytes); if (rc == 0) rc = pthread_create(&th, &attr, deck_thread, t); @@ -181,6 +187,7 @@ static int run_on_small_stack(struct deck_task *t, size_t stack_bytes) int main(int argc, char **argv) { long stack_kib; + char *end; int i, failures = 0; struct deck_task task; @@ -189,16 +196,16 @@ int main(int argc, char **argv) stderr); return 2; } - stack_kib = strtol(argv[1], NULL, 10); - if (stack_kib <= 0) { - fputs("FAIL: stack size must be a positive number of KiB\n", stderr); + stack_kib = strtol(argv[1], &end, 10); + if (end == argv[1] || *end != '\0' || stack_kib <= 0 || stack_kib > 1048576L) { + fputs("FAIL: the stack size must be a whole number of KiB in [1, 1048576]\n", stderr); return 2; } for (i = 3; i < argc; i += 2) { memset(&task, 0, sizeof task); task.deck = argv[i]; - task.dt = strtod(argv[i - 1], NULL); - if (!(task.dt > 0.0)) { + task.dt = strtod(argv[i - 1], &end); + if (end == argv[i - 1] || *end != '\0' || !(task.dt > 0.0) || !isfinite(task.dt)) { fprintf(stderr, "FAIL: %s: the coupling step must be positive\n", argv[i]); ++failures; continue; From 2059aa11a9b608a8ef9e770425cdf8e47f644256 Mon Sep 17 00:00:00 2001 From: Jae Hoon Seo Date: Fri, 2 Oct 2026 16:18:35 +0900 Subject: [PATCH 5/6] Compile the OpenFAST adapter with heap arrays in the Visual Studio release The Visual Studio project that builds the CableDyn adapter into the static openfast.exe set no heap-arrays option, so with IFX its run-time-sized arrays and temporaries went on the openfast.exe stack. Every configuration now sets HeapArrays="1024", which Visual Studio passes as /heap-arrays1024, the same threshold as the /heap-arrays:1024 of the static CMake build of the core. --- CHANGELOG.md | 3 +++ integration/openfast/README.md | 4 +++- integration/openfast/vs-build/CableDyn.vfproj.in | 16 ++++++++-------- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 16c3eb7..7ace2b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,9 @@ All notable changes to CableDyn are recorded in this file. The format follows initialisation workspaces of the coupled aggregate and the C API are now allocated on the heap, every executable of such a build reserves a 64 MiB stack, and the new `stack` tests run the library on a 1 MiB thread stack. Results are unchanged. +- The static Windows `openfast.exe` compiles the CableDyn adapter with `/heap-arrays:1024`, + like the CableDyn core, so its run-time-sized arrays no longer use the + `openfast.exe` stack. ## [0.1.0] - 2026-10-01 diff --git a/integration/openfast/README.md b/integration/openfast/README.md index 6b6f8c4..8a09817 100644 --- a/integration/openfast/README.md +++ b/integration/openfast/README.md @@ -45,7 +45,9 @@ the system code page cannot spell; the static release build embeds it. For the static Windows release, pass a clean, unpatched checkout to `release/build_static_windows.ps1`; it applies the series itself (see -[Installation](../../doc/installation.rst)). +[Installation](../../doc/installation.rst)). Its Visual Studio project compiles the CableDyn +adapter with `/heap-arrays:1024`, as the static CMake build compiles the CableDyn core, so arrays +whose size is known only at run time are placed on the heap rather than on the `openfast.exe` stack. ## License of modified files diff --git a/integration/openfast/vs-build/CableDyn.vfproj.in b/integration/openfast/vs-build/CableDyn.vfproj.in index 7f2b0e2..2b3b99c 100644 --- a/integration/openfast/vs-build/CableDyn.vfproj.in +++ b/integration/openfast/vs-build/CableDyn.vfproj.in @@ -5,35 +5,35 @@ - + - + - + - + - + - + - + - + From a8b522f58052f45a7897029235cf5c0b03a2b48c Mon Sep 17 00:00:00 2001 From: Jae Hoon Seo Date: Fri, 2 Oct 2026 16:18:40 +0900 Subject: [PATCH 6/6] Keep gfortran 16 builds free of -Wuninitialized warnings gfortran 16.2 reported 29 -Wuninitialized warnings that 15.2 does not. Each names the bounds, offset, or span of an unallocated allocatable component of a default-initialised local (CD_HFMF_ModuleType or CD_HermiteCableDynType): the optimiser copies these never-set descriptor fields when it rewrites the default initialisation, and the program never reads them. These locals, in the still-water friction reference of the recursive Hermite cable builder and in six tests, are now ALLOCATABLE, which leaves results unchanged. --- CHANGELOG.md | 2 ++ DEVELOPMENT.md | 6 +++++- src/CableDyn_DeckDriver.f90 | 10 ++++++++-- tests/test_deck_hermite_cable.f90 | 16 +++++++++++----- tests/test_hermite_cable_dynamic.f90 | 3 ++- tests/test_hermite_cable_static.f90 | 3 ++- tests/test_hermite_drag_dynamics.f90 | 6 ++++-- tests/test_hermite_irregular_waves.f90 | 3 ++- tests/test_hermite_torsion_dynamic.f90 | 6 ++++-- 9 files changed, 40 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ace2b7..26b0e78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,8 @@ All notable changes to CableDyn are recorded in this file. The format follows - The static Windows `openfast.exe` compiles the CableDyn adapter with `/heap-arrays:1024`, like the CableDyn core, so its run-time-sized arrays no longer use the `openfast.exe` stack. +- Builds with gfortran 16 are free of `-Wuninitialized` warnings; the reported values (the + bounds of unallocated components) were never read. ## [0.1.0] - 2026-10-01 diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 498ab6f..5d3d7f1 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -135,7 +135,11 @@ Fortran lines longer than 120 columns must be split with `&` before formatting. `ErrMsg (CHARACTER(*), INTENT(OUT))`. - LAPACK: banded solvers (`DGBSV`, and `DPBTRF`/`DPBTRS` for symmetric positive definite bands); a dense assembly is packed into band storage before the solve. -- Warning-clean under `-std=f2018 -Wall -Wextra -fimplicit-none`. +- Warning-clean under `-std=f2018 -Wall -Wextra -fimplicit-none` with gfortran 15 + and 16. gfortran 16 can report `-Wuninitialized` for a default-initialised local + derived-type variable with allocatable components: the optimiser copies the + unset bounds of its unallocated arrays, which the program never reads. Such a + local is `ALLOCATABLE` where this occurs. - Keep large data off the stack: a local array or derived-type variable that can exceed about 64 KB is `ALLOCATABLE`, except on the per-step path, which uses the preallocated workspaces and does not allocate. diff --git a/src/CableDyn_DeckDriver.f90 b/src/CableDyn_DeckDriver.f90 index e944de0..4dc02be 100644 --- a/src/CableDyn_DeckDriver.f90 +++ b/src/CableDyn_DeckDriver.f90 @@ -3305,9 +3305,15 @@ SUBROUTINE still_water_reference(esr, emr) INTEGER, INTENT(OUT) :: esr CHARACTER(*), INTENT(OUT) :: emr TYPE(DeckOptions) :: opts_sw - TYPE(CD_HFMF_ModuleType) :: sw_cable - INTEGER :: sw_node, k, nsw + ! ALLOCATABLE: off the stack of this recursive builder (see DEVELOPMENT.md) + TYPE(CD_HFMF_ModuleType), ALLOCATABLE :: sw_cable + INTEGER :: sw_node, k, nsw, sw_stat CHARACTER(2048) :: sw_note + ALLOCATE (sw_cable, STAT=sw_stat) + IF (sw_stat /= 0) THEN + CALL fail(esr, emr, 'still-water friction reference allocation failed') + RETURN + END IF opts_sw = opts opts_sw%has_current = .FALSE. opts_sw%has_current_profile = .FALSE. diff --git a/tests/test_deck_hermite_cable.f90 b/tests/test_deck_hermite_cable.f90 index ed69107..a0ce238 100644 --- a/tests/test_deck_hermite_cable.f90 +++ b/tests/test_deck_hermite_cable.f90 @@ -49,7 +49,7 @@ SUBROUTINE require(cond, msg) END SUBROUTINE require SUBROUTINE case_build_and_drive() - TYPE(CD_HFMF_ModuleType) :: cable + TYPE(CD_HFMF_ModuleType), ALLOCATABLE :: cable ! see DEVELOPMENT.md (gfortran 16 -Wuninitialized) INTEGER :: cn, es, s, cnr, nn CHARACTER(256) :: em REAL(wp) :: r0(3), yk(3), u_pos(3), u_vel(3), u_acc(3), t, zh, vh, ah, ymin, ymax, ymag @@ -59,6 +59,7 @@ SUBROUTINE case_build_and_drive() INTEGER, PARAMETER :: NSTEP = 220, NSETTLE = 140 LOGICAL :: stepped_ok + ALLOCATE (cable) CALL write_cable_deck('hcab_deck_hcable.dat') CALL CD_Init_Deck_HermiteCable('hcab_deck_hcable.dat', DT, cable, cn, es, em) CALL require(es == CD_DECKDRV_OK, 'deck -> Hermite cable init: '//TRIM(em)) @@ -127,7 +128,7 @@ SUBROUTINE case_adaptive_mesh_refines() !! arrays stay aligned with the refined solution); (c) the coupled node still tracks the !! refined fairlead; (d) the refined cable steps stably with smooth, bounded curvature (the !! prolongation produced a valid finer equilibrium, not a kink). - TYPE(CD_HFMF_ModuleType) :: plain, adapt + TYPE(CD_HFMF_ModuleType), ALLOCATABLE :: plain, adapt ! see DEVELOPMENT.md (gfortran 16 -Wuninitialized) INTEGER :: cn, es, s, nn_plain, nn_adapt, cnr CHARACTER(256) :: em REAL(wp) :: r0(3), u_pos(3), u_vel(3), u_acc(3), t, zh, vh, ah @@ -137,6 +138,7 @@ SUBROUTINE case_adaptive_mesh_refines() INTEGER, PARAMETER :: NSTEP = 30 LOGICAL :: stepped_ok + ALLOCATE (plain, adapt) CALL write_cable_deck('hcab_deck_hc_plain.dat') CALL CD_Init_Deck_HermiteCable('hcab_deck_hc_plain.dat', DT, plain, cn, es, em) CALL require(es == CD_DECKDRV_OK, 'adaptive-mesh: plain deck init: '//TRIM(em)) @@ -199,10 +201,11 @@ SUBROUTINE case_build_no_dtm() !! deck's dtM. The same lazy-wave deck WITHOUT dtM/TMax must still build and take a step. (A !! standalone finite-EI deck is structurally unaffected: its End A must be Coupled/Vessel/Body, !! so the standalone marching contract still demands a motionFile -- and hence dtM.) - TYPE(CD_HFMF_ModuleType) :: cable + TYPE(CD_HFMF_ModuleType), ALLOCATABLE :: cable ! see DEVELOPMENT.md (gfortran 16 -Wuninitialized) INTEGER :: cn, es CHARACTER(256) :: em REAL(wp) :: u_pos(3) + ALLOCATE (cable) CALL write_cable_deck('hcab_deck_nodtm.dat', omit_time=.TRUE.) CALL CD_Init_Deck_HermiteCable('hcab_deck_nodtm.dat', 0.05_wp, cable, cn, es, em) CALL require(es == CD_DECKDRV_OK, 'caller-driven finite-EI deck builds WITHOUT dtM/TMax: '//TRIM(em)) @@ -286,12 +289,14 @@ SUBROUTINE case_end_connections() !! Prove the public End-A row maps to the internal final node, the finite spring !! participates in the static seed and dynamic march, and its support moment is !! returned. An explicitly Pinned row must remain bit-identical to omission. - TYPE(CD_HFMF_ModuleType) :: omitted, pinned, finite, rigid, stock + ! ALLOCATABLE: see DEVELOPMENT.md (gfortran 16 -Wuninitialized) + TYPE(CD_HFMF_ModuleType), ALLOCATABLE :: omitted, pinned, finite, rigid, stock INTEGER :: cn_o, cn_p, cn_f, cn_r, cn_s, es, s, i CHARACTER(256) :: em REAL(wp) :: pos(3), vel(3), acc(3), force(3), moment(3), dcm(3, 3), angle REAL(wp) :: d0_saved(3, 2), ymax, tangent(3), tangent_norm + ALLOCATE (omitted, pinned, finite, rigid, stock) cn_o = 0; cn_p = 0; cn_f = 0; cn_r = 0; cn_s = 0 CALL write_cable_deck('hcab_deck_endconn_omitted.dat') @@ -431,7 +436,7 @@ SUBROUTINE case_azimuthal_frame_objectivity() !! fairlead load, and (rotation-invariant) curvature -- at every compared step. A !! frame error anywhere (a missed rotation, a wrong transpose, an over-frozen DOF) !! breaks this equality at leading order. - TYPE(CD_HFMF_ModuleType) :: cab_p, cab_r + TYPE(CD_HFMF_ModuleType), ALLOCATABLE :: cab_p, cab_r ! see DEVELOPMENT.md (gfortran 16 -Wuninitialized) INTEGER :: cn_p, cn_r, es, s, nn CHARACTER(256) :: em REAL(wp), PARAMETER :: DT = 0.05_wp, AMP = 1.0_wp, PER = 12.0_wp @@ -443,6 +448,7 @@ SUBROUTINE case_azimuthal_frame_objectivity() REAL(wp) :: up(3), uv(3), ua(3), yp(3), yr(3), yp_rot(3), lerr, lref REAL(wp), ALLOCATABLE :: curv_p(:), curv_r(:) + ALLOCATE (cab_p, cab_r) c = COS(THETA); sth = SIN(THETA) a0 = [0.0_wp, 0.0_wp, -56.0_wp] f0 = [90.0_wp, 0.0_wp, -14.0_wp] diff --git a/tests/test_hermite_cable_dynamic.f90 b/tests/test_hermite_cable_dynamic.f90 index 3d4ebf4..6d59e50 100644 --- a/tests/test_hermite_cable_dynamic.f90 +++ b/tests/test_hermite_cable_dynamic.f90 @@ -298,12 +298,13 @@ SUBROUTINE check_contact_configuration() !! Installing the production nodal contact law must immediately refresh the !! consistent acceleration, while malformed replacement data fails before it can !! disturb the committed configuration. - TYPE(CD_HermiteCableDynType) :: m + TYPE(CD_HermiteCableDynType), ALLOCATABLE :: m ! see DEVELOPMENT.md (gfortran 16 -Wuninitialized) REAL(wp), ALLOCATABLE :: l0(:), EAv(:), EIv(:), rhoAv(:), wv(:), seed(:), kn(:), cn(:), a_before(:) INTEGER, ALLOCATABLE :: fx(:) INTEGER :: es, i CHARACTER(300) :: em + ALLOCATE (m) CALL build_beam(0.0_wp, l0, EAv, EIv, rhoAv, wv, seed, fx) DO i = 1, NE + 1 seed(6*(i - 1) + 3) = -0.01_wp diff --git a/tests/test_hermite_cable_static.f90 b/tests/test_hermite_cable_static.f90 index 2e5737b..16e654f 100644 --- a/tests/test_hermite_cable_static.f90 +++ b/tests/test_hermite_cable_static.f90 @@ -193,13 +193,14 @@ SUBROUTINE check_dry_buoyancy_static() INTEGER, PARAMETER :: NE = 25, NN = NE + 1, NDOF = 6*NN REAL(wp), PARAMETER :: G = 9.80665_wp, RHOW = 1025.0_wp, D = 0.2_wp, MPL = 80.0_wp REAL(wp), PARAMETER :: EAX = 1.0e6_wp, LTOT = 100.0_wp, ZTOP = 18.0_wp, PI_L = 3.14159265358979324_wp - TYPE(CD_HermiteCableDynType) :: dyn + TYPE(CD_HermiteCableDynType), ALLOCATABLE :: dyn ! see DEVELOPMENT.md (gfortran 16 -Wuninitialized) REAL(wp) :: l0(NE), EA(NE), EI(NE), w(NE), bvec(NE), rho_a(NE), diam(NE), cd0(NE) REAL(wp) :: seed(NDOF), q(NDOF), curv(NN), res, area, wsub, buoy, lo, hi, sd, t0, zex, sig, err, fr(3) REAL(wp) :: amax, zend INTEGER :: fixed(4*NN + 1), nfix, i, es, iters, k CHARACTER(300) :: em + ALLOCATE (dyn) area = 0.25_wp*PI_L*D*D wsub = (MPL - RHOW*area)*G buoy = RHOW*G*area diff --git a/tests/test_hermite_drag_dynamics.f90 b/tests/test_hermite_drag_dynamics.f90 index efeb66e..04e7c88 100644 --- a/tests/test_hermite_drag_dynamics.f90 +++ b/tests/test_hermite_drag_dynamics.f90 @@ -274,12 +274,13 @@ END SUBROUTINE check_still_water_fixed_point SUBROUTINE check_current_rest_acceleration() !! A transverse current on an at-rest straight cable induces a non-zero initial acceleration -- !! confirming Set_Drag refreshes the consistent a0 (still water leaves the straight cable at a0=0). - TYPE(CD_HermiteCableDynType) :: m + TYPE(CD_HermiteCableDynType), ALLOCATABLE :: m ! see DEVELOPMENT.md (gfortran 16 -Wuninitialized) REAL(wp), ALLOCATABLE :: l0(:), EAv(:), EIv(:), rhoAv(:), wv(:), seed(:) INTEGER, ALLOCATABLE :: fx(:) REAL(wp) :: dv(NE), cn(NE), ct(NE), cur(3) INTEGER :: es CHARACTER(300) :: em + ALLOCATE (m) CALL build_beam(0.0_wp, l0, EAv, EIv, rhoAv, wv, seed, fx) CALL CD_HermiteCable_Dyn_Init(m, l0, EAv, EIv, rhoAv, wv, seed, fx, 0.0_wp, 0.0_wp, 0.9_wp, es, em) CALL require(es == CD_HCDYN_OK, 'current-a0 init: '//TRIM(em)) @@ -297,7 +298,7 @@ SUBROUTINE check_held_field_history_contract() !! completes a0 initialisation, but a later update must not overwrite the committed !! generalised-alpha acceleration from the preceding time station. The update still !! validates the proposed load transactionally. - TYPE(CD_HermiteCableDynType) :: m + TYPE(CD_HermiteCableDynType), ALLOCATABLE :: m ! see DEVELOPMENT.md (gfortran 16 -Wuninitialized) REAL(wp), ALLOCATABLE :: l0(:), EAv(:), EIv(:), rhoAv(:), wv(:), seed(:) INTEGER, ALLOCATABLE :: fx(:) REAL(wp) :: fluid_v(3, NE + 1), fluid_a(3, NE + 1), waterline(NE + 1) @@ -305,6 +306,7 @@ SUBROUTINE check_held_field_history_contract() INTEGER :: es CHARACTER(300) :: em + ALLOCATE (m) CALL build_beam(0.0_wp, l0, EAv, EIv, rhoAv, wv, seed, fx) CALL CD_HermiteCable_Dyn_Init(m, l0, EAv, EIv, rhoAv, wv, seed, fx, 0.0_wp, 0.0_wp, 0.4_wp, es, em) CALL require(es == CD_HCDYN_OK, 'held-field init: '//TRIM(em)) diff --git a/tests/test_hermite_irregular_waves.f90 b/tests/test_hermite_irregular_waves.f90 index 1e78585..4d5d716 100644 --- a/tests/test_hermite_irregular_waves.f90 +++ b/tests/test_hermite_irregular_waves.f90 @@ -353,10 +353,11 @@ END SUBROUTINE fail_closed_battery SUBROUTINE irregular_smoke() INTEGER, PARAMETER :: N = 12 - TYPE(CD_HermiteCableDynType) :: m + TYPE(CD_HermiteCableDynType), ALLOCATABLE :: m ! see DEVELOPMENT.md (gfortran 16 -Wuninitialized) REAL(wp) :: amp(N), per(N), ph(N), q0(66), qend(66) INTEGER :: es CHARACTER(300) :: em + ALLOCATE (m) CALL make_table(N, amp, per, ph) CALL build_hanging_cable(m) q0 = m%q diff --git a/tests/test_hermite_torsion_dynamic.f90 b/tests/test_hermite_torsion_dynamic.f90 index d6b1f92..9eae10d 100644 --- a/tests/test_hermite_torsion_dynamic.f90 +++ b/tests/test_hermite_torsion_dynamic.f90 @@ -164,10 +164,11 @@ END SUBROUTINE build_cable ! ------------------------------------------------------------------------------------------ SUBROUTINE check_step_response() - TYPE(CD_HFMF_ModuleType) :: cab + TYPE(CD_HFMF_ModuleType), ALLOCATABLE :: cab ! see DEVELOPMENT.md (gfortran 16 -Wuninitialized) REAL(wp) :: q(NDOF), xa(3), phi, worst, bent, th, mt, f(3), m(3), mref INTEGER :: k, es CHARACTER(300) :: em + ALLOCATE (cab) CALL line_seed(0.0_wp, 0.0_wp, q) CALL build_cable(cab, q, 1.0_wp, 0.01_wp, 1.0e-8_wp, 0.8_wp) xa = q(NDOF - 5:NDOF - 3) @@ -344,13 +345,14 @@ END SUBROUTINE march SUBROUTINE check_restart() INTEGER, PARAMETER :: K_SNAP = 250, K_END = 400 - TYPE(CD_HFMF_ModuleType) :: cab, fresh + TYPE(CD_HFMF_ModuleType), ALLOCATABLE :: cab, fresh ! see DEVELOPMENT.md (gfortran 16 -Wuninitialized) REAL(wp) :: q(NDOF), qa(NDOF), va(NDOF), tha, mta, th, mt, x(3), v(3), a(3), dcm(3, 3), w(3), al(3), phi REAL(wp) :: t_snap REAL(wp), ALLOCATABLE :: buf(:) LOGICAL :: ok INTEGER :: es CHARACTER(300) :: em + ALLOCATE (cab, fresh) CALL line_seed(0.0_wp, 0.0_wp, q) CALL build_cable(cab, q, 1.0_wp, 0.01_wp, 1.0e-6_wp, 0.8_wp) ! a restart is exact with cross-step factor reuse off (the factor's age is not state)