Cross-platform C SDK logo

Cross-platform C SDK

Error management

❮ Back
Next ❯
This page has been automatically translated using the Google Translate API services. We are working on improving texts. Thank you for your understanding and patience.

There is always one more bug to fix. Ellen Ullman


Developing software of a certain size and complexity can become a hellish task, if we do not adopt concrete measures for the prevention and rapid localization of programming bugs. We will talk below about some strategies that we have used in the development of NAppGUI and that you can apply in your own projects.


1. Exhaustive tests

Ensuring that our software is free of errors is as "simple" as performing a test for each and every one of the cases that will face the program (Figure 1).

Flow chart where the path a program follows is based on its entries.
Figure 1: In the exhaustive tests all possible combinations of the input data are used.

From trivial theoretical examples, we see that we are dealing with an exponential problem (Figure 2), which will overflow the resources of any system with relatively few input variables. Therefore, we can intuit that it will be impossible to guarantee that our software is free of errors since it will not be feasible to reproduce all of its use cases. However, we can define a strategy that helps us to minimize the impact that these will have on the final product, detecting them and correcting them as soon as possible.

Graph that grows exponentially depending on the input variables.
Figure 2: With only 9 input variables (in the 0..99 range) the computation resources will overflow.

2. Static analysis

Static analysis is the one that is carried out before executing the program and consists of two parts: The use of standards where rules and quality controls are applied during the code's own writing. And compiler warnings that will help us locate potential errors at compile time.

2.1. Standards

The use of standards, understood as rules that we follow when programming, is essential when it comes to maintaining minimum levels of quality in our projects (Figure 3). If not applied, a program of a certain size will become anarchic, illegible, difficult to maintain and complicated to understand. In this scenario it will be easy to add new errors as we manipulate the source code.

Show bugs running towards a written code page without using standards.
Figure 3: The use of standards will reduce the likelihood of bugs.

In reality, it is difficult to differentiate between good and bad standards, since they will depend on the type of project, programming languages, company philosophy and objectives to be prioritized. We can see them as a Style Guide that evolves over time from the hand of experience. What is really important is to become aware of its usefulness, define them and apply them. For example, if we decide to name variables with descriptive identifiers in English and underscore (product_code), all our code should comply with this rule without exception. Let's look at some of the standards we apply within NAppGUI. They are not the best nor do they have to adapt to all cases. They are just ours:

  • Use a reduced subset of the language. For example, expressions of the type *((int*)block + i++) = i+1, we are totally forbidden. They are perfectly valid in C but not very readable and confusing. Some programmers think that cryptic and compact code is much more maintainable, but we think they are wrong. The following sentence sums up this decision perfectly:
Many programming languages contain good and bad parts. I discovered that I can be a better programmer using only the good parts and discarding the bad ones. After all, how can you create something good with bad parts? - Douglas Crockford.
  • Comments are forbidden, except on rare occasions and very justified. If something needs a comment, rewrite it. A comment that minimally contradicts the code that seeks to clarify produces the opposite effect of the expected when writing it. And it is very simple that they become obsolete.
  • Reduced and clean public interfaces. The header files (*.h) they suppose a great level of abstraction since they reduce the connections between software components (Figure 4). They allow condensing, as an index, hundreds or thousands of lines of code in just fifteen or twenty public functions. It is completely forbidden to include type definitions (they will go in the *.hxx), comments (of course) and documentation blocks in .h files.
  • It shows how a header file significantly simplifies the complexity of the software, reducing its connections.
    Figure 4: The headers *.h they suppose a great level of abstraction hiding the complexity of the solution (a). They facilitate a horizontal development, based on the problem, versus vertical learning based on APIs (b). They help the linker to reduce the size of the executable (c).
  • Opaque objects. Object definitions (struct _object_t) will be carried out within the implementation files (*.c) and never in the *.h. The objects will be manipulated with public functions that accept pointers to them, always hiding the fields that compose them. This point, together with the previous one of the interfaces, perfectly delimits the barriers between modules, marking clearly when a problem ends and another.

The first two rules help reduce the internal complexity of a module by making it as readable and less cryptic as possible. We could enrich them with others about indentation, style, named variables, etc. We follow with more or less rigor the advice of the great book The Practice of Programming (Figure 5).

