Windows native build with ASan - Claude AI notes: ASan found a heap-use-after-free bug

Thanks for all the work. It should be fixed with c527bb29a976ead5a39b5a7e6b37e4723f50ec76. The crucial fix are the changes to libbf.c. The changes to pl-gmp.h are mostly cleanup and debugging, though AR_CLEANUP() was broken, possibly leading to memory errors in case of some error involving big integers or rational numbers.

The mystery is not completely resolved. Basically nothing went really wrong unless AR_CLEANUP() was used, which is used if arithmetic does not complete normally, i.e., if there is some kind of error. That is why the problem did not show up when using LibBF on Linux: there should not be an error in this test.

So, I suspect there is some other error in the bignum handling using MSVC that triggered this issue … We’ll see …

I wouldn’t thank me just yet.

The results are mixed and interesting.

  1. cmake --build buld fails with the same error as before.
    C:\Program Files\Microsoft Visual Studio\18\Community\MSBuild\Microsoft\VC\v180\Microsoft.CppCommon.targets(254,5): error MSB8066: Custom build for 'C:\dev-MSVC-PR\swipl-devel\build\CMakeFiles\ef26a058d73045c5cc7a626820c0b1c6\manindex.db.rule;C:\dev-MSVC-PR\swipl-devel\build\CMakeFiles\3756dba4d1b419f94b58a6a1dab534db\man_index.rule;C:\dev-MSVC-PR\swipl-devel\CMakeLists.txt' exited with code -1073741819. [C:\dev-MSVC-PR\swipl-devel\build\man_index.vcxproj]

  2. The command line test I ran fails with ACCESS ERROR (-1073741819)

  3. The test run using SWI-Prolog you provided passes.


Here are the build and tests steps used.


C:\dev-MSVC-PR\swipl-devel\build>cd C:\dev-MSVC-PR\swipl-devel

C:\dev-MSVC-PR\swipl-devel>rmdir /s /q build

C:\dev-MSVC-PR\swipl-devel>mkdir build

C:\dev-MSVC-PR\swipl-devel>cd build

C:\dev-MSVC-PR\swipl-devel\build>cmake .. -G "Visual Studio 18 2026" -A x64 -DCMAKE_TOOLCHAIN_FILE=C:/dev/vcpkg/scripts/buildsystems/vcpkg.cmake -DCMAKE_C_FLAGS="/fsanitize=address" -DCMAKE_CXX_FLAGS="/fsanitize=address" -DCMAKE_EXE_LINKER_FLAGS="/DEBUG:FULL" -DCMAKE_SHARED_LINKER_FLAGS="/DEBUG:FULL" -DPython_EXECUTABLE="C:/Users/Eric/AppData/Local/Programs/Python/Python313/python.exe" -DBDB_LIBRARY=C:/dev/vcpkg/installed/x64-windows/lib/libdb48.lib > "C:\Users\Eric\Projects\SWI-Prolog-PR\output\cmake-config (028).txt" 2>&1

C:\dev-MSVC-PR\swipl-devel\build>echo %ERRORLEVEL%                                                                             
0

C:\dev-MSVC-PR\swipl-devel\build>cmake --build . --config Debug --verbose > "C:\Users\Eric\Projects\SWI-Prolog-PR\output\cmake-build (028).txt" 2>&1

C:\dev-MSVC-PR\swipl-devel\build>echo %ERRORLEVEL%                                                                             
1

C:\dev-MSVC-PR\swipl-devel\build>src\Debug\swipl.exe -f none --no-packs --home=home -g "use_module(library(pldoc/man_index))" -g "prolog_manual_index:index_man_file(manual, swi('/doc/Manual'), 'c:/dev-msvc-pr/swipl-devel/build/man/manual/arith.html'), writeln(done)" -g halt
done

C:\dev-MSVC-PR\swipl-devel\build>echo %ERRORLEVEL%
-1073741819

C:\dev-MSVC-PR\swipl-devel\build>src\Debug\swipl.exe ../tests/core/test_arith.pl
Welcome to SWI-Prolog (threaded, 64 bits, version 10.1.2-28-g93c3960f5-DIRTY)
SWI-Prolog comes with ABSOLUTELY NO WARRANTY. This is free software.
Please run ?- license. for legal details.

    CMake built from "c:/dev-MSVC-PR/swipl-devel/build"

For online help and background, visit https://www.swi-prolog.org
For built-in help, use ?- help(Topic). or ?- apropos(Word).

3 ?- run_tests(bigint:bf_trig_alloc).
[1/1] bigint:bf_trig_alloc ...................................................................... passed (0.056 sec)
% test passed in 0.131 seconds (0.125 cpu)
true.

