Friday, January 19, 2024

The curse of band-aid solutions

The flexibility inherent in software development can become a curse because it allows developers to implement quick and dirty fixes without fully understanding the root cause of a problem. Suppose you are tasked with writing a factorial function, knowing that factorial(1) = 1 and factorial(2) = 2. You write a function to satisfy these conditions:
double factorial(size_t a) {
    return a;
}
Then, during live tests, you realize that the function should return 6 for an input of 3, and 24 for an input of 4. Instead of investigating the correct mathematical approach, you modify your code by adding if statements, because that is what you know:
double factorial(size_t a) {
    if (a < 3) return a;
    else if (a < 4) return a*(a-1);
    else return a*(a-1)*(a-2);
}
You add new if conditions as failed tests pile up. For such a simple case, all developers agree that this is not the way to go. However, as problems become more complex, they often lack straightforward solutions that a single line prompt to ChatGPT can provide. Also, there is always pressure to get things done quickly and you don't have time to get to the bottom of things. Most engineers yield under pressure which over time leads to a growing mess, dissatisfaction, burnout and resignation.
The optimum strategy is to use a band-aid solution in the short term, make a note of it (preferably in an issue tracking system), and as soon as you get a chance, spend time on how your solution could fail and make it more robust. It is crucial to be interested in the problem rather than merely viewing it as something to be gotten rid of. You never attain perfection, you approach it asymptotically. Those who are curious and have the discipline to conduct thorough root cause analysis will become 100X engineers. Those who don't will be replaced by AI. 

Tuesday, January 16, 2024

Visual Studio C++ debug and release configurations

Visual Studio 2019 C++ debug configuration is more forgiving than release. Examples that a debug build allows but release build does not:
enum E {a = 0, b=1};
E r[3] = {0}; //causes "initializing: cannot convert from int to E"

double a = 0;
E d = (E) a; //causes "typecast: cannot convert from double to E"

typedef struct {
    double d[3] {0}; //causes "an in-class initializer is not allowed for a member 
of an anonymous union in non-class scope"
} S; class C { double s[3] {0}; //causes "cannot specify explicit initializer for arrays" }
Therefore, to make sure that the C/C++ code you generated from Simulink model is suitable for building, in your nightly tests, build in release configuration.

Monday, January 15, 2024

Visual Studio: Include all *.c and *.cpp by default

In Eclipse CDT, implementation files (*c., *.cpp) are included until you exclude them. In Visual Studio 2019, it is the reverse, i.e. files are excluded until you manually include them in your project. To include all files automatically and just exclude the file sim\ert_rtw\ert_main.c file, open your *.vcxproj file with an editor, remove all <CICompile Include = .../> lines and use the following:
<ItemGroup>
    <ClCompile Include="**\*.cpp" />
    <ClCompile Include="**\*.c" Exclude="sim\ert_rtw\ert_main.c" />
</ItemGroup>

Friday, December 29, 2023

Windows batch file to run an exe and check its result

Windows batch file to run an exe and check its result. If you get the strange ") was unexpected at this time" error when you run it, try adding a comment line before that ")", it usually fixes it (and use "rem" instead of "::"). If you get "3 was unexpected at this time", move inner for loop out of the nested loop:

Thursday, December 28, 2023

The unbearable lightness of C

We have a Simulink project from which I generate C code to use in a Visual Studio C++ project. The Simulink project works fine, I can build the C code without any errors, but when I ran the C executable, I got an access violation error due to trying to write to address 0x0. The C project was working fine for previous versions. 

I initially identified the revision where this error first appeared. I reviewed the changed code and couldn't find anything wrong.

After a week of debugging I found out that it was due to an off-by-one error; An array was defined with size 47 using Simulink function ssSetNumDiscState(S, 47) in mdlInitializeSizes(...), but later in function mdlInitializeConditions(...), a for loop with upper bound of 48 was executed which resulted in writing to the memory adjacent to the allocated section for that array. 
static void mdlInitializeConditions(SimStruct *S) {
    real_T *states = ssGetRealDiscStates(S);
    for (int i=0; i < 48; i++) {
    	*(states + i) = 0;
    }
}
It has nothing to do with the latest change in the sense that it was not related to the logic of the change. Instead, that change altered the memory mapping of the build, putting another array (TUBufferPtr) after the first and the overflow caused the value at TUBufferPtr[0] to become 0x0 (NULL). When the program tried to write to the address represented by TUBufferPtr[0], it naturally caused an access violation because writing to address 0x0 is not allowed.
When I looked at the repository history, I saw that this error was introduced 3 years ago and for all these years, it did not become visible! The access violation only occurred when other unrelated code updates caused the compiler to arrange memory slightly differently.

This is also one of the reasons why sometimes C programs behave differently between debug and release builds or on different versions (service packs) of the same operating system. That discrepancy is an indication of an error hiding somewhere in your program. Another way such an error can become visible is when you have it in your C++ DLL that you call from your Java program. One day you update your JDK and your program crashes because DLL and JVM share the same memory space. Naturally, your first inclination is to blame the JDK update but in reality there is a buffer overflow in your DLL code.

You can never say for sure that modifying a code in module A won't have an effect on an unrelated module B. As long as these modules are in the same process, i.e. use the same memory, the compiler might put them side by side and an error in A can overflow to B. Functionally distant modules can become "close relatives" in memory address space.

In a way, I was fortunate that the overwrite contained zero values. If it had used some value that was a valid address for the application to write to, it would cause seemingly random behavior and would have been a lot more fun (!)

Note that a typical static code analyzer would not able to catch this problem because we are defining the data structure with Simulink specific ssSetNumDiscState and getting it with ssGetRealDiscStates functions.

I solved the problem by adding #define NUMBER_OF_DISCRETE_STATES (47) and using that define in both ssSetNumDiscState() and mdlInitializeConditions(). This case study also serves as a cautionary tale illustrating why you should use defined constants rather than magic numbers.

In my nightly automated tests I was only checking if exe was generated which only proved that it compiled. I added running the exe and checking if it finished successfully because an access violation can only occur at runtime.

Wednesday, December 13, 2023

Downloading DTED from the internet

You can downlad DTED 2 files (1 arc-second ~ 30m) from USGS EarthExplorer by first creating an account, clicking on the link in verification email and then following these steps:


Monday, December 11, 2023

Avoiding catastrophic cancellation

Catastrophic cancellation occurs when subtracting two nearly equal numbers, leading to a significant loss of precision due to limited precision of floating-point numbers in a computer. Below are some examples and their better versions (reference):
log(a)−log(b) = log(a/b)

Computing cos(x)−1 for small values of x --> cos(x) ≈ 1−(x^2)/2