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:
- Allocate memory for vector.
- Initialize values for vector.
- Divide every vector element by denominator.
- 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.
-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)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
TRACEorTRY - Remember to check for errors