4 ?- 

I am not even going to attempt to understand how that happened.

If there is something you need me to try, just ask.

That seems to be a SEGV crash. Is there really no additional information? Running it in the MSVC IDE might give a clue. Looks like it is crashing in the process cleanup somewhere as it writes the final ok. It is not unlikely that a release build would succeed. The process cleanup is pretty tricky. The debug build tries to clean everything and checks a lot of stuff. The release builds normally simply skip the cleanup. Nevertheless, this passes on Linux when using both Asan and Debug.

This surely is unrelated to the bigint issues; the manual is not that big that we need big integers for it :slight_smile:

Good. So that is gone and that was a real bug that could also affect other platforms (in different scenarios).

Yes.

Linux → Microsoft translation:

On Windows this typically shows up as:

ACCESS ERROR (-1073741819)

Representation Value
Dec -1073741819
DWORD Hex C000 0005

Which maps to:

ERROR_ACCESS_DENIED

5 (0x5)

Access is denied.


I’ll take that as a rhetorical question, since the next statement is exactly the reply I was hoping to see as a suggested next step.

:+1:


As an Air Traffic Controller (ATC) might say: You are #1 for departure

In other words, that is exactly where I would look next.


I did not know this.

So with using debug builds, MSVC and ASan are we now in somewhat unexplored territory? (Not a rhetorical question.)


Claude suggested that this might be related to differences in structure sizes between Windows and Linux. I did not pursue this further, as there were no concrete facts to support it, but it felt worth mentioning in case someone can confirm whether this is plausible and explain it more clearly.


That is how I would like to interpret the result.

  • The SEGV crash occurs during cmake --build.
  • The bigint issue was isolated to the bigint:bf_trig_alloc test.

I will keep the bigint:bf_trig_alloc test enabled going forward and monitor it across the next several builds and ctest runs, just to be safe.


Any feedback on this PR would be appreciated:

I know it did not pass the checks because the CI was cancelled.

image

Also, I should mention (again) that I’m horrible with Git. Being left in a detached HEAD state is particularly confusing. :slightly_smiling_face:

The three, yes. I never tried debug builds with asan. As said, just did and it passes fine on Linux, both the build and ctest.

That is true. Windows long is 32 bits (64 on anything else) and wchar_t is 16 bits (32 on anything else). That changes things. I consider that a small risk for a crash though. There are many more significant differences between platforms that require distinct handling on Windows and other systems. Windows specific code could have a bug.

I know that can feel annoying :slight_smile: Roughly,

  • What changes do you have compared to the upstream version? If there are local not committed changes, commit them (to your loose HEAD).
  • Copy the hash of the HEAD to a safe place.
  • Run git fetch, git checkout master and git reset --hard origin/master to get in sync with the upstream code again.
  • Switch to a new branch for your porting work using git checkout -b my-work
  • Run git log <saved HEAD commit. Identify the hashes of the changes you want to keep and for each of them run git cherry-pick <hash>.

Main take home:

  • Before doing anything dangerous, make sure all is committed.
  • Record the hash of the detached head

Now, there is pretty much nothing you can do (except deleting the repo) that does irreversible damage. All your commits are in one of your branches or below the detached HEAD. Decide where you want to restart and merge/cherry pick all work you need. If you mess up, simply delete the branch and try again. Git repos are basically a set of states connected to each other using commits. That forms a directed graph. Branches are named (often leaf) nodes of this graph. A detached head is nothing more than a node in the graph that happens to not be a branch head.

I just synced with these repositories

  • swipl-devel
  • packages-xpce
  • packages-clib
  • packages-pcre

and currently also run git checkout fix-msvc-asan-sleep.

fix-msvc-asan-sleep is the PR with statements like

#if defined(__SANITIZE_ADDRESS__) && !defined(__WINDOWS__)

to allow MSVC to compile without error.

Claude, for the most part is the only reason I can do any Git without major problems. However with so many things in play sometimes even Claude does not get it correct.


Yes


One VS Code extension that helps me at least is

Even with that still have to manually check for the code in the unapproved PRS before doing a compile.


Thanks.

Sharing this for those that may want to see how to use Visual Studio IDE to debug SWI-Prolog compiled with MSCV.

I will note that I use Claude regularly because it just does lots of the grunt work for me. Here is what Claude suggested to setup Visual Studio IDE to work on the cmake --build SEGV crash.


Crash reproduced: 0xC0000005 (access violation).