Book cover of The Practice of Programming by Brian W. Kernighan y Rob Pike
Figure 5: The Practice of Programming by Brian W. Kernighan and Rob Pike is a good source of inspiration to define your own programming style.

2.2. Compiler warnings

The compiler is our great ally when it comes to examining the code in search of possible failures (Figure 6). Activating the highest possible level of warnings is essential to reduce errors derived from the conversion of types, uninitialized variables, code not reachable, etc. All projects created with NAppGUI will activate the highest level of warnings possible, equivalent to -Wall -Wpedantic on all platforms (Figure 7).

Several warning of the Xcode compiler.
Figure 6: Correcting all compiler warnings should be a priority.
Policy of warnings in an Xcode project.
Figure 7: NAppGUI activates the highest level of warnings possible.

3. Dynamic analysis

The dynamic analysis is performed once the program is running. Here our main weapon is the self-validations, implemented as sentences Asserts. The asserts are checks distributed throughout the length of the source code, which are checked at run time each time the program passes through them. If a sentence is resolved as FALSE, the process will stop and an informative window will be displayed (Figure 8).

1
2
3
4
5
6
void layout_set_row_margin(Layout *layout, const uint32_t row, const real32_t margin)
{
    cassert_no_null(layout);
    cassert_msg(row < layout->num_rows, "'row' out of range");
    ...
}
Window displayed after activating an assert.
Figure 8: Window displayed after the activation of an assert.

3.1. Disable Asserts

Within the code of the NAppGUI SDK, more than 5000 asserts have been distributed, located in strategic points, which constantly evaluate the coherence and integrity of the software. Obviously, this number will grow after each revision, as more functionality is integrated. This makes the SDK a real minefield, where any errors in the use of the API functions will be automatically notified to the programmer. Depending on the build configuration that we are using, the asserts will be activated or deactivated:

  • Debug: The assert statements are activated.
  • Release: The assert statements are deactivated.
  • ReleaseWithAssert: As its name suggests, it activates all the Release optimizations, but it leaves the assert statements activated.

3.2. Debugging the program

When an assert is activated, the program stops just at the point of the check, showing the confirmation window of the assert. If we press the button [Debug], we will access the call stack (Figure 9), which is the current stack of function calls, from your own main() up to the current stop point Stack Segment. By browsing the stack we can check the values of variables and objects at any call level. This will help us identify the source of the error, since the cause may be some levels below detection.

Call stack capture, debugging in Visual Studio
Figure 9: Call stack while debugging the assert of the previous example.

3.3. Error log

A Log of execution is a file where the program is dumping information about its status or anomalies detected. It can be very useful to know the cause of a failure when the software has already been distributed and it is not possible to debug it. NAppGUI automatically creates a log file for each application located in the data directory of the application APP_DATA\APP_NAME\log.txt , for example C:\Users\USER\AppData\Roaming\HelloWorld\log.txt.

1
2
3
4
5
[15:42:29] Starting log for 'HelloWorld'
[15:42:29] TextView created: [0x6FFC7A30]
[15:42:32] Assertion failed (c:\\nappgui_1_0\\src\\gui\\layout.c:638): "'row' out of range"
[15:42:32] Assertion failed (c:\\nappgui_1_0\\src\\core\\array.c:512): "Array invalid index"
[15:42:34] You have an execution log in: 'C:\\Users\\USUARIO\\AppData\\Roaming\\HelloWorld\\log.txt'

As you can see, the asserts are automatically redirected to the log file. It is possible to disable this writing by unchecking the check 'Write assert info in log' of the information window. You can also add your own messages using the method log_printf.

1
log_printf("TextView created: [0x%X]", view);

3.4. Memory Auditor

The memory manager of NAppGUI Heap - Memory manager, has an associated auditor that verifies that there are no leaks of memory (leaks) after each execution of each application that uses the SDK. This is a great advantage with respect to the use of external utilities, since dynamic memory checks are always being carried out and not in isolated stages of development.

1
2
3
4
5
6
7
8
NApp memory statistics
======================
Total a/dellocations: 61, 61
Total rellocations (reals): 0, 0
Total bytes a/dellocated: 6136, 6136
Max bytes allocated: 6126
Real allocations: 2 pages of 4096 bytes
======================
❮ Back
Next ❯