Skip to content

Conversation

@christopherlam
Copy link
Contributor

@christopherlam christopherlam commented Nov 2, 2025

std::sort void* pointers should have Comparator -- apparently comparing void* is UB https://eel.is/c++draft/expr.rel#4.3 (says chatgpt)

gnc_account_foreach_split forward loops only because the reverse iteration isn't used at all. having the reverse arg is an unnecessary complication.

@christopherlam
Copy link
Contributor Author

Imho the sort can be removed because the splits are already sorted if edit level is zero.

@christopherlam christopherlam force-pushed the better-cpp branch 2 times, most recently from b06ac03 to 211acd7 Compare November 2, 2025 14:41
@jralls
Copy link
Member

jralls commented Nov 2, 2025

std::sort void* pointers should have Comparator -- apparently comparing void* is UB https://eel.is/c++draft/expr.rel#4.3 (says chatgpt)

It's sorting std::fwd_iterator<std::vector<Transaction>> that decomposes to Transaction** in an array, so the first subparagraph of the section you linked, about pointers in an array, applies. It's not UB. It is, however, a no-op because the pointers being sorted are obviously sequential, that's how std::vector works. The intent is to group the identical Transaction*s adjacent so that std::unique can remove the duplicates. For that the sort should be on *iter. That would be UB unless you cast them to integer: reinterpret_cast<intptr_t>(reinterpret_cast<void*>(*iter)).

Imho the sort can be removed because the splits are already sorted if edit level is zero.

Sorted or not the splits should be grouped so that those belonging to the same transaction are adjacent and that's sufficient for std::unique to work. It's probably worth a comment to point that out.

@jralls
Copy link
Member

jralls commented Nov 2, 2025

(*) the GDate is typically used by gdate_to_time64 which performs a julian->dmy conversion, and this is now avoided

But if it's just going to get turned into a time64 why bother with the GDate?

Modern 64-bit builds (or 32-bit with * _TIME_BITS=64) are unaffected.

I think that makes the whole comment obsolete... but we should probably introduce a configure check to break the build if anybody tries on a system with 32-bit time_t.

@christopherlam
Copy link
Contributor Author

It's sorting std::fwd_iterator<std::vector<Transaction>> that decomposes to Transaction** in an array, so the first subparagraph of the section you linked, about pointers in an array, applies. It's not UB. It is, however, a no-op because the pointers being sorted are obviously sequential, that's how std::vector works. The intent is to group the identical Transaction*s adjacent so that std::unique can remove the duplicates. For that the sort should be on *iter. That would be UB unless you cast them to integer: reinterpret_cast<intptr_t>(reinterpret_cast<void*>(*iter)).

Dunno... they (i.e. llm) seem to suggest that sorting Transaction* pointers would then lead to sorting by their absolute address. https://claude.ai/share/8d0b3aae-b348-49b4-bb32-eb3297650771 for examples. But I still believe this sorting is unnecessary and even detrimental. The split are definitely ordered by posting date which ensures efficient destruction from the rbegin, whereas after sorting by pointer address, the destruction will be in arbitrary order.

Modern 64-bit builds (or 32-bit with * _TIME_BITS=64) are unaffected.

I think that makes the whole comment obsolete... but we should probably introduce a configure check to break the build if anybody tries on a system with 32-bit time_t.

added in...

@jralls
Copy link
Member

jralls commented Nov 3, 2025

Dunno... they (i.e. llm) seem to suggest that sorting Transaction* pointers would then lead to sorting by their absolute address.

I think most compilers would. "Undefined behavior" just means that the standard doesn't require the compiler to do--or not do--anything in particular. It's allowed to wipe your hard drive if the implementors want, but that would generally be too much trouble.

But I still believe this sorting is unnecessary and even detrimental.

As written it's definitely unnecessary but since it doesn't do anything it can't be detrimental. If you were to change to actually sort by the Transaction* absolute addresses then it would probably still not do anything, but if it did it would guarantee that std::unique would correctly find and remove duplicate Transaction*s. Keep in mind what the function is doing: Deleting all of the transactions associated with the splits in the current account. If there are two instances of the same transaction then, since they're raw pointers, the program will crash when it tries to destroy the already destroyed and freed transaction. That's not hypothetical, that code was inserted because of exactly that crash.

The split are definitely ordered by posting date which ensures efficient destruction from the rbegin, whereas after sorting by pointer address, the destruction will be in arbitrary order.

Posting date isn't sufficient, there are often many transactions with the same posting date. But as I said earlier the regular sort algorithm ensures that splits are grouped by transaction (otherwise they wouldn't be immediately adjacent in the register) so this sort probably doesn't do anything.