To get a stack trace, run under Visual Studio debugger:

  1. Open Visual Studio 2026
  2. File → Open → Project/Solution → C:\dev-MSVC-PR\swipl-devel\build\SWI-Prolog.sln
  3. Right-click swipl project → Set as Startup Project
  4. Project → Properties → Debugging:
  • Command Arguments: -f none --no-packs --home=home -g "use_module(library(pldoc/man_index))" -g save_man_index -t halt
  • Working Directory: C:\dev-MSVC-PR\swipl-devel\build
  1. Debug → Start Debugging (F5)

When it crashes, VS will break and show the call stack.

Alternative - quick command-line approach with procdump:

If you have Sysinternals procdump:

procdump -e -ma -x C:\dev-MSVC-PR\swipl-devel\build C:\dev-MSVC-PR\swipl-devel\build\src\Debug\swipl.exe -f none --no-packs --home=home -g "use_module(library(pldoc/man_index))" -g save_man_index -t halt

Which approach would you like to try?


Steps with notes

1. Open Visual Studio 2026

For this Visual Studio 2026 needs to be installed on a Windows machine.
Visual Studio 2026 community is free and what I use.

2. File → Open → Project/Solution → C:\dev-MSVC-PR\swipl-devel\build\SWI-Prolog.sln

The *.sln file is created during cmake .. --G ( Configuration Step, AKA Generation Step ) . With VS 2026 installed one just needs to double click on the file and if the build was clean the solution (*.sln) should open.

3. Right-click swipl project → Set as Startup Project

Once VS 2026 starts find

scroll down to swipl

Right-click swipl project → Set as Startup Project

The list of options is long so you have to look for it

Once swipl is set as the startup project, the text will be bold.

It may be hard to see that the text is bold, but it is.

image

4. Project → Properties → Debugging:

Before using the menu check these two values
image

Using menu
Project → Properties

For Property PagesConfiguration Properties
Select: Debugging

* Command Arguments: -f none --no-packs --home=home -g "use_module(library(pldoc/man_index))" -g save_man_index -t halt

Paste the arguments into the Command Arguments value

* Working Directory: C:\dev-MSVC-PR\swipl-devel\build

Paste the path into the Working Directory value

Click Apply
Click OK

Press F5

Output windows shows

7> swipl.vcxproj → C:\dev-MSVC-PR\swipl-devel\build\src\Debug\swipl.exe
========== Build: 7 succeeded, 0 failed, 1 up-to-date, 0 skipped ==========
========== Build completed at 7:51 AM and took 38.161 seconds ==========

Click OK

Press F5

The code will run until the crash.

I think the best way to deal with this is to create a branch for your porting work. Whenever you want to create a PR.

 git checkout master
 git pull (or git reset --hard origin/master if you messed up master)
 git checkout my-porting-work
 git rebase master (get your branch consistent with upstream)
 git checkout master 
 git checkout -b my-nice-pr
 git cherry-pick <hash> (repeat to get all commits from my-porting-work you want upstream

Now create a PR. If the PR is approved as is, syncing master and rebasing my-port-work should remove the commits that were accepted upstream. In case the PR was modified

 - Use `git rebase -i master` in the `my-porting-work` and remove all commits that were part of the PR
 - Sync master and rebase

P.s. Merged the PR that allows Asan with MSVC (after changes).

1 Like

Currently working through fixes for the C++ tests.

An issue was encountered where the names of individual C++ tests were not visible in the ctest output. While reviewing test_cpp.pl, the following comment was noted:

% Some of the tests can result in crashes if there’s a bug, so the
% output(on_failure) option results in nothing being written.
% If so, uncomment the following line
% :- set_test_options([output(always), format(log)]).

Uncommenting the line did the trick.


The following tests are currently blocked so that the entire cpp test suite can pass:

	Line  126: test(call_cpp_06,[blocked('EGT Fix me'),nondet]) :-
	Line  130: test(call_cpp_07,[blocked('EGT Fix me'), Out == "hello(世界四)\n"]) :-
	Line  132: test(call_cpp_08,[blocked('EGT Fix me'), error(existence_error(procedure,unknown_pred/1))]) :-
	Line  867: test(blob, [blocked(cant_throw_error),
	Line  874: test(blob, blocked('throws std::runtime_error')) :-
	Line 1139: test(plterm_scoped, [blocked('crashes in PL_free_term_ref')]) :-
	Line 1154: test(atom_atom_map,[blocked('EGT Fix me')]) :-
	Line 1172: test(atom_term_map,[blocked('EGT Fix me')]) :-

Note: Some test names were made unique by appending a numeric suffix so they can be more easily identified in ctest output.

"C:\dev-MSVC-PR\swipl-devel\build\src\Debug\swipl.exe" -p "foreign=" -f none --no-packs --on-error=status -s "C:/dev-MSVC-PR/swipl-devel/packages/cpp/test_cpp.pl" -g test_cpp -t halt
...
% 8 tests are blocked (use run_tests/2 with show_blocked(true) for details)
% All 256 tests passed in 1.172 seconds (0.922 cpu)

FYI

Run all tests in test_cpp.pl

test_cpp :-
    run_tests.
"C:\dev-MSVC-PR\swipl-devel\build\src\Debug\swipl.exe" -p "foreign=" -f none --no-packs --on-error=status -s "C:/dev-MSVC-PR/swipl-devel/packages/cpp/test_cpp.pl" -g test_cpp -t halt

% Start unit: cpp
% [1/256] cpp:unwrap ........................................................................................................................... passed (0.038 sec)
% [2/256] cpp:hello_01 ......................................................................................................................... passed (0.000 sec)        
% [3/256] cpp:helloex_01 ....................................................................................................................... passed (0.001 sec)

...

% [256/256] cpp_map_str_str:map ................................................................................................................ passed (0.000 sec)        
% End unit cpp_map_str_str: passed (0.031 sec CPU)
% 8 tests are blocked (use run_tests/2 with show_blocked(true) for details)
% All 256 tests passed in 1.146 seconds (1.000 cpu)

Run tests in test suite cpp :- begin_tests(cpp).

C:\dev-MSVC-PR\swipl-devel\build>"C:\dev-MSVC-PR\swipl-devel\build\src\Debug\swipl.exe" -p "foreign=" -f none --no-packs --on-error=status -s "C:/dev-MSVC-PR/swipl-devel/packages/cpp/test_cpp.pl" -g "run_tests(cpp)" -t halt 
% Start unit: cpp
% [1/247] cpp:unwrap ........................................................................................................................... passed (0.039 sec)
% [2/247] cpp:hello_01 ......................................................................................................................... passed (0.001 sec)        
% [3/247] cpp:helloex_01 ....................................................................................................................... passed (0.000 sec)  

...

% [247/247] cpp:record_ext ..................................................................................................................... passed (0.000 sec)        
% End unit cpp: passed (0.813 sec CPU)
% 6 tests are blocked (use run_tests/2 with show_blocked(true) for details)
% All 247 tests passed in 1.088 seconds (0.813 cpu)

Run single test call_cpp_o4

test(call_cpp_04, fail) :-
    call_cpp(atom(hello(foo))).

Note: My local working copy of the file uses the name call_cpp_04 to uniquely identify the test.

C:\dev-MSVC-PR\swipl-devel\build>"C:\dev-MSVC-PR\swipl-devel\build\src\Debug\swipl.exe" -p "foreign=" -f none --no-packs --on-error=status -s "C:/dev-MSVC-PR/swipl-devel/packages/cpp/test_cpp.pl" -g "run_tests(cpp:call_cpp_04)" -t halt
% Start unit: cpp
% [1/1] cpp:call_cpp_04 ........................................................................................................................ passed (0.036 sec)
% End unit cpp: passed (0.047 sec CPU)
% test passed in 0.103 seconds (0.109 cpu)
1 Like

That is a good idea. Please put that in the PR. It is just laziness and copy/paste why the same test name is often reused.

1 Like

No, I had the same question.

If you look at test_cpp.cpp there are multiple test for the predicates in the cpp file. It confused me at first until I saw the names, e.g.

	Line   99: PREDICATE(hello, 0)
	Line  105: PREDICATE(hello, 2)
	Line  117: PREDICATE(helloex, 0)
	Line  121: PREDICATE(helloex, 1)
	Line  125: PREDICATE(hello2, 2)
	Line  138: PREDICATE(hello3, 2)
	Line  157: PREDICATE(hello4, 1)

My view is to get @peter.ludemann input, I did this solely to make it easier to work on the test.

Thinking out loud here so @jan, @peter.ludemann and @mgondan1 can see this and possibly comment.

The builds for the tests discussed below are done using MSVC + CMake, Debug, with ASan enabled..

Note: I am not entirely sure the following interpretation is correct. I have been trying to confirm details in Microsoft documentation, but searching for %ERRORLEVEL% has not been very helpful.

On Windows, %ERRORLEVEL% is used to propagate process exit codes (see Microsoft’s documentation on system error codes).

It appears that some tests do not check the value of %ERRORLEVEL% when running on Windows. As a result, tests may report as passed while still returning a non-zero exit code, which can lead to invalid or unexpected results.

This may be happening in the following case.

PREDICATE(call_cpp_ex, 2)
{ try
  { PlCheckFail(PlCall(A1, PL_Q_CATCH_EXCEPTION));
  } catch ( PlException& ex )
  { bool rc = A2.unify_term(ex.term());
    Plx_clear_exception();
    return rc;
  }
  return A2.unify_string("no exception");
}

Corresponding test

test(call_cpp_06,nondet) :-
    call_cpp_ex(unknown_pred(hello(世界四)), Ex),
    assertion(subsumes_term(error(existence_error(procedure, unknown_pred/1), _), Ex)).

When this test is run by itself, it reports success: passed (0.031 sec CPU)

However, checking %ERRORLEVEL% afterward shows that the return value is 1, not 0:

C:\dev-MSVC-PR\swipl-devel\build>"C:\dev-MSVC-PR\swipl-devel\build\src\Debug\swipl.exe" -p "foreign=" -f none --no-packs --on-error=status -s "C:/dev-MSVC-PR/swipl-devel/packages/cpp/test_cpp.pl" -g "run_tests(cpp:call_cpp_06)" -t halt
% Start unit: cpp
% [1/1] cpp:call_cpp_06 .................................................................................................................................... passed (0.035 sec)
% test passed

C:\dev-MSVC-PR\swipl-devel\build>echo %ERRORLEVEL%
1

For comparison, similar tests do return 0 as expected:

test(call_cpp_05, Ex == "no exception") :-
    call_cpp_ex(writeln(hello(世界四)), Ex).
C:\dev-MSVC-PR\swipl-devel\build>"C:\dev-MSVC-PR\swipl-devel\build\src\Debug\swipl.exe" -p "foreign=" -f none --no-packs --on-error=status -s "C:/dev-MSVC-PR/swipl-devel/packages/cpp/test_cpp.pl" -g "run_tests(cpp:call_cpp_05)" -t halt  
% Start unit: cpp
% [1/1] cpp:call_cpp_05 ..hello(世界四)
............................................................................................................................................................ passed (0.039 sec)
% End unit cpp: passed (0.047 sec CPU)
% test passed in 0.114 seconds (0.125 cpu)

C:\dev-MSVC-PR\swipl-devel\build>echo %ERRORLEVEL%                                                                                                                                     
0
test(call_cpp_03, error(existence_error(procedure,unknown_pred/1))) :-
    call_cpp(unknown_pred(hello(世界四))).
C:\dev-MSVC-PR\swipl-devel\build>"C:\dev-MSVC-PR\swipl-devel\build\src\Debug\swipl.exe" -p "foreign=" -f none --no-packs --on-error=status -s "C:/dev-MSVC-PR/swipl-devel/packages/cpp/test_cpp.pl" -g "run_tests(cpp:call_cpp_03)" -t halt
% Start unit: cpp
% [1/1] cpp:call_cpp_03 .................................................................................................................................... passed (0.039 sec)        
% End unit cpp: passed (0.031 sec CPU)
% test passed in 0.108 seconds (0.094 cpu)

C:\dev-MSVC-PR\swipl-devel\build>echo %ERRORLEVEL%
0

What led me to this

When call_cpp_06 is blocked, the output looks correct and test numbering proceeds as expected:

% [13/247] cpp:call_cpp_04 ..................................................................................................................... passed (0.000 sec)
% [14/247] cpp:call_cpp_05 ..hello(世界四)
................................................................................................................................................ passed (0.001 sec)        
% [15/247] cpp:call_cpp_09 ..................................................................................................................... passed (0.000 sec)        
% [16/247] cpp:as_string_01 .................................................................................................................... passed (0.000 sec)  

(call_cpp_07 and call_cpp_08 are also blocked.)

However, when call_cpp_06 is unblocked, the output changes in an unexpected way:

% [13/248] cpp:call_cpp_04 ................................................................................................................................. passed (0.000 sec)        
% [14/248] cpp:call_cpp_05 ..hello(世界四)
............................................................................................................................................................ passed (0.000 sec)        
% [15/248] cpp:call_cpp_06 ................................................................................................................................. passed (0.000 sec)        
% 5 tests are blocked (use run_tests/2 with show_blocked(true) for details)
% All 248 (+-233 sub-tests) tests passed

What I would expect instead is for the output to continue with the remaining tests, for example:

% [16/248] cpp:call_cpp_09 ..................................................................................................................... passed (0.000 sec)        
% [17/248] cpp:as_string_01 .................................................................................................................... passed (0.000 sec) 

…and then the rest of the test suite.

There is definitely something fishy with this call_cpp_06 test. It seems MSVC specific though. I ran the cpp tests using the Debug and Asan (combined) version as

 src/swipl ../packages/cpp/test_cpp.pl
 ?- prolog_debug(chk_secure).
 ?- test_cpp.

And they pass without issues. The prolog_debug/1 predicate is only effective when compiled with -DO_DEBUG (default for -DCMAKE_BUILD_TYPE=Debug) It activates the DEBUG(<channel>, <code>) macros. Most of the are MSG_<topic>, but CHK_SECURE activates a lot of often very expensive consistency checks to the code.

Nice job for the AI, I’d say :slight_smile: So far it came with various useful clues (but often with poor or even wrong solutions).

Just to clarify: are you noting that you used AI, or suggesting that I did?

For this particular work, I did not use AI, except to generate the command lines for running the tests. Claude has been quite off the mark on this bug hunt, so the call_cpp_06 investigation was done entirely the old-school way.

That said, I do have some local changes that Claude previously suggested in pl-vmi.c and pl-wam.c which may be influencing the results I’m seeing. They are additional BFR assertions/changes needed when ASan is enabled. I’m holding off on sharing those changes until I better understand their interaction with the other C++ tests.

If you’d like, I can send the pl-vmi.c and pl-wam.c changes as a PR for review, with the caveat that they exist just there to let me make progress. I also suspect some of the checks should be conditional on ASan being enabled.

I was hoping you do. I’m not a Windows user :slight_smile: I’m only mildly interested in an MSVC port. It can be useful to some people and your work solved a couple of more general issues. Long time ago the Windows version was compiled using MSVC, but the burden to maintain two sets of Makefiles was too much and when it became clear that the MinGW (GCC targetting Windows) compiled version was 20%-30% faster the decision was clear. Since then, GCC made more progress than MSVC. The GCC compiled version is now probably closer to twice as fast. But, using CMake it became feasible to target MSVC using the same code base, making it an option again.

I’m happy to have a look. As is though, my suspicion is something non-portable in SWI-cpp2.h or something dubious in MSVC.

I am not expecting you to spend time on them as they currently exist but they tend to offer up some thoughts.

I’ll get this out, should be about an hour or two do a full resync and rebuild with just these. Still need to pick up the change for nlp that is in my code that you just added.

Do I understand correctly that -DCMAKE_BUILD_TYPE=Sanitize does not imply Debug? Can I tweak this somehow?

1 Like

It does enable debug compilation from the viewpoint of the C compiler (no optimization and add symbol info for debugging). It is not enable SWI-Prolog’s debug features. To do that, using

 CFLAGS=-DO_DEBUG cmake ...
2 Likes

:slightly_smiling_face:

In checking the cmake .. -G "Visual Studio 18 2026" warnings, found

C:\dev-MSVC-PR\swipl-devel\packages\cpp\SWI-cpp2.cpp(311,5): warning C4530: C++ exception handler used, but unwind semantics are not enabled. Specify /EHsc [C:\dev-MSVC-PR\swipl-devel\build\packages\cpp\plugin_test_cpp.vcxproj]
  (compiling source file '../../../packages/cpp/test_cpp.cpp')

Asked Claude if this and the noted problem with the test cpp:call_cpp_06 are related.


Yes — there is a very plausible cause-and-effect relationship

The problem in post #52 is centered on call_cpp_06, which tests C++ exception handling — specifically catching a PlException thrown when calling an undefined Prolog predicate:

PREDICATE(call_cpp_ex, 2)
{ try
  { PlCheckFail(PlCall(A1, PL_Q_CATCH_EXCEPTION));
  } catch ( PlException& ex )
  { bool rc = A2.unify_term(ex.term());
    Plx_clear_exception();
    return rc;
  }
  return A2.unify_string("no exception");
}

The connection

Without /EHsc, MSVC compiles the code but does not enable proper stack unwinding semantics. This means:

  1. The catch (PlException& ex) block may not execute correctly — the runtime may not properly transfer control to the catch handler, or may do so without running destructors for intermediate stack objects.
  2. Plx_clear_exception() may never be called — if the catch block is skipped or partially executed, the Prolog exception state remains “dirty” after the test.
  3. This explains both symptoms exactly:
  • Test “passes” but exit code is 1 — the Prolog-level test assertion succeeds (or appears to), but the uncleared exception state causes the process to exit with a non-zero status.
  • Test runner halts prematurely after call_cpp_06 — the lingering exception state corrupts the test harness, causing it to bail out early with “All 248 tests passed” before actually running the remaining tests.

In short

Missing /EHsc → broken C++ exception unwinding → catch (PlException&) doesn’t work reliably → exception state leaks → exit code 1 + premature test termination.

Adding /EHsc to your CMAKE_CXX_FLAGS may resolve or significantly change the behavior of this bug. It’s worth rebuilding with it and re-running call_cpp_06 to see if the symptoms disappear.


Claude also explained the error message with more detail.

This is MSVC warning C4530, and it’s straightforward to explain.

What it means

The code in SWI-cpp2.cpp (line 311) uses C++ exception handling (try/catch/throw), but the compiler flag /EHsc (which enables C++ exception unwinding semantics) is not enabled for that compilation unit.

Without /EHsc, destructors of local objects on the stack may not be called when an exception is thrown, which can cause resource leaks or undefined behavior.

Why it’s happening

By setting -DCMAKE_CXX_FLAGS="/fsanitize=address", you are overriding the default CXX flags that CMake/the project would normally set — which typically include /EHsc. Your custom flags replace them rather than appending to them.

The fix

Include /EHsc in your custom CXX flags:

-DCMAKE_CXX_FLAGS="/fsanitize=address /EHsc"

So your full configure command becomes:

cmake .. -G "Visual Studio 18 2026" -A x64 ^
  -DCMAKE_TOOLCHAIN_FILE=C:/dev/vcpkg/scripts/buildsystems/vcpkg.cmake ^
  -DCMAKE_C_FLAGS="/fsanitize=address" ^
  -DCMAKE_CXX_FLAGS="/fsanitize=address /EHsc" ^
  -DCMAKE_EXE_LINKER_FLAGS="/DEBUG:FULL" ^
  -DCMAKE_SHARED_LINKER_FLAGS="/DEBUG:FULL" ^
  -DPython_EXECUTABLE="C:/Users/Eric/AppData/Local/Programs/Python/Python313/python.exe" ^
  -DBDB_LIBRARY=C:/dev/vcpkg/installed/x64-windows/lib/libdb48.lib

/EHsc means: enable C++ exceptions for extern "C" functions (they don’t throw) and standard C++ functions (they may throw). It’s the standard setting for virtually all MSVC C++ projects.


I will rebuild with the noted changes and report back if there are any differences.

2 Likes

The cpp unit test failures that appear as BFR problems only show up when doing a MSVC CMake Debug build with Address Sanitizer and without /EHsc added.

These instructions cause the cpp unit test failures and the BFR noted problems which are only because the build instructions are not correct.


039 Build: Debug + ASAN less /EHsc
Using x64 Native Tools Command Prompt for VS Terminal
   swipl --version -> 'swipl' is not recognized as an internal or external command, operable program or batch file.
   set PATH=%PATH:C:\Program Files\swipl\bin;=%                           (If needed)
   python --version -> Python 3.13.5
   set PATH=C:\Users\Eric\AppData\Local\Programs\Python\Python313;%PATH%  (If needed)
   set ASAN_SAVE_DUMPS="C:\Users\Eric\Projects\SWI-Prolog-PR\output\asan_crash (039).dmp"
   echo %ASAN_SAVE_DUMPS%
   cd C:\dev-MSVC-PR\swipl-devel
   del \\.\C:\dev-MSVC-PR\swipl-devel\build\src\Debug\nul                (If needed)   
   rmdir /s /q build
   mkdir build 
   cd build  
   cmake .. -G "Visual Studio 18 2026" -A x64 -DCMAKE_TOOLCHAIN_FILE=C:/dev/vcpkg/scripts/buildsystems/vcpkg.cmake -DCMAKE_C_FLAGS="/fsanitize=address" -DCMAKE_CXX_FLAGS="/fsanitize=address" -DCMAKE_EXE_LINKER_FLAGS="/DEBUG:FULL" -DCMAKE_SHARED_LINKER_FLAGS="/DEBUG:FULL" -DPython_EXECUTABLE="C:/Users/Eric/AppData/Local/Programs/Python/Python313/python.exe" -DBDB_LIBRARY=C:/dev/vcpkg/installed/x64-windows/lib/libdb48.lib > "C:\Users\Eric\Projects\SWI-Prolog-PR\output\cmake-config (039).txt" 2>&1
   echo %ERRORLEVEL% -> 0  
   cmake --build . --config Debug --verbose > "C:\Users\Eric\Projects\SWI-Prolog-PR\output\cmake-build (039).txt" 2>&1
   echo %ERRORLEVEL% -> 0
   dumpbin /headers src\Debug\swipl.exe > "C:\Users\Eric\Projects\SWI-Prolog-PR\output\dumpbin (039).txt" 
   ctest -C Debug --output-on-failure --timeout 300 --verbose > "C:\Users\Eric\Projects\SWI-Prolog-PR\output\ctest (039).txt" 2>&1
   echo %ERRORLEVEL% -> 8

This variation with Address Sanitizer and /EHsc runs the cpp tests cleanly.


042 Build: Debug + ASAN without BFR changes + /EHsc
Using x64 Native Tools Command Prompt for VS Terminal
   swipl --version -> 'swipl' is not recognized as an internal or external command, operable program or batch file.
   set PATH=%PATH:C:\Program Files\swipl\bin;=%                           (If needed)
   python --version -> Python 3.13.5
   set PATH=C:\Users\Eric\AppData\Local\Programs\Python\Python313;%PATH%  (If needed)
   set ASAN_SAVE_DUMPS="C:\Users\Eric\Projects\SWI-Prolog-PR\output\asan_crash (042).dmp"
   echo %ASAN_SAVE_DUMPS%
   cd C:\dev-MSVC-PR\swipl-devel
   del \\.\C:\dev-MSVC-PR\swipl-devel\build\src\Debug\nul                (If needed)   
   rmdir /s /q build
   mkdir build 
   cd build  
   cmake .. -G "Visual Studio 18 2026" -A x64 -DCMAKE_TOOLCHAIN_FILE=C:/dev/vcpkg/scripts/buildsystems/vcpkg.cmake -DCMAKE_C_FLAGS="/fsanitize=address" -DCMAKE_CXX_FLAGS="/fsanitize=address /EHsc" -DCMAKE_EXE_LINKER_FLAGS="/DEBUG:FULL" -DCMAKE_SHARED_LINKER_FLAGS="/DEBUG:FULL" -DPython_EXECUTABLE="C:/Users/Eric/AppData/Local/Programs/Python/Python313/python.exe" -DBDB_LIBRARY=C:/dev/vcpkg/installed/x64-windows/lib/libdb48.lib > "C:\Users\Eric\Projects\SWI-Prolog-PR\output\cmake-config (042).txt" 2>&1
   echo %ERRORLEVEL% -> 0  
   cmake --build . --config Debug --verbose > "C:\Users\Eric\Projects\SWI-Prolog-PR\output\cmake-build (042).txt" 2>&1
   echo %ERRORLEVEL% -> 0
   dumpbin /headers src\Debug\swipl.exe > "C:\Users\Eric\Projects\SWI-Prolog-PR\output\dumpbin (042).txt" 
   ctest -C Debug --output-on-failure --timeout 300 --verbose > "C:\Users\Eric\Projects\SWI-Prolog-PR\output\ctest (042).txt" 2>&1
   echo %ERRORLEVEL% -> 8

Note: The CTest result %ERRORLEVEL% value of 8 is because the failing test
c:/dev-msvc-pr/swipl-devel/tests/thread/test_shared_dynamic.pl which is not failing for a Debug build without Address Sanitizer and is not failing for a Release build.

In short, I think we are done fixing MSVC build errors for the standard build of SWI-Prolog, just have to look at the warnings.

The instructions for a Release version


041 Build: Release 
Using x64 Native Tools Command Prompt for VS Terminal
   swipl --version -> 'swipl' is not recognized as an internal or external command, operable program or batch file.
   set PATH=%PATH:C:\Program Files\swipl\bin;=%                           (If needed)
   python --version -> Python 3.13.5
   set PATH=C:\Users\Eric\AppData\Local\Programs\Python\Python313;%PATH%  (If needed)
   cd C:\dev-MSVC-PR\swipl-devel
   del \\.\C:\dev-MSVC-PR\swipl-devel\build\src\Release\nul                (If needed)   
   rmdir /s /q build
   mkdir build 
   cd build  
   cmake .. -G "Visual Studio 18 2026" -A x64 -DCMAKE_TOOLCHAIN_FILE=C:/dev/vcpkg/scripts/buildsystems/vcpkg.cmake -DPython_EXECUTABLE="C:/Users/Eric/AppData/Local/Programs/Python/Python313/python.exe" -DBDB_LIBRARY=C:/dev/vcpkg/installed/x64-windows/lib/libdb48.lib > "C:\Users\Eric\Projects\SWI-Prolog-PR\output\cmake-config (041).txt" 2>&1
   echo %ERRORLEVEL% -> 0  
   cmake --build . --config Release --verbose > "C:\Users\Eric\Projects\SWI-Prolog-PR\output\cmake-build (041).txt" 2>&1
   echo %ERRORLEVEL% -> 0
   dumpbin /headers src\Release\swipl.exe > "C:\Users\Eric\Projects\SWI-Prolog-PR\output\dumpbin (041).txt" 
   ctest -C Release --output-on-failure --timeout 300 --verbose > "C:\Users\Eric\Projects\SWI-Prolog-PR\output\ctest (041).txt" 2>&1
   echo %ERRORLEVEL% -> 0

Requiring the MSVC warning C4530 to be promoted to an error when using the package cpp should be considered.


A big THANKS to Jan W., could not have done this without his help.

:clap:

1 Like