Skip to content

Commit 58c7337

Browse files
committed
Release v1.8.8: Add gspy.error() for fatal error signaling
1 parent 64042a9 commit 58c7337

File tree

10 files changed

+424
-73
lines changed

10 files changed

+424
-73
lines changed

GSPy.cpp

Lines changed: 60 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -7,57 +7,71 @@
77

88
extern "C" void GSPy(int methodID, int* status, double* inargs, double* outargs)
99
{
10-
// Initialize the logger once using the new ConfigManager
11-
static bool logger_initialized = false;
12-
if (!logger_initialized) {
13-
std::string log_filename = GetLogFilename();
14-
int log_level = GetLogLevel(); // Get from config file
15-
InitLogger(log_filename, static_cast<LogLevel>(log_level));
16-
SetLogLevelFromInt(log_level); // Apply log level atomically
17-
logger_initialized = true;
18-
}
10+
try {
11+
// Initialize the logger once using the new ConfigManager
12+
static bool logger_initialized = false;
13+
if (!logger_initialized) {
14+
std::string log_filename = GetLogFilename();
15+
int log_level = GetLogLevel(); // Get from config file
16+
InitLogger(log_filename, static_cast<LogLevel>(log_level));
17+
SetLogLevelFromInt(log_level); // Apply log level atomically
18+
logger_initialized = true;
19+
}
1920

20-
LogDebug("GSPy called with MethodID: " + std::to_string(methodID));
21+
LogDebug("GSPy called with MethodID: " + std::to_string(methodID));
2122

22-
*status = 0;
23-
std::string errorMessage;
23+
*status = 0;
24+
std::string errorMessage;
2425

25-
switch (methodID)
26-
{
27-
case 0: // Initialize
28-
if (!InitializePython(errorMessage)) {
29-
SendErrorToGoldSim(errorMessage, status, outargs);
30-
}
31-
break;
26+
switch (methodID)
27+
{
28+
case 0: // Initialize
29+
if (!InitializePython(errorMessage)) {
30+
SendErrorToGoldSim(errorMessage, status, outargs);
31+
}
32+
break;
3233

33-
case 1: // Calculate
34-
ExecuteCalculation(inargs, outargs, errorMessage);
35-
if (!errorMessage.empty()) {
36-
SendErrorToGoldSim(errorMessage, status, outargs);
37-
}
38-
break;
39-
40-
case 2: // Report Version
41-
LogInfo("Reporting version to GoldSim: " + std::string(GSPY_VERSION));
42-
outargs[0] = GSPY_VERSION_DOUBLE;
43-
break;
44-
45-
case 3: // Report Arguments
46-
// We need to initialize to read the config to get argument counts
47-
if (!InitializePython(errorMessage)) {
48-
SendErrorToGoldSim(errorMessage, status, outargs);
49-
return;
50-
}
51-
outargs[0] = static_cast<double>(GetNumberOfInputs());
52-
outargs[1] = static_cast<double>(GetNumberOfOutputs());
53-
break;
34+
case 1: // Calculate
35+
ExecuteCalculation(inargs, outargs, errorMessage);
36+
if (!errorMessage.empty()) {
37+
LogDebug("Sending error to GoldSim: " + errorMessage);
38+
SendErrorToGoldSim(errorMessage, status, outargs);
39+
LogDebug("Error sent to GoldSim successfully");
40+
}
41+
break;
5442

55-
case 99: // Cleanup
56-
FinalizePython();
57-
break;
43+
case 2: // Report Version
44+
LogInfo("Reporting version to GoldSim: " + std::string(GSPY_VERSION));
45+
outargs[0] = GSPY_VERSION_DOUBLE;
46+
break;
5847

59-
default:
60-
*status = 1;
61-
break;
48+
case 3: // Report Arguments
49+
// We need to initialize to read the config to get argument counts
50+
if (!InitializePython(errorMessage)) {
51+
SendErrorToGoldSim(errorMessage, status, outargs);
52+
return;
53+
}
54+
outargs[0] = static_cast<double>(GetNumberOfInputs());
55+
outargs[1] = static_cast<double>(GetNumberOfOutputs());
56+
break;
57+
58+
case 99: // Cleanup
59+
FinalizePython();
60+
break;
61+
62+
default:
63+
*status = 1;
64+
break;
65+
}
66+
}
67+
catch (const std::exception& e) {
68+
LogError(std::string("C++ exception caught in GSPy: ") + e.what());
69+
std::string errorMsg = std::string("C++ exception: ") + e.what();
70+
SendErrorToGoldSim(errorMsg, status, outargs);
71+
}
72+
catch (...) {
73+
LogError("Unknown C++ exception caught in GSPy");
74+
std::string errorMsg = "Unknown C++ exception in GSPy";
75+
SendErrorToGoldSim(errorMsg, status, outargs);
6276
}
6377
}

GSPy.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
// Version information
55
#define GSPY_VERSION_MAJOR 1
66
#define GSPY_VERSION_MINOR 8
7-
#define GSPY_VERSION_PATCH 7
7+
#define GSPY_VERSION_PATCH 8
88

99
// Automatically generate version string and double from components
1010
#define STRINGIFY(x) #x

GSPy_Error.cpp

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#include "GSPy_Error.h"
2+
#include "Logger.h"
23
#include <cstring> // For strncpy_s
34

45
// The static buffer now lives here, dedicated to our error messenger.
@@ -7,14 +8,16 @@ static char errorMessageBuffer[256];
78
// The function's implementation is moved here.
89
void SendErrorToGoldSim(const std::string& message, int* status, double* outargs)
910
{
10-
// Copy the message into our static buffer, ensuring it's null-terminated.
11-
strncpy_s(errorMessageBuffer, message.c_str(), sizeof(errorMessageBuffer) - 1);
12-
errorMessageBuffer[sizeof(errorMessageBuffer) - 1] = '\0'; // Ensure null termination
13-
14-
// Tell GoldSim a fatal error with a message has occurred.
15-
* status = -1;
16-
17-
// GoldSim expects the first output argument to be a pointer to the message.
18-
uintptr_t* pAddr = (uintptr_t*)outargs;
19-
*pAddr = (uintptr_t)errorMessageBuffer;
11+
// For 64-bit DLLs running in separate process space, pointer-based error
12+
// messages don't work. Instead, we just set the status to indicate failure.
13+
// The error message is logged, and GoldSim will show a generic error.
14+
15+
// Log the full error message
16+
LogError("Fatal error to report to GoldSim: " + message);
17+
18+
// Tell GoldSim a fatal error occurred
19+
// Use status = 1 for generic failure (not -1 which expects a pointer)
20+
*status = 1;
21+
22+
LogDebug("Status set to: " + std::to_string(*status));
2023
}

PythonManager.cpp

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ static PyObject* pFunc = nullptr;
3434
// Python-Callable Logging Function
3535
// =================================================================
3636

37+
// Global pointer to store error message for GoldSim (static - internal use only)
38+
static std::string* g_python_error_message = nullptr;
39+
3740
// Python-callable logging function (static - internal use only)
3841
static PyObject* PythonLog(PyObject* self, PyObject* args) {
3942
const char* message;
@@ -66,9 +69,40 @@ static PyObject* PythonLog(PyObject* self, PyObject* args) {
6669
Py_RETURN_NONE;
6770
}
6871

72+
// Python-callable error function that signals a fatal error to GoldSim
73+
// NOTE: For 64-bit DLLs running in separate process space, we cannot pass
74+
// pointers to GoldSim. Instead, we raise a Python exception that C++ will catch.
75+
static PyObject* PythonError(PyObject* self, PyObject* args) {
76+
const char* message;
77+
78+
// Parse arguments: message (required)
79+
if (!PyArg_ParseTuple(args, "s", &message)) {
80+
return nullptr;
81+
}
82+
83+
// Log the error
84+
LogError(std::string(message));
85+
86+
// Store the error message for GoldSim
87+
if (g_python_error_message == nullptr) {
88+
g_python_error_message = new std::string(message);
89+
} else {
90+
*g_python_error_message = message;
91+
}
92+
93+
LogDebug("gspy.error() called, message stored: " + std::string(message));
94+
95+
// Raise a Python RuntimeError with the message
96+
// This will cause PyObject_CallObject to return NULL, triggering our error handling
97+
PyErr_SetString(PyExc_RuntimeError, message);
98+
99+
return nullptr; // Return NULL to indicate an exception occurred
100+
}
101+
69102
// Method definition for the gspy module
70103
static PyMethodDef GSPyMethods[] = {
71104
{"log", PythonLog, METH_VARARGS, "Write a message to the GSPy log file"},
105+
{"error", PythonError, METH_VARARGS, "Signal a fatal error to GoldSim and terminate the simulation"},
72106
{nullptr, nullptr, 0, nullptr} // Sentinel
73107
};
74108

@@ -360,6 +394,12 @@ void FinalizePython() {
360394
Py_XDECREF(pFunc);
361395
Py_XDECREF(pModule);
362396

397+
// Clean up the error message pointer
398+
if (g_python_error_message != nullptr) {
399+
delete g_python_error_message;
400+
g_python_error_message = nullptr;
401+
}
402+
363403
if (Py_IsInitialized()) {
364404
// LOGGING: Confirm that we are shutting down the interpreter.
365405
LogInfo("Shutting down Python interpreter.");
@@ -448,6 +488,33 @@ void ExecuteCalculation(double* inargs, double* outargs, std::string& errorMessa
448488
PyObject* pResultTuple = PyObject_CallObject(pFunc, pArgs);
449489
Py_DECREF(pArgs);
450490

491+
// 2.5. Check if Python raised an exception (including from gspy.error())
492+
if (pResultTuple == nullptr) {
493+
// Python exception occurred
494+
if (g_python_error_message != nullptr && !g_python_error_message->empty()) {
495+
// gspy.error() was called - use the stored message
496+
errorMessage = "GSPy Error: " + *g_python_error_message;
497+
LogDebug("Python signaled fatal error via gspy.error(): " + *g_python_error_message);
498+
g_python_error_message->clear();
499+
} else {
500+
// Some other Python exception - get the error details
501+
PyErr_Print();
502+
errorMessage = "Python exception occurred (see log for details)";
503+
LogError(errorMessage);
504+
}
505+
return;
506+
}
507+
508+
// 2.6. Check if gspy.error() was called but Python still returned successfully
509+
// (This shouldn't happen with the new implementation, but keep as safety check)
510+
if (g_python_error_message != nullptr && !g_python_error_message->empty()) {
511+
errorMessage = "GSPy Error: " + *g_python_error_message;
512+
LogDebug("Python signaled fatal error: " + errorMessage);
513+
g_python_error_message->clear();
514+
Py_DECREF(pResultTuple);
515+
return;
516+
}
517+
451518
// 3. Delegate result processing
452519
if (!MarshalOutputsToCpp(pResultTuple, config["outputs"], outargs, errorMessage)) {
453520
// MarshalOutputsToCpp handles its own error logging and Py_DECREF

docs/CHANGELOG.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,86 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
## [1.8.8] - 2025-11-18
6+
7+
### Added
8+
- **Fatal Error Signaling:** New `gspy.error()` function allows Python code to signal critical errors and stop GoldSim simulations
9+
* **Function:** `gspy.error(message)` - Signals a fatal error and terminates the simulation
10+
* **Use Case:** Critical errors where results would be invalid (division by zero, missing required data, invalid configuration)
11+
* **Behavior:** Raises Python RuntimeError, C++ catches it, logs detailed error, sets status = 1, simulation stops
12+
* **Error Details:** Full error message and Python traceback written to log file
13+
* **GoldSim Display:** Shows "Error in external function. Return code 1." (generic message due to process separation)
14+
* **Architecture Note:** 32-bit GoldSim / 64-bit DLL run in separate processes, preventing direct error message passing
15+
16+
### Enhanced
17+
- **Error Handling Documentation:** Comprehensive documentation for both error handling approaches
18+
* `docs/Using_gspy_error.md` - Quick start guide with examples
19+
* `docs/Error_Handling_Best_Practices.md` - Complete guide with use cases and patterns
20+
* `docs/Error_Handling_Quick_Reference.md` - Quick reference card
21+
* `docs/Slide_Updates_Error_Handling.md` - Updated presentation content
22+
* `docs/CHANGELOG_Error_Handling.md` - Detailed version history and migration guide
23+
* `examples/Error Handling Demo/` - Working demonstration with test cases
24+
25+
### Improved
26+
- **Exception Handling:** Added comprehensive try-catch blocks in main GSPy function
27+
* Catches and logs C++ exceptions with detailed error messages
28+
* Prevents crashes and provides better error diagnostics
29+
* Ensures clean error reporting to GoldSim
30+
31+
- **Python Exception Detection:** Enhanced ExecuteCalculation to properly handle Python exceptions
32+
* Detects when Python raises exceptions (including from gspy.error())
33+
* Retrieves stored error messages for detailed logging
34+
* Provides fallback error handling for unexpected exceptions
35+
36+
### Technical Details
37+
- **Implementation:**
38+
* Added `PythonError()` function that raises Python RuntimeError
39+
* Added global error message storage (`g_python_error_message`)
40+
* Modified `ExecuteCalculation()` to detect Python exceptions and retrieve stored messages
41+
* Updated `SendErrorToGoldSim()` to use status = 1 (compatible with separate process architecture)
42+
* Added exception handling wrapper in main `GSPy()` function
43+
* Added cleanup in `FinalizePython()` for error message pointer
44+
45+
- **Error Handling Flow:**
46+
1. Python calls `gspy.error(message)`
47+
2. Message stored in C++ and Python RuntimeError raised
48+
3. C++ detects NULL return from Python
49+
4. C++ retrieves stored message and logs it
50+
5. C++ sets status = 1 (failure)
51+
6. GoldSim detects failure and stops simulation
52+
7. User checks log file for detailed error information
53+
54+
- **Process Architecture:** Solution accounts for 32-bit GoldSim / 64-bit DLL separate process requirement
55+
* Memory pointers cannot be shared across process boundaries
56+
* Error messages written to log file instead of passed via pointers
57+
* This is the correct approach for the 32-bit/64-bit architecture
58+
59+
### Comparison: Error Handling Approaches
60+
61+
| Approach | Function | Simulation | GoldSim Message | Details Location |
62+
|----------|----------|------------|-----------------|------------------|
63+
| **Graceful Degradation** | `gspy.log()` only | Continues | None | Log file |
64+
| **Fatal Error** | `gspy.error()` | **Stops** | "Return code 1" | Log file |
65+
66+
### Migration Guide
67+
- **No breaking changes** - Existing code continues to work unchanged
68+
- **Optional enhancement** - Add `gspy.error()` for critical error handling
69+
- **Recommended pattern:**
70+
```python
71+
try:
72+
result = calculate(args)
73+
return (result,)
74+
except ValueError as e:
75+
# Recoverable - use graceful degradation
76+
gspy.log(f"Warning: {e}", 1)
77+
return (default_value,)
78+
except Exception as e:
79+
# Critical - stop simulation
80+
gspy.log(traceback.format_exc(), 0)
81+
gspy.error(f"Fatal: {e}")
82+
return (0.0,)
83+
```
84+
585
## [1.8.7] - 2025-11-15
686

787
### Fixed

0 commit comments

Comments
 (0)