Skip to Content

Tutorial

Here is a detailed tutorial for C Traceback.

If you are not sure how to compile a C project with our library. check Get Started.

Project Setup

For demonstration, let us start with a simple vector calculation project. It should do four things:

  1. Allocate memory for vector.
  2. Initialize values for vector.
  3. Divide every vector element by denominator.
  4. Clean up.

Let us look at them one by one.

/** * \file: project.c */ #include <stdio.h> #include <stdlib.h> #include "c_traceback.h" int main(void) { const size_t n = 1000; // Vector size const double denominator = 123.0; /* Allocate memory */ /* Initialize values */ /* Division */ /* Clean up */ return 0; error_clean_up: /* Error clean up */ return 1; }

Allocate memory

For demonstrative purpose, let us try allocating memory by using a function. Below is what I would write in C99:

int alloc_memory(double **arr, const size_t n) { *arr = malloc(n * sizeof(double)); if (!(*arr)) { return 1; // Failed to allocate memory } return 0; }

While it is fine to return 1 for failure, it is better to have an error traceback and error message, so that users know what happened without having to go through your source code.

THROW Macro

We can first make use of the THROW macro. Instead of returning a value, we throw an error instead.

void alloc_memory(double **arr, const size_t n) { *arr = malloc(n * sizeof(double)); if (!(*arr)) { // Failed to allocate memory THROW(CTB_MEMORY_ERROR, "Failed to allocate memory for vector!"); } }

However, this doesn’t do much by itself as it only silently records an error. We will also have to modify the outer scope.

TRY Macro

On the outer scope, we can wrap our function call with TRY, which returns an boolean error value. On top of that, TRY will manage the call stack automatically for you.

/* Allocate memory */ double *arr; if (!TRY(alloc_memory(&arr, n))) { goto error_clean_up; }

TRY_GOTO Macro

TRY_GOTO does the same thing as TRY, but it jumps to a label when the expression fails.

/* Allocate memory */ double *arr; TRY_GOTO(alloc_memory(&arr, n), error_clean_up);

Clean up

ctb_dump_traceback Function

Now, add ctb_dump_traceback at the error handling. It will log the traceback to stderr and reset the internal error status.

int main(void) { /* Clean up */ free(arr); return 0; error_clean_up: /* Error clean up */ free(arr); ctb_dump_traceback(); return 1; }

Below is the final code. Lets set n to 1000000000000000 to see what happens.

/** * \file: example_tut01.c */ #include <stdio.h> #include <stdlib.h> #include "c_traceback.h" void alloc_memory(double **arr, const size_t n); int main(void) { const size_t n = 1000000000000000; // Vector size const double denominator = 123.0; /* Allocate memory */ double *arr; TRY_GOTO(alloc_memory(&arr, n), error_clean_up); /* Initialize values */ /* Division */ /* Clean up */ free(arr); return 0; error_clean_up: /* Error clean up */ free(arr); ctb_dump_traceback(); return 1; } void alloc_memory(double **arr, const size_t n) { *arr = malloc(n * sizeof(double)); if (!(*arr)) { // Failed to allocate memory THROW(CTB_MEMORY_ERROR, "Failed to allocate memory for vector!"); } }

Output

n should be too big to be allocated on your computer. You should see the traceback below.

If you don’t see the traceback, turn off optimization flags by using -O0 as compilers could optimize the malloc away.

──────────────────────────────────────────────────────────────────────────────── Traceback (most recent call last): (#00) File "/home/alvinng/Desktop/c_traceback/examples/example_tut01.c", line 19 in main: alloc_memory(&arr, n) (#01) File "/home/alvinng/Desktop/c_traceback/examples/example_tut01.c", line 42 in alloc_memory: <Error thrown here> MemoryError: Failed to allocate memory for vector! ────────────────────────────────────────────────────────────────────────────────

Initialize vector

Again, for demonstrative purpose, we will write a function to initialize the vector.

void initialize_vec(double *arr, const size_t n) { if (!arr) { // Null Pointer THROW(CTB_NULL_POINTER_ERROR, "Received null pointer."); } for (int i = 0; i < n; i++) { arr[i] = i; } }

In main, we should use TRY_GOTO to wrap the function call.

ctb_install_signal_handler Function

Wait, what if initialize_vec received an n that’s larger than the actual vector size? There is no way to check it inside the function, and it will trigger a segmentation fault.

While we cannot avoid segmentation faults, we can use a signal handler to dump the traceback when the program crashes. Simply call ctb_install_signal_handler() at program entry.

Below is the final code. Let’s try passing 1000 * n to trigger a segmentation fault.

/** * \file: example_tut02.c */ #include <stdio.h> #include <stdlib.h> #include "c_traceback.h" void alloc_memory(double **arr, const size_t n); void initialize_vec(double *arr, const size_t n); int main(void) { ctb_install_signal_handler(); const size_t n = 1000; // Vector size const double denominator = 123.0; /* Allocate memory */ double *arr; TRY_GOTO(alloc_memory(&arr, n), error_clean_up); /* Initialize values */ TRY_GOTO(initialize_vec(arr, 1000 * n), error_clean_up); /* Division */ /* Clean up */ free(arr); return 0; error_clean_up: /* Error clean up */ free(arr); ctb_dump_traceback(); return 1; } void alloc_memory(double **arr, const size_t n) { *arr = malloc(n * sizeof(double)); if (!(*arr)) { // Failed to allocate memory THROW(CTB_MEMORY_ERROR, "Failed to allocate memory for vector!"); } } void initialize_vec(double *arr, const size_t n) { if (!arr) { // Null Pointer THROW(CTB_NULL_POINTER_ERROR, "Received null pointer."); } for (int i = 0; i < n; i++) { arr[i] = i; } }

Output

You should see the traceback below.

-------------------------------------------------------------------------------- Traceback (most recent call last): (#00) File "/home/alvinng/Desktop/c_traceback/examples/example_tut02.c", line 25 in main: initialize_vec(arr, 1000 * n)1dfs2 SignalSegmentationFault -------------------------------------------------------------------------------- Segmentation fault (core dumped)
There is no colors and fancy output in signal handlers to ensure that it works on all systems.

TRACE_BLOCK Macro

Similar to TRY and TRY_BLOCK, TRACE and TRACE_BLOCK manages the call stack automatically. However, they doesn’t check for errors.

Let’s try wrapping the for loop using TRACE_BLOCK.

/** * \file: example_tut03.c */ #include <stdio.h> #include <stdlib.h> #include "c_traceback.h" void alloc_memory(double **arr, const size_t n); void initialize_vec(double *arr, const size_t n); int main(void) { ctb_install_signal_handler(); const size_t n = 1000; // Vector size const double denominator = 123.0; /* Allocate memory */ double *arr; TRY_GOTO(alloc_memory(&arr, n), error_clean_up); /* Initialize values */ TRY_GOTO(initialize_vec(arr, 1000 * n), error_clean_up); /* Division */ /* Clean up */ free(arr); return 0; error_clean_up: /* Error clean up */ free(arr); ctb_dump_traceback(); return 1; } void alloc_memory(double **arr, const size_t n) { *arr = malloc(n * sizeof(double)); if (!(*arr)) { // Failed to allocate memory THROW(CTB_MEMORY_ERROR, "Failed to allocate memory for vector!"); } } void initialize_vec(double *arr, const size_t n) { if (!arr) { // Null Pointer THROW(CTB_NULL_POINTER_ERROR, "Received null pointer."); } TRACE_BLOCK( for (int i = 0; i < n; i++) { arr[i] = i; } ); }

Output

You should see the traceback below.

-------------------------------------------------------------------------------- Traceback (most recent call last): (#00) File "/home/alvinng/Desktop/c_traceback/examples/example_tut03.c", line 25 in main: initialize_vec(arr, 1000 * n) (#01) File "/home/alvinng/Desktop/c_traceback/examples/example_tut03.c", line 58 in initialize_vec: for (int i = 0; i < n; i++) { arr[i] = i; } SignalSegmentationFault -------------------------------------------------------------------------------- Segmentation fault (core dumped)

Division

This one is simple. We simply pass the array, n and denominator to the function.

LOG_WARNING_FMT Function

However, we need to be careful for division by zero. While this could be an issue, sometimes a simple warning is more suitable than throwing an error and terminating the program.

We can use the LOG_WARNING_FMT API to log a warning message without stacktrace.

void divide_vec(double *arr, const size_t n, const double denominator) { if (denominator == 0.0) { // Division by zero LOG_WARNING_FMT(CTB_MATH_ERROR, "Denominator should not be zero! Received: %lf.", denominator); } for (int i = 0; i < n; i++) { arr[i] /= denominator; } }

Same as before, let’s pass 0.0 to denominator for testing.

/** * \file: example_tut04.c */ #include <stdio.h> #include <stdlib.h> #include "c_traceback.h" void alloc_memory(double **arr, const size_t n); void initialize_vec(double *arr, const size_t n); void divide_vec(double *arr, const size_t n, const double denominator); int main(void) { ctb_install_signal_handler(); const size_t n = 1000; // Vector size const double denominator = 0.0; /* Allocate memory */ double *arr; TRY_GOTO(alloc_memory(&arr, n), error_clean_up); /* Initialize values */ TRY_GOTO(initialize_vec(arr, n), error_clean_up); /* Division */ TRY_GOTO(divide_vec(arr, n, denominator), error_clean_up); /* Clean up */ free(arr); return 0; error_clean_up: /* Error clean up */ free(arr); ctb_dump_traceback(); return 1; } void alloc_memory(double **arr, const size_t n) { *arr = malloc(n * sizeof(double)); if (!(*arr)) { // Failed to allocate memory THROW(CTB_MEMORY_ERROR, "Failed to allocate memory for vector!"); } } void initialize_vec(double *arr, const size_t n) { if (!arr) { // Null Pointer THROW(CTB_NULL_POINTER_ERROR, "Received null pointer."); } TRACE_BLOCK( for (int i = 0; i < n; i++) { arr[i] = i; } ); } void divide_vec(double *arr, const size_t n, const double denominator) { if (denominator == 0.0) { // Division by zero LOG_WARNING_FMT(CTB_WARNING, "Denominator should not be zero! Received: %lf.", denominator); } for (int i = 0; i < n; i++) { arr[i] /= denominator; } }

Output

You should see the following output:

Warning: File "/home/alvinng/Desktop/c_traceback/examples/example_tut04.c", line 73 in divide_vec: Denominator should not be zero! Received: 0.000000.

Closing Note

To use our framework, make sure to

  • Clear context and install signal handler at program entry
  • Wrap function calls with either TRACE or TRY
  • Remember to check for errors
Last updated on