Efficiency is about the memory we're accessing being contiguous and small enough for several items to fit in cache. Neither of those will be true no matter what, and time spent cleaning up memory is insignificant compared to time spent writing to disk so it's not worth worrying about.

@jralls
Copy link
Member

jralls commented Nov 3, 2025

Unfortunately Mingw32 still has the problem:

Reading symbols from C:/Program Files (x86)/gnucash/bin/gnucash.exe...
(gdb) p sizeof(time_t)
$1 = 4

But I set a bp on g_date_set_time_t and it didn't fire so maybe GDate has changed. More research required.

@christopherlam
Copy link
Contributor Author

Efficiency is about the memory we're accessing being contiguous and small enough for several items to fit in cache. Neither of those will be true no matter what, and time spent cleaning up memory is insignificant compared to time spent writing to disk so it's not worth worrying about.

Ok maybe it's minor. I've confirmed that std::sort a vector of void* will sort (on gcc) ascending pointer address, so, it meets the purpose of deduping transactions, but isn't necessary because the splits are already sorted by xaccSplitOrder which groups same-transaction splits together.

@@ -1598,6 +1598,15 @@ xaccAccountDestroy (Account *acc)
     xaccAccountCommitEdit (acc);
 }
 
+static void dump_transactions (const char* header,
+                               std::vector<Transaction*> transactions)
+{
+    printf ("%s dump: ", header);
+    for (auto t : transactions)
+        printf ("transaction %p\n", &*t);
+    printf ("\n");
+}
+
 void
 xaccAccountDestroyAllTransactions(Account *acc)
 {
@@ -1607,7 +1616,9 @@ xaccAccountDestroyAllTransactions(Account *acc)
     std::transform(priv->splits.begin(), priv->splits.end(),
                    back_inserter(transactions),
                    [](auto split) { return split->parent; });
+    dump_transactions ("before sort", transactions);
     std::stable_sort(transactions.begin(), transactions.end());
+    dump_transactions ("after sort", transactions);
     transactions.erase(std::unique(transactions.begin(), transactions.end()),
                        transactions.end());
     qof_event_suspend();

outputs

before sort dump: transaction 0x16e45660
transaction 0x16e90060
transaction 0x16e8e060
transaction 0x16e8e1f0
transaction 0x16e8e300

after sort dump: transaction 0x16e45660
transaction 0x16e8e060
transaction 0x16e8e1f0
transaction 0x16e8e300
transaction 0x16e90060

Unfortunately Mingw32 still has the problem:

Reading symbols from C:/Program Files (x86)/gnucash/bin/gnucash.exe...
(gdb) p sizeof(time_t)
$1 = 4

But I set a bp on g_date_set_time_t and it didn't fire so maybe GDate has changed. More research required.

win32 has 2038 bug? 😨

because they are already sorted. besides, std::sort void* pointers
should have Comparator because comparing void* is undefined
https://eel.is/c++draft/expr.rel#4
instead of vice-versa.

- avoids malloc/free in gnc_gdate_set_today
- avoids unnecessary dmy->julian->dmy(*) conversion

(*) the GDate is typically used by gdate_to_time64 which performs a
julian->dmy conversion, and this is now avoided
@jralls
Copy link
Member

jralls commented Nov 3, 2025

win32 has 2038 bug? 😨

https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/time-time32-time64?view=msvc-170.

Mingw32 seens to turn on _USE_32BIT_TIME_T by default, but like I said, more research required. GLib might already have a work-around.

@christopherlam
Copy link
Contributor Author

christopherlam commented Nov 3, 2025

win32 has 2038 bug? 😨

https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/time-time32-time64?view=msvc-170.

Mingw32 seens to turn on _USE_32BIT_TIME_T by default, but like I said, more research required. GLib might already have a work-around.

Ok I've confirmed the bug exists. On windows. xaccTransGetDate has 32bit time_t crash.

Test: create an annual sx starting today. Manually change the windows date to 2037. Select Since Last Run to trigger the many sx to be presented for creation. Create them successfully.

Now change date to 2039, launch SLR which calls TransSetDate and it crashes.

@jralls
Copy link
Member

jralls commented Nov 3, 2025

Ok I've confirmed the bug exists. On windows. xaccTransGetDate has 32bit time_t crash.

That's interesting, xaccTransGetDate shouldn't use time_t at all. Can you get a stack trace?

@christopherlam
Copy link
Contributor Author

Ok I've confirmed the bug exists. On windows. xaccTransGetDate has 32bit time_t crash.

That's interesting, xaccTransGetDate shouldn't use time_t at all. Can you get a stack trace?

Can't seem to, I'm afraid. running gdb causes a python-not-found error on my seldom-used win machine.

@jralls
Copy link
Member

jralls commented Nov 4, 2025

Try this: Install drmingw in a MSYS2 or Mingw32 terminal window
pacman -S mingw-w64-i686-drmingw
Then run GnuCash with (still in the MSYS2/Mingw32 window)
catchesegv -v /c/Program\ Files\ (86)/gnucash/bin/gnucash.exe

That should print a stack trace and a bunch of other good stuff to the terminal.

@christopherlam
Copy link
Contributor Author

Ok this cmdline was tricky to start. Maybe the following?

7384E4C 082F0070 5B385FB0 00B6A57C  ntdll.dll!NtTerminateProcess+0xc
7735EE6F 00000000 0085D200 5B385FE4  ntdll.dll!RtlAcquireSRWLockExclusive+0x46f
7735E6C9 5B88F588 9D14830D 082F0070  ntdll.dll!RtlEnterCriticalSection+0x49
5B385FE4 082F6B68 00B6A584 000FFDB5  D3D10Warp.dll!Ordinal199+0x12c84
5B9E9CA2 00000000 5B9E9270 0085D268  d3d11.dll!D3D11CoreCreateLayeredDevice+0x23b2
5B9E9326 00B6A57C 00000000 000FFDED  d3d11.dll!D3D11CoreCreateLayeredDevice+0x1a36
5B9E92C5 0085D2AC 5BA01B57 00B6A52C  d3d11.dll!D3D11CoreCreateLayeredDevice+0x19d5
5B9E927D 00B6A52C 000FFD29 04968888  d3d11.dll!D3D11CoreCreateLayeredDevice+0x198d
5BA01B57 00B6A4F0 000FFD49 5B9E0510  d3d11.dll!D3D11CoreCreateLayeredDevice+0x1a267
5B9E0552 00B6A530 04968888 5BEB7F00  d3d11.dll!0x30552
5BEB7F1C 00000004 5BEB7F00 00000000  d2d1.dll!D2D1IsMatrixInvertible+0x32e7c
5BEE1E87 00000001 5BE7D670 00000000  d2d1.dll!D2D1IsMatrixInvertible+0x5cde7
5BE7D6A8 04968698 00000000 04AE8C30  d2d1.dll!D2D1CreateFactory+0x3ba8
5BEDC6EB 00000001 5BE7FC70 01664B48  d2d1.dll!D2D1IsMatrixInvertible+0x5764b
5BE7FCAD 04AE8C34 01664B20 01664A50  d2d1.dll!D2D1CreateFactory+0x61ad
5BEC3842 00000000 01664A50 01664A50  d2d1.dll!D2D1IsMatrixInvertible+0x3e7a2
5BEC378D 5BF28580 0085D3C8 5BE76DD9  d2d1.dll!D2D1IsMatrixInvertible+0x3e6ed
5BEC3F51 00000001 01769AA8 67D8B000  d2d1.dll!D2D1IsMatrixInvertible+0x3eeb1
5BE76DD9 01664A50 0085D460 0085D3E8  d2d1.dll!0x256dd9
67D0CB24 7734F1B4 8E81A244 77384B46  libcairo-2.dll!cairo_win32_scaled_font_get_device_to_logical+0x864
77384B46 67C511F0 67C50000 00000000  ntdll.dll!LdrDeleteEnclave+0x1a6
7734F19E 00000000 00000001 8E81A36C  ntdll.dll!RtlDeactivateActivationContextUnsafeFast+0x13e
7734C694 0D512C68 6B1FA010 00000000  ntdll.dll!LdrShutdownProcess+0x194
7737AB45 00000003 77E8F3B0 FFFFFFFF  ntdll.dll!RtlExitUserProcess+0xb5
75189283 00000003 0085D6C4 750F781F  KERNEL32.DLL!ExitProcess+0x13
750F7273 00000003 25E56ADB 0D512C68  msvcrt.dll!_exit+0x83
750F781F 00000003 00000001 00000000  msvcrt.dll!_initterm_e+0x1ef
750F7201 00000003 00010001 6B0B2014  msvcrt.dll!_exit+0x11
750EC678 0000000A 75147640 0000000B  msvcrt.dll!abort+0xe8
6B318184 0085DAD8 0085DC24 655D437C  libstdc++-6.dll!std::unexpected+0x628
653A05BA 0085DB0C 00000000 0085DB80  libgnc-engine.dll!boost::date_time::c_time::localtime+0x3a  [C:/gcdev64/msys2/mingw32/include/boost/assert/source_location.hpp @ 44]
653A07A3 0085DB30 0085DB40 00000018  libgnc-engine.dll!boost::date_time::day_clock<boost::gregorian::date>::get_local_time+0x23  [C:/gcdev64/msys2/mingw32/include/boost/assert/source_location.hpp @ 44]
653A06FD 0085DB8A 01760000 00CE179C  libgnc-engine.dll!boost::date_time::day_clock<boost::gregorian::date>::local_day_ymd+0x11  [C:/gcdev64/msys2/mingw32/include/boost/assert/source_location.hpp @ 44]
653A07B9 01760000 00000000 00000010  libgnc-engine.dll!boost::date_time::day_clock<boost::gregorian::date>::local_day+0x11  [C:/gcdev64/msys2/mingw32/include/boost/assert/source_location.hpp @ 44]
65339E35 00000010 00000003 0000006D  libgnc-engine.dll!GncDateTimeImpl::GncDateTimeImpl+0x15  [C:/gcdev64/gnucash/releases/src/gnucash-5.13/libgnucash/engine/gnc-datetime.cpp @ 883]
652AFDA7 016DA4C8 016C2B70 00000000  libgnc-engine.dll!GncDateTime::GncDateTime+0x27  [C:/gcdev64/gnucash/releases/src/gnucash-5.13/libgnucash/engine/gnc-datetime.cpp @ 766]
652A9C7C 00000000 00000001 0085DC78  libgnc-engine.dll!gnc_time+0x13  [C:/gcdev64/gnucash/releases/src/gnucash-5.13/libgnucash/engine/gnc-date.cpp @ 262]
6A018085 0D618080 016C2B70 0D2797B4  libgnc-expressions.dll!gnc_sx_get_current_instances+0x27  [C:/gcdev64/gnucash/releases/src/gnucash-5.13/libgnucash/app-utils/gnc-sx-instance-model.c @ 563]
6A28F89F 0C94E7A0 00000000 0B3F16A8  libgnc-gnome.dll!gnc_main_window_cmd_actions_since_last_run+0x88  [C:/gcdev64/gnucash/releases/src/gnucash-5.13/gnucash/gnome/gnc-plugin-basic-commands.c @ 492]
684183E1 00000001 0085DE80 0085DE50  libgobject-2.0-0.dll!g_closure_invoke+0x161
6842E3BB 00000000 0085DE50 0085DF68  libgobject-2.0-0.dll!g_param_spec_variant+0x3adb
6842E3BB 01760000 00000000 0F9AD5A0  libgobject-2.0-0.dll!g_param_spec_variant+0x3adb
750D7E09 04332270 91EAAE8F 00000000  msvcrt.dll!free+0x69
68433C59 00000000 00000000 00000000  libgobject-2.0-0.dll!g_signal_emit+0x29
5C3C4FEE 00000000 0085E1D8 0085E1C0  CoreMessaging.dll!MsgRelease+0xd7e
6842E3BB 049696A8 01682900 0C71FA00  libgobject-2.0-0.dll!g_param_spec_variant+0x3adb
68433C59 0085E36C 00000000 0FB41880  libgobject-2.0-0.dll!g_signal_emit+0x29
68433C59 0C71FA00 00000067 00000000  libgobject-2.0-0.dll!g_signal_emit+0x29
6B74D1E5 0C71FA00 043F9258 048B8490  libgtk-3-0.dll!gtk_widget_activate+0x75
6B5FB2EC 049696A8 0C71FA00 00000001  libgtk-3-0.dll!gtk_menu_shell_activate_item+0xfc
6B5F6D34 00000000 00000000 00000000  libgtk-3-0.dll!gtk_menu_item_get_type+0xb14

@christopherlam
Copy link
Contributor Author

christopherlam commented Nov 4, 2025

another more comprehensive one. latest nightly. date changed to 2055 or something. crash when launching SLR. seems to crash on

GncDateTime gncdt;
with today (ie 2055) date.

catchsegv.txt

@jralls
Copy link
Member

jralls commented Nov 4, 2025

Perfect.

Dang, they get the current date-time with std::localtime_r(). Fortunately in normal use that won't break for 13 more years and by then surely Guile will work on 64-bit Windows and we'll have ditched 32-bit.

They've also changed the exception they throw from their own bad_year (it's still there, they just don't throw it) to std::out_of_range so we need to fix our catch statements. That will at least avert this crash though it will make for some nonsensical GDates.

christopherlam referenced this pull request Dec 29, 2025
Boost::date_time has changed to throwing a std::out_of_range instead of a
boost::date_time::gregorian::bad_year when a date is outside of the
1400-9999 year range it can deal with.

We also recently discovered that it will use the system localtime function
when creating a new date which can lead to a 2038 failure. Use std::chrono
to resolve that problem.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants