Skip to content

Latest commit

 

History

History
2511 lines (1863 loc) · 83 KB

README.org

File metadata and controls

2511 lines (1863 loc) · 83 KB

c-utils

This is my personal C utilities which contain the following modules:

1. Heap-allocated String

Wrap and hide all null-terminated C-style string in struct, hide the null-terminated detail and pointer, just deal with normal function call.

The defer_string macro ensures the heap-allocated instance auto-free when it goes out of its scope.

Examples:

1.1 Create empty string

// `defer_string(variable_name);`
defer_string(empty_str) = HS_from_empty();
defer_string(empty_str_2) = HS_from_str(NULL);

// (D) [ String ] > auto_free_string - out of scope with string ptr: 0x6020000004d0, as_str: (null)
// (D) [ String ] > auto_free_string - out of scope with string ptr: 0x6020000003b0, as_str: (null)

1.2 Create from char * or char []

char arr[] = "Unit Test:)";
defer_string(str) = HS_from_arr(arr);

// (D) [ String ] > auto_free_string - out of scope with string ptr: 0x6020000002f0, as_str: Unit Test:)

1.3 Clone from other String

Clone from the given String instance but don’t touch the heap-allocated memory it owned

defer_string(original_str) = HS_from_str("I'm original:)");
defer_string(clone_from_other_str) = HS_clone_from(original_str);

// `clone_from_other_str` is a deep clone, so `original_str` doesn't changes
assert(HS_length(original_str) == 12);

// (D) [ String ] > auto_free_string - out of scope with string ptr: 0x6020000004d2, as_str: I'm original:)
// (D) [ String ] > auto_free_string - out of scope with string ptr: 0x6020000003b1, as_str: I'm original:)

1.4 Move from other String

Move from the given String instance and move ownership of the heap-allocated memory to the newly created String instance. The original String becomes an empty string, as it points to nothing!!!

defer_string(original_str) = HS_from_str("I'm original:)");
defer_string(move_from_other_str) = HS_move_from(original_str);

// After that, `original_str` becomes an empty string
assert(HS_length(original_str) == 0);
assert(HS_as_str(original_str) == NULL);

// (D) [ String ] > auto_free_string - out of scope with string ptr: 0x6020000004d8, as_str: (null)
// (D) [ String ] > auto_free_string - out of scope with string ptr: 0x6020000003b9, as_str: I'm original:)

1.5 Push to the end, get back length and get back char *

defer_string(original_str) = HS_from_str("I'm original:)");
defer_string(empty_str) = HS_from_empty();

// Push from `char *`
HS_push_str(empty_str, "123_");

// Push from other `String`
HS_push_other(empty_str, original_str);

// Get back length
assert(HS_length(empty_str) == strlen("123_I'm original:)"));

// Get back `char *`
assert(strcmp(HS_as_str(empty_str), "123_I'm original:)") == 0);

// (D) [ String ] > auto_free_string - out of scope with string ptr: 0x602000000110, as_str: 123_I'm original:)
// (D) [ String ] > auto_free_string - out of scope with string ptr: 0x6020000000d0, as_str: I'm original:)⏎

1.6 Insert at beginning

defer_string(original_str) = HS_from_str("I'm original:)");
defer_string(empty_str) = HS_from_empty();

// Insert other `String` to the beginning
HS_push_other(empty_str, original_str);

// Insert `char *` to the beginning
HS_insert_str_to_begin(empty_str, "123_");

// Get back length
assert(HS_length(empty_str) == strlen("123_I'm original:)"));

// Get back `char *`
assert(strcmp(HS_as_str(empty_str), "123_I'm original:)") == 0);

// (D) [ String ] > auto_free_string - out of scope with string ptr: 0x602000000110, as_str: 123_I'm original:)
// (D) [ String ] > auto_free_string - out of scope with string ptr: 0x6020000000d0, as_str: I'm original:)⏎

1.7 Find substring

defer_string(original_str) = HS_from_str("I'm original:)");
defer_string(empty_str) = HS_move_from(original_str);

//
// Find the given `char *` index, return `-1` if not found
//
assert(HS_index_of(empty_str, "I'm") == 0);
assert(HS_index_of(empty_str, "nal") == 9);
assert(HS_index_of(empty_str, "RIG") == 5);
assert(HS_index_of(empty_str, "ABC") == -1);

//
// Find the given `char *`(case-sensitive) index, return `-1` if not found
//
assert(HS_index_of_case_sensitive(empty_str, "RIG") == -1);

//
// Check whether contain the given `char *` or not
//
assert(HS_contains(empty_str, "rig") == true);
assert(HS_contains(empty_str, "RIG") == true);
assert(HS_contains(empty_str, "ABC") == false);

1.8 Reset to empty

defer_string(str) = HS_from_str("Hello");
HS_reset_to_empty(str);

assert(HS_length(str) == 0);
assert(HS_as_str(str) == NULL);

// (D) [ String ] > auto_free_string - out of scope with string ptr: 0x6020000000d0, as_str: (null)⏎

1.9 Create String on the stack and free it manually

Usually, it’s convenient to use defer_string to create a String instance, it’s an opaque pointer to struct HeapString. The variable created via defer_string will be freed automatically when the variable goes out of scope.

Here is what defer_string(abc) = HS_from_empty(); does under the hood:

  1. Create struct HeapString instance on the heap, attach the cleanup attribute to that variable, then the auto_free_string function gets call when it out of scope.
    __attribute__((cleanup(auto_free_string))) String abc = HS_from_empty();
        
  1. But the disadvantage is that it calls malloc twice:
    • One for creating String (struct str *) itself
    • One for the internal _buffer member to hold the auctal char * on the heap

So you might choose to create struct HeapString on the stack when you need to create a lot of instances and know their lifetime won’t go out of the current scope, it avoids a lot of unnecessary malloc calls.

Here is the example:

You need to create 1000 struct HeapString to handle a complicated logic in a loop. By creating struct HeapString on the stack, you will save 1000 calls on malloc and free!!!

for (usize index = 0; index < 1000; index++) {
    // Create on stack and init
    struct HeapString temp_str;
    HS_init(&temp_str);

    // Modify it
    char temp_buffer[12];
    snprintf(temp_buffer, sizeof(temp_buffer), "index %lu", index);
    HS_push_str(&temp_str, temp_buffer);

    //
    // ...Another complex logic here
    // ...Another complex logic here
    // ...Another complex logic here
    //
    printf("\n>>> Index in complicated logic: %s", HS_as_str(&temp_str));

    //
    // Make sure free it manually!!!
    // You should call `HS_free_buffer_only` instead of `HS_free`, as
    // you don't need to free `temp_str`, it's stack-allocated instance,
    // it's NOT a pointer!!!
    //
    HS_free_buffer_only(&temp_str);
}

2. Log

Handy logging implementation.

2.1 LOG_VAR macro

It’s only available when ENABLE_DEBUG_LOG macro is defined!!!

Use to print the single variable’s value, only for debugging purposes.

Interface

LOG_VAR(VAR_NAME)

Example

char *string_value = "Nice";
char char_value = 'c';
u8 u8_value = 255;

LOG_VAR(string_value);
LOG_VAR(char_value);
LOG_VAR(u8_value);

LOG_VAR(sizeof(int));
LOG_VAR(sizeof(long));

// >>> string_value: Nice
// >>> char_value: c
// >>> u8_value: 255
// >>> sizeof(int): 4
// >>> sizeof(long): 8

2.2 printf liked formatted logger macro

Interface

/**
* Debug log
*/
DEBUG_LOG(MODULE_NAME, FUNCTION_NAME, format_str, ...)

/**
* Info log
*/
INFO_LOG(MODULE_NAME, FUNCTION_NAME, format_str, ...)

/**
* Warn log
*/
WARN_LOG(MODULE_NAME, FUNCTION_NAME, format_str, ...)

/**
* Error log
*/
ERROR_LOG(MODULE_NAME, FUNCTION_NAME, format_str, ...)

Example

#include "utils/log.h"
#include "utils/string.h"

String my_str = HS_from_str("This macro is so cool:)");
DEBUG_LOG(Main, main, "add(2, 3): %d", add(2, 3));
DEBUG_LOG(Main, main, "2 + 2 :%d", 2 + 2);
DEBUG_LOG(Main, main, "my_str value is: %s", HS_as_str(my_str));
INFO_LOG(Main, main, "my_str value is: %s", HS_as_str(my_str));
WARN_LOG(Main, main, "my_str value is: %s", HS_as_str(my_str));
ERROR_LOG(Main, main, "my_str value is: %s", HS_as_str(my_str));

// (D) [ Main ] > main - add(2, 3): 5
// (D) [ Main ] > main - 2 + 2 :4
// (D) [ Main ] > main - my_str value is: This macro is so cool:)
// (I) [ Main ] > main - my_str value is: This macro is so cool:)
// (W) [ Main ] > main - my_str value is: This macro is so cool:)
// (E) [ Main ] > main - my_str value is: This macro is so cool:)

3. HexBuffer

Handle convertion between char * and u8[]

3.1 char * to HexBuffer

Interface

/*
 * Create `HexBuffer` from the given `char *`. Only accept `0~9` `a~f` `A~F`
 * characters, all another characters will be ignored.
 *
 * Return `NULL` if:
 *
 * - `hex_str` is NULL or empty string
 * - `hex_str` (after ignored all invalid characters) has an odd length
 */
HexBuffer Hex_from_string(const char *hex_str);

/*
 * Return the u8 array iterator
 */
const HexBufferIteractor Hex_iter(const HexBuffer self);

Example

char hex_str_1[] = "AABBCCDD";

HexBuffer buffer_1 = Hex_from_string(hex_str_1);

HexBufferIteractor hex_iter = Hex_iter(buffer_1);
for (usize index = 0; index < hex_iter.length; index++) {
    printf("\n>>> hex_iter[%lu]: 0x%02X", index, hex_iter.arr[index]);
}

// (D) [ HexBuffer ] > Hex_from_string - valid_hex_str len: 8, value: AABBCCDD
// (D) [ HexBuffer ] > Hex_from_string - temp_hex_str: AA, strlen: 2
// (D) [ HexBuffer ] > Hex_from_string - buffer->_buffer[0]: AA
// (D) [ HexBuffer ] > Hex_from_string - temp_hex_str: BB, strlen: 2
// (D) [ HexBuffer ] > Hex_from_string - buffer->_buffer[1]: BB
// (D) [ HexBuffer ] > Hex_from_string - temp_hex_str: CC, strlen: 2
// (D) [ HexBuffer ] > Hex_from_string - buffer->_buffer[2]: CC
// (D) [ HexBuffer ] > Hex_from_string - temp_hex_str: DD, strlen: 2
// (D) [ HexBuffer ] > Hex_from_string - buffer->_buffer[3]: DD
// >>> hex_iter[0]: 0xAA
// >>> hex_iter[1]: 0xBB
// >>> hex_iter[2]: 0xCC
// >>> hex_iter[3]: 0xDD

3.2 HexBuffer to char *

Interface

/*
 * Return the hex buffer length
 */
usize Hex_length(HexBuffer self);

/*
 * Return `out_buffer` size (same with strlen()) if `HexBuffer` is an valid
 * `HexBuffer`.
 *
 * Return 0 when something wrong
 * Return -1 when `out_buffer_size` is not big enough to hold the hex string.
 */
int Hex_to_string(const HexBuffer self, char *out_buffer,
                  usize out_buffer_size);

Example

// `+1` is for the `null-terminated` character
usize out_buffer_size = Hex_length(buffer_1) * 2 + 1;

// Create return `char *` buffer and init to all `0`
char hex_string[out_buffer_size];
memset(hex_string, 0, out_buffer_size);
PRINT_MEMORY_BLOCK_FOR_SMART_TYPE(char [], hex_string, out_buffer_size);

/*
 * Return `out_buffer` size (same with strlen()) if `HexBuffer` is an valid
 * `HexBuffer`.
 *
 * Return 0 when something wrong
 * Return -1 when `out_buffer_size` is not big enough to hold the hex string.
 */
usize return_hex_len = Hex_to_string(buffer_1, hex_string, out_buffer_size);
DEBUG_LOG(Main, test_hex_buffer, "return_hex_len: %lu", return_hex_len);
if (return_hex_len > 0) {
    DEBUG_LOG(Main, test_hex_buffer, "hex_string len: %lu, value: %s",
                strlen(hex_string), hex_string);
}
PRINT_MEMORY_BLOCK_FOR_SMART_TYPE(char [], hex_string, out_buffer_size);

// (D) [ Memory ] > print_memory_block - [ char [] hex_string, size: 9 ]
// (D) [ Memory ] > print_memory_block - ------------------
// (D) [ Memory ] > print_memory_block - 000000000000000000
// (D) [ Memory ] > print_memory_block - ------------------
// 
// (D) [ HexBuffer ] > Hex_to_string - copied_buffer_size: 8, out_buffer_size: 9
// (D) [ HexBuffer ] > Hex_to_string - self->_len: 4, copied_buffer_size: 8, self->_buffer: 0xAABBCCDD
// (D) [ HexBuffer ] > Hex_to_string - copied_size: 2, hex_value: AA
// (D) [ HexBuffer ] > Hex_to_string - copied_size: 2, hex_value: BB
// (D) [ HexBuffer ] > Hex_to_string - copied_size: 2, hex_value: CC
// (D) [ HexBuffer ] > Hex_to_string - copied_size: 2, hex_value: DD
// (D) [ Main ] > test_hex_buffer - return_hex_len: 8
// (D) [ Main ] > test_hex_buffer - hex_string len: 8, value: AABBCCDD
// (D) [ Memory ] > print_memory_block - [ char [] hex_string, size: 9 ]
// (D) [ Memory ] > print_memory_block - ------------------
// (D) [ Memory ] > print_memory_block - 414142424343444400
// (D) [ Memory ] > print_memory_block - ------------------

4. Memory

Handy memory utils.

4.1 PRINT_MEMORY_BLOCK macro

It’s only available when ENABLE_DEBUG_LOG macro is defined!!!

It’s used to print the memory block data in HEX format from a given variable.

Interface

PRINT_MEMORY_BLOCK(TYPE_NAME, VAR_NAME)

Example:

struct Person {
    char birthday[9];
    u8 age;
};

struct Person me = {
    .birthday = "19880531",
    .age = 0xAA,
};
PRINT_MEMORY_BLOCK(struct Person, me)

int data = 10;
PRINT_MEMORY_BLOCK(int, data);

// (D) [ Memory ] > print_memory_block - [ struct Person me, size: 10 ]
// (D) [ Memory ] > print_memory_block - --------------------
// (D) [ Memory ] > print_memory_block - 313938383035333100AA
// (D) [ Memory ] > print_memory_block - --------------------
//
// (D) [ Memory ] > print_memory_block - [ int data, size: 4 ]
// (D) [ Memory ] > print_memory_block - --------
// (D) [ Memory ] > print_memory_block - 0A000000
// (D) [ Memory ] > print_memory_block - --------

4.2 PRINT_MEMORY_BLOCK_FOR_SMART_TYPE macro

It’s only available when ENABLE_DEBUG_LOG macro is defined!!!

It works like the same with the PRINT_MEMORY_BLOCK macro but focuses on the SMART_XXXX variable case, as those variables are opaque pointer types without the original struct type available.

Interface

PRINT_MEMORY_BLOCK_FOR_SMART_TYPE(TYPE_NAME, VAR_NAME, TYPE_SIZE)

Example:

defer_string(str1) = HS_from_str("String in vector");
PRINT_MEMORY_BLOCK_FOR_SMART_TYPE(struct HeapString, str1, HS_struct_size());

// (D) [ String ] > from_str - self ptr: 0x82346a000, malloc ptr: 0x82346b000, from_str: String in vector
// (D) [ Memory ] > print_memory_block - [ struct HeapString str1, size: 16 ]
// (D) [ Memory ] > print_memory_block - --------------------------------
// (D) [ Memory ] > print_memory_block - 100000000000000000B0462308000000
// (D) [ Memory ] > print_memory_block - --------------------------------
```

As you can see above, proven by the `lldb` memory block printing in
`Big Endian` order:

```bash
(lldb) v str1
# (String) str1 = 0x000000082346a000

(lldb) memory read -s `sizeof(struct HeapString)` -c1 -fX `str1`
# 0x82346a000: 0x000000082346B0000000000000000010

Bit Field Declaration

For example if you have the following struct:

 typedef struct {
       bool gpio;
       bool adc;
       bool i2c;
       bool spi;
} ResetReg;

The original purpose of ResetReg is to define a structure to hold 4 boolean values (either 0 or 1) in a group to represent the particular MCU register status. Nothing is wrong but only it consumes 4 Bytes to store just 4 Bits information:

ResetReg reg1 = (ResetReg) {
        .gpio = false,
        .adc = false,
        .i2c = true,
        .spi = true,
};
PRINT_MEMORY_BLOCK(ResetReg, reg1);

The output should be:

>>> ResetReg size: 4

(D) [ Memory ] > print_memory_block - [ ResetReg reg1, size: 4 ]
(D) [ Memory ] > print_memory_block - --------
(D) [ Memory ] > print_memory_block - 00000101
(D) [ Memory ] > print_memory_block - --------

So, how to optimize it?

The answer is use Bit Field: give the width of how many bits should be used for the given type. The width (bits) must less or equal than the data type!!!

For example

typedef struct {
    u8 a: 1; // Only use 1 bit in a uint8_t, that's fine
    u8 b: 2; // Only use 2 bit2 in a uint8_t, that's fine
    u8 c: 4; // Only use 4 bit2 in a uint8_t, that's fine
    u8 d: 9; // Try to use 9 bits in a uint8_t, that's NOT OK!!!
} TestA;

If you try to use a width bigger than the give data type, you got error (e.g. error from the u8 d: 9 line):

error: width of bit-field 'd' (9 bits) exceeds the width of its type (8 bits)

Here is the optimized version of ResetReg:

// typedef struct {
//       u8 gpio: 1;
//       u8 adc: 1;
//       u8 i2c: 1;
//       u8 spi: 1;
// } ResetReg2;

// `bool` and `u8` both are just one byte
typedef struct {
      bool gpio: 1; // Only use 1 bit
      bool adc: 1;
      bool i2c: 1;
      bool spi: 1;
} ResetReg2;

bool is_gpio_enabled(const ResetReg2 *self) {
      return self->gpio;
}
bool is_adc_enabled(const ResetReg2 *self) {
      return self->adc;
}
bool is_i2c_enabled(const ResetReg2 *self) {
      return self->i2c;
}
bool is_spi_enabled(const ResetReg2 *self) {
      return self->spi;
}

ResetReg2 reg2 = (ResetReg2) {
        .gpio = true,
        .adc = true,
        .i2c = false,
        .spi = false,
};

PRINT_MEMORY_BLOCK(ResetReg2, reg2);

printf("\n>>> reg2->gpio enabled: %s", is_gpio_enabled(&reg2) ? "Yes" : "No");
printf("\n>>> reg2->adc enabled: %s", is_adc_enabled(&reg2) ? "Yes" : "No");
printf("\n>>> reg2->i2c enabled: %s", is_i2c_enabled(&reg2) ? "Yes" : "No");
printf("\n>>> reg2->spi enabled: %s", is_spi_enabled(&reg2) ? "Yes" : "No");

Now, total type size of ResetReg2 should just 1 byte (use 4 bits):

 >>> ResetReg2 size: 1

(D) [ Memory ] > print_memory_block - [ ResetReg2 reg2, size: 1 ]
(D) [ Memory ] > print_memory_block - --
(D) [ Memory ] > print_memory_block - 03
(D) [ Memory ] > print_memory_block - --

>>> reg2->gpio enabled: Yes
>>> reg2->adc enabled: Yes
>>> reg2->i2c enabled: No
>>> reg2->spi enabled: No⏎

5. Timer

High resolution timer utils

Interface

/*
 * Time unit
 */
typedef enum TimeUnit {
    TU_NANOSECONDS = 0x01,
    TU_MICROSECONDS = 0x02,
    TU_MILLISECONDS = 0x03,
    TU_SECONDS = 0x04,
} TimeUnit;

/*
 * Get back current time in the given time unit
 */
long double Timer_get_current_time(TimeUnit time_unit);

Example

long double start_time = Timer_get_current_time(TU_NANOSECONDS);
long double end_time = Timer_get_current_time(TU_NANOSECONDS);
long double elapsed_time = end_time - start_time;

DEBUG_LOG(Main, test_timer, "elapsed_time: %Lf\n", elapsed_time);
time ./build_memory_leak_checking/c-utils

# (D) [ Timer ] > Timer_get_current_time - FreeBSD Initialization
# (D) [ Main ] > test_timer - elapsed_time: 238.000000
# 
# ________________________________________________________
# Executed in    3.35 millis    fish           external
#    usr time    0.98 millis  981.00 micros    0.00 millis
#    sys time    5.93 millis    0.00 micros    5.93 millis

6. Smart pointer

MAKE_UNIQUE_PTR simulates the std::make_unique in C++:

Interface

MAKE_UNIQUE_PTR(VAR_DEFINE, DESTRUCTOR)

Example

String return_string_on_the_heap() {
    String str_on_the_heap = HS_from_str("String allocated on the heap:)");
    return str_on_the_heap;
}

Vector return_vector_on_the_heap() {
    usize double_size = sizeof(double);
    Vector temp_vec = Vector_with_capacity(5, double_size);
    double d = 888.88;
    Vector_push(temp_vec, &d, double_size);
    return temp_vec;
}

void test_smart_ptr() {
    //
    // `return_str` will be destroyed by calling `auto_free_string` automatic
    //
    MAKE_UNIQUE_PTR(String return_str = return_string_on_the_heap(), auto_free_string);

    //
    // `return_vector` will be destroyed by calling `auto_free_vector` automatic
    //
    MAKE_UNIQUE_PTR(Vector return_vec = return_vector_on_the_heap(), auto_free_vector);

    DEBUG_LOG(Main, test_smart_ptr, "return_str: %p, value: %s", return_str,
              HS_as_str(return_str));
    DEBUG_LOG(Main, test_smart_ptr,
              "return_vec: %p, len: %lu, first elemnt: %f", return_vec,
              Vector_len(return_vec),
              *((double *)Vector_get(return_vec, 0, sizeof(double))));
}

// (D) [ String ] > from_str - self ptr: 0x5472040, malloc ptr: 0x5472090, from_str: String allocated on the heap:)
// (D) [ Vector ] > with_capacity - self pointer: 0x5474130, capacity: 5
// (D) [ Main ] > test_smart_ptr - return_str: 0x5472040, value: String allocated on the heap:)
// (D) [ Main ] > test_smart_ptr - return_vec: 0x5474130, len: 1, first elemnt: 888.880000
// (D) [ Vector ] > auto_free_vector - out of scope with vector ptr: 0x5474130, length: 1
// (D) [ String ] > auto_free_string - out of scope with string ptr: 0x5472040, as_str: String allocated on the heap:)==42550==

7. Bits

Handy macros to handle bits, only available when ENABLE_DEBUG_LOG macro is defined!!!

7.1 PRINT_BITS macro

Interface

PRINT_BITS(VAR_NAME)

Example

unsigned char status = 0x3D;
PRINT_BITS(status);

unsigned short int status_16 = 0x376D;
PRINT_BITS(status_16);

int status_32 = 0x376DAA0B;
PRINT_BITS(status_32);

long long status_64 = 0x376DAA0B5F8E9ABC;
PRINT_BITS(status_64);

// (D) [ Bits ] > PRINT_BITS "u08" - >>> 0x3D bits: 00111101
// (D) [ Bits ] > PRINT_BITS "u16" - >>> 0x376D bits: 0011011101101101
// (D) [ Bits ] > PRINT_BITS "u32" - >>> 0x376DAA0B bits: 00110111011011011010101000001011
// (D) [ Bits ] > PRINT_BITS "u64" - >>> 0x376DAA0B5F8E9ABC bits: 0011011101101101101010100000101101011111100011101001101010111100

7.2 IS_BIT_1 macro

Check whether the given bit is 1 or not

Interface

BIT_IS_1(VAR_NAME, WHICH_BIT)

Example

v = 0xCD;
PRINT_BITS(v);
which_bit = 1;
printf("\n>>> bit %d in '0x%02X' is 1?: %s", which_bit, v, v >> (which_bit - 1) & 0x01 ? "Yes" : "No");
which_bit = 2;
printf("\n>>> bit %d in '0x%02X' is 1?: %s", which_bit, v, v >> (which_bit - 1) & 0x01 ? "Yes" : "No");
which_bit = 3;
printf("\n>>> bit %d in '0x%02X' is 1?: %s", which_bit, v, v >> (which_bit - 1) & 0x01 ? "Yes" : "No");
which_bit = 4;
printf("\n>>> bit %d in '0x%02X' is 1?: %s", which_bit, v, v >> (which_bit - 1) & 0x01 ? "Yes" : "No");
which_bit = 5;
printf("\n>>> bit %d in '0x%02X' is 1?: %s", which_bit, v, v >> (which_bit - 1) & 0x01 ? "Yes" : "No");
which_bit = 6;
printf("\n>>> bit %d in '0x%02X' is 1?: %s", which_bit, v, v >> (which_bit - 1) & 0x01 ? "Yes" : "No");
which_bit = 7;
printf("\n>>> bit %d in '0x%02X' is 1?: %s", which_bit, v, v >> (which_bit - 1) & 0x01 ? "Yes" : "No");
which_bit = 8;
printf("\n>>> bit %d in '0x%02X' is 1?: %s", which_bit, v, v >> (which_bit - 1) & 0x01 ? "Yes" : "No");

// (D) [ Bits ] > PRINT_BITS "u08" - >>> 0xCD bits: 11001101
// >>> bit 1 in '0xCD' is 1?: Yes
// >>> bit 2 in '0xCD' is 1?: No
// >>> bit 3 in '0xCD' is 1?: Yes
// >>> bit 4 in '0xCD' is 1?: Yes
// >>> bit 5 in '0xCD' is 1?: No
// >>> bit 6 in '0xCD' is 1?: No
// >>> bit 7 in '0xCD' is 1?: Yes
// >>> bit 8 in '0xCD' is 1?: Yes

8. File

Wrap the C-style file APIs.

The defer_file macro ensures the heap-allocated instance auto-free when it goes out of its scope.

Examples:

8.1 Open existing file and read all data into internal buffer

// `defer_file(variable_name);`
char *filename = "/home/wison/temp/test.log";
defer_file(my_file) = File_open(filename, FM_READ_ONLY);
if (File_is_open_successfully(my_file)) {
    usize read_bytes = File_load_into_buffer(my_file);
    const char *file_content = File_get_data(my_file);
    usize file_size = File_get_size(my_file);
    LOG_VAR(read_bytes);
    LOG_VAR(file_size);
    LOG_VAR(file_content);
}

#ifdef ENABLE_DEBUG_LOG
File_print_debug_info(my_file);
#endif


// (D) [ File ] > open - self ptr: 0x5472040, filename: /home/wison/temp/test.log, open mode: r
// (D) [ File ] > load_into_buffer - file_size: 6
// (D) [ File ] > load_into_buffer - after read from file, self->data, len: 6, value: 12345
//
// >>> read_bytes: 6
// >>> file_size: 6
// >>> file_content: 12345
//
// (D) [ File ] > print_debug_info -
// [ File, ptr: 0x5472040 ]
// ----------------------------------------
// inner: 0x4a4e2b0
// mode: r
// filename: /home/wison/temp/test.log
// error: (null)
// size: 6
// data: 12345
//
// ----------------------------------------
// (D) [ File ] > auto_free_file - out of scope with File ptr: 0x5472040, filename: /home/wison/temp/test.log
// (D) [ File ] > free - Close file - '/home/wison/temp/test.log', result: 0

8.2 Open non-existing file

// `defer_file(variable_name);`
char *filename = "/home/wison/temp/non-exists.log";
defer_file(my_file) = File_open(filename, FM_READ_ONLY);

#ifdef ENABLE_DEBUG_LOG
File_print_debug_info(my_file);
#endif


// (D) [ File ] > open - self ptr: 0x5472040, filename: /home/wison/temp/non-exists.log, open mode: r
// (D) [ File ] > open - Open file failed - '/home/wison/temp/non-exists.log': No such file or directory
// (D) [ File ] > print_debug_info -
// [ File, ptr: 0x5472040 ]
// ----------------------------------------
// inner: 0x0
// mode: r
// filename: /home/wison/temp/non-exists.log
// error: No such file or directory
// size: 0
// data: (null)
// ----------------------------------------
// (D) [ File ] > auto_free_file - out of scope with File ptr: 0x5472040, filename: /home/wison/temp/non-exists.log

Project Setup

CMake configurations

This project has 2 cmake setups for different purposes:

  • cmake/CMakelists.txt

    Use C compiler to compile main.c and support to use memory leaking tools to check memory leaking issue on the pure C binary.

  • cmake/unit_test/CMakelists.txt

    Use Unity to do unit test.

For unit test

This project uses Unity as test framework, you need to run the following commands to install Unity:

cd ~/temp
git clone --depth=1 https://github.com/ThrowTheSwitch/Unity.git
cd Unity
mkdir build && cd build

#
# `-DCMAKE_C_FLAGS="-DUNITY_INCLUDE_DOUBLE"`: Compile Unity to support double!!!
#
# `-DCMAKE_INSTALL_INCLUDEDIR` and `-DCMAKE_INSTALL_LIBDIR`: install path
#
cmake .. \
    -DCMAKE_C_FLAGS="-DUNITY_INCLUDE_DOUBLE" \
    -DCMAKE_INSTALL_INCLUDEDIR=~/my-installed/include \
    -DCMAKE_INSTALL_LIBDIR=~/my-installed/lib

#
# Compile and insteall
#
make install
# [ 50%] Building C object CMakeFiles/unity.dir/src/unity.c.o
# [100%] Linking C static library libunity.a
# [100%] Built target unity
# Install the project...
# -- Install configuration: ""
# -- Installing: /home/wison/my-installed/lib/libunity.a
# -- Installing: /home/wison/my-installed/include/unity/unity.h
# -- Installing: /home/wison/my-installed/include/unity/unity_internals.h
# -- Installing: /home/wison/my-installed/lib/cmake/unity/unityTargets.cmake
# -- Installing: /home/wison/my-installed/lib/cmake/unity/unityTargets-noconfig.cmake
# -- Installing: /home/wison/my-installed/lib/cmake/unity/unityConfig.cmake
# -- Installing: /home/wison/my-installed/lib/cmake/unity/unityConfigVersion.cmake

CMake setup and run

Default configuration

You should only use this configuration when you try to installing c_utils!!!

You should use one of the rest configured ways to check memory leaking during development!!! You should use one of the rest configured ways to check memory leaking during development!!! You should use one of the rest configured ways to check memory leaking during development!!!

./configure.sh

Use Google AddressSanitizer for checking memory leaking

AddressSanitizer (aka ASan) is a memory error detector for C/C++. It finds:

  • Use after free (dangling pointer dereference)
  • Heap buffer overflow
  • Stack buffer overflow
  • Global buffer overflow
  • Use after return
  • Use after scope
  • Initialization order bugs
  • Memory leaks: Doesn’t support FreeBSD!!!
./configure_address_sanitizer.sh

By default, BSD builtin clang/clang++ doesn’t support AddressSanitizer.

If you want to enable AddressSanitizer in BSD (MacOS or FreeBSD), then have to use installed llvm clang/clang++ instead of the builtin clang/clang++!!!

Then, compile src/main.c and run temp_build/build_memory_leak_checking/c-utils-demo.

./run_address_sanitizer.sh

# [100%] Built target c-utils
#
# // ...ignore...
#
# =================================================================
# ==49381==ERROR: LeakSanitizer: detected memory leaks
#
# Direct leak of 16 byte(s) in 1 object(s) allocated from:
#     #0 0x10d795000 in wrap_malloc+0xa0 (libclang_rt.asan_osx_dynamic.dylib:x86_64+0x4a000) (BuildId: eb137767d72432a1a6e32c107b9c74d42400000010000000000a0a0000010c00)
#     #1 0x10d71c549 in HS_from_str string.c:80
#     #2 0x10d71f2bd in test_string main.c:74
#     #3 0x10d722768 in main main.c:556
#     #4 0x7fff204faf3c in start+0x0 (libdyld.dylib:x86_64+0x15f3c) (BuildId: 5fbd0e1aacce36dbb11c622f26c8513232000000200000000100000000060b00)
#
# Indirect leak of 11 byte(s) in 1 object(s) allocated from:
#     #0 0x10d795000 in wrap_malloc+0xa0 (libclang_rt.asan_osx_dynamic.dylib:x86_64+0x4a000) (BuildId: eb137767d72432a1a6e32c107b9c74d42400000010000000000a0a0000010c00)
#     #1 0x10d71c593 in HS_from_str string.c:88
#     #2 0x10d71f2bd in test_string main.c:74
#     #3 0x10d722768 in main main.c:556
#     #4 0x7fff204faf3c in start+0x0 (libdyld.dylib:x86_64+0x15f3c) (BuildId: 5fbd0e1aacce36dbb11c622f26c8513232000000200000000100000000060b00)
#
# SUMMARY: AddressSanitizer: 27 byte(s) leaked in 2 allocation(s).

Use Unity to run unit test

./configure_unit_test.sh

Then, compile src/unit_test.c and run temp_build/unit_test.

./run_unit_test.sh

# [  9%] Building C object CMakeFiles/c-utils-unit-test.dir/home/wison/c/c-utils/src/utils/log.c.o
# [ 18%] Building C object CMakeFiles/c-utils-unit-test.dir/home/wison/c/c-utils/src/utils/memory.c.o
# [ 27%] Building C object CMakeFiles/c-utils-unit-test.dir/home/wison/c/c-utils/src/utils/heap_string.c.o
# [ 36%] Building C object CMakeFiles/c-utils-unit-test.dir/home/wison/c/c-utils/src/utils/hex_buffer.c.o
# [ 45%] Building C object CMakeFiles/c-utils-unit-test.dir/home/wison/c/c-utils/src/utils/file.c.o
# [ 54%] Building C object CMakeFiles/c-utils-unit-test.dir/home/wison/c/c-utils/src/test/utils/hex_buffer_test.c.o
# [ 63%] Building C object CMakeFiles/c-utils-unit-test.dir/home/wison/c/c-utils/src/test/utils/data_types_test.c.o
# [ 72%] Building C object CMakeFiles/c-utils-unit-test.dir/home/wison/c/c-utils/src/test/utils/file_test.c.o
# [ 81%] Building C object CMakeFiles/c-utils-unit-test.dir/home/wison/c/c-utils/src/test/utils/string_test.c.o
# [ 90%] Building C object CMakeFiles/c-utils-unit-test.dir/home/wison/c/c-utils/src/unit_test.c.o
# [100%] Linking C executable c-utils-unit-test
# [100%] Built target c-utils-unit-test
# /home/wison/c/c-utils/src/unit_test.c:25:test_hex_buffer_empty_hex_buffer:PASS
# /home/wison/c/c-utils/src/unit_test.c:26:test_hex_buffer_invalid_buffer:PASS
# /home/wison/c/c-utils/src/unit_test.c:27:test_hex_buffer_valid_buffer:PASS
# /home/wison/c/c-utils/src/unit_test.c:29:test_data_types_type_name:PASS
# /home/wison/c/c-utils/src/unit_test.c:30:test_data_types_is_the_same_type:PASS
# /home/wison/c/c-utils/src/unit_test.c:31:test_data_types_type_name_to_string:PASS
# /home/wison/c/c-utils/src/unit_test.c:32:test_data_types_type_size:PASS
# /home/wison/c/c-utils/src/unit_test.c:33:test_data_types_type_size_from_type:PASS
# /home/wison/c/c-utils/src/unit_test.c:35:test_file_open_should_fail:PASS
# /home/wison/c/c-utils/src/unit_test.c:36:test_file_open_should_success:PASS
# /home/wison/c/c-utils/src/unit_test.c:37:test_file_read_should_success:PASS
# /home/wison/c/c-utils/src/unit_test.c:39:test_string_init:PASS
# /home/wison/c/c-utils/src/unit_test.c:40:test_string_init_with_capacity:PASS
# /home/wison/c/c-utils/src/unit_test.c:41:test_string_empty_string:PASS
# /home/wison/c/c-utils/src/unit_test.c:42:test_string_empty_string_with_capacity:PASS
# /home/wison/c/c-utils/src/unit_test.c:43:test_string_from_array:PASS
# /home/wison/c/c-utils/src/unit_test.c:44:test_string_from_str_with_pos_and_count:PASS
# /home/wison/c/c-utils/src/unit_test.c:45:test_string_clone:PASS
# /home/wison/c/c-utils/src/unit_test.c:46:test_string_find_substring:PASS
# /home/wison/c/c-utils/src/unit_test.c:47:test_string_contain_substring:PASS
# /home/wison/c/c-utils/src/unit_test.c:48:test_string_reset_to_empty:PASS
# /home/wison/c/c-utils/src/unit_test.c:49:test_string_reset_to_empty_without_freeing_buffer:PASS
# /home/wison/c/c-utils/src/unit_test.c:50:test_string_push:PASS
# /home/wison/c/c-utils/src/unit_test.c:51:test_string_insert_at_begin:PASS
# /home/wison/c/c-utils/src/unit_test.c:52:test_string_move_semantic:PASS
# 
# -----------------------
# 25 Tests 0 Failures 0 Ignored
# OK
How to still finish all tests after any of them is fail

By default, GoogleTest run all tests even any test is fail.

In run_unit_test.sh, added a env var GTEST_FAIL_FAST=true which allows skip all the rest tests if any test is fail. Feel free to remove it if you don’t like that.

How to run the given TestSuite only

You can use GTEST_FILTER env var to control which test (or test suite) your want to run only. Doc is here.

Example:

GTEST_FILTER="String.*" ./run_unit_test.sh
GTEST_FILTER="String.MoveSemantic" ./run_unit_test.sh

GTEST_FILTER="DataTypes.*" ./run_unit_test.sh

Install c_utils share library

The default prefix install path to /${HOME}/my-installed, but you can change the INSTALL_PREFIX setting in configure.sh or configure_address_sanitizer.sh.

Install the cutils share library to your system:

./configure.sh

./install-lib.sh
# [ 57%] Built target c-utils-demo
# [ 63%] Building C object CMakeFiles/c_utils.dir/home/wison/c/c-utils/src/utils/file.c.o
# [ 68%] Building C object CMakeFiles/c_utils.dir/home/wison/c/c-utils/src/utils/hex_buffer.c.o
# [ 73%] Building C object CMakeFiles/c_utils.dir/home/wison/c/c-utils/src/utils/log.c.o
# [ 78%] Building C object CMakeFiles/c_utils.dir/home/wison/c/c-utils/src/utils/memory.c.o
# [ 84%] Building C object CMakeFiles/c_utils.dir/home/wison/c/c-utils/src/utils/random.c.o
# [ 89%] Building C object CMakeFiles/c_utils.dir/home/wison/c/c-utils/src/utils/heap_string.c.o
# [ 94%] Building C object CMakeFiles/c_utils.dir/home/wison/c/c-utils/src/utils/timer.c.o
# [100%] Linking C shared library libc_utils.so
# [100%] Built target c_utils
# Install the project...
# -- Install configuration: "Debug"
# -- Installing: /home/wison/my-installed/lib/libc_utils.so
# -- Up-to-date: /home/wison/my-installed/include/c_utils/bits.h
# -- Up-to-date: /home/wison/my-installed/include/c_utils/data_types.h
# -- Installing: /home/wison/my-installed/include/c_utils/file.h
# -- Up-to-date: /home/wison/my-installed/include/c_utils/hex_buffer.h
# -- Up-to-date: /home/wison/my-installed/include/c_utils/log.h
# -- Up-to-date: /home/wison/my-installed/include/c_utils/memory.h
# -- Up-to-date: /home/wison/my-installed/include/c_utils/random.h
# -- Up-to-date: /home/wison/my-installed/include/c_utils/smart_ptr.h
# -- Up-to-date: /home/wison/my-installed/include/c_utils/heap_string.h
# -- Up-to-date: /home/wison/my-installed/include/c_utils/timer.h

How to preview preprocess step source code

It’s beneficial if you can print out the source code content after the preprocessor step (but before throwing it into the compiler)

# `-D`: Use to define macros
# `-E`: Run the preprocessor stage.
clang -E -D ENABLE_DEBUG_LOG src/main.c | bat

How to print all supported macros on current computer and OS

clang -dM -E - < /dev/null

Also, you can use it to confirm whether the given OS macro defines or not:

clang -dM -E - < /dev/null | rg BSD
#define __FreeBSD__ 14
#define __FreeBSD_cc_version 1400001

Support OS:

OPERATING SYSTEMMACRO PRESENTNOTES
Windows 32 bit + 64 bit_WIN32for all Windows OS
Windows 64 bit_WIN64Only for 64 bit Windows
Apple__APPLE__for all Apple OS
Apple__MACH__alternative to above
iOS embeddedTARGET_OS_EMBEDDEDinclude TargetConditionals.h
iOS stimulatorTARGET_IPHONE_SIMULATORinclude TargetConditionals.h
iPhoneTARGET_OS_IPHONEinclude TargetConditionals.h
MacOSTARGET_OS_MACinclude TargetConditionals.h
Android__ANDROID__subset of linux
Unix based OS__unix__
Linux__linux__subset of unix
POSIX based_POSIX_VERSIONWindows with Cygwin
Solaris__sun
HP UX__hpux
BSDBSDall BSD flavors
DragonFly BSD__DragonFly__
FreeBSD__FreeBSD__
NetBSD__NetBSD__
OpenBSD__OpenBSD__

Appendix: From Rust to C

A-1. Primitive Data Types

Here is the C Date Types

CRust
[ Integer ]
chari8
unsigned charu8
shorti16
unsigned shortu16
inti32
unsigned intu32
longi64
unsigned longu64
size_tu64/usize (But u32 in FreeBSD)
[ Floating point ]
floatf32
doublef64
[ Boolean ]
_Boolbool

A-2. printf related

How to print fixed width HEX

C
unsigned char unsigned_char_v = 0x0A;
unsigned short unsigned_short_v = 0x0123;

// `02` means left-padding `0` until output len is 2
// Output: >>> unsigned_char_v: 0x0A
printf("\n>>> unsigned_char_v: 0x%02X", unsigned_char_v);

// `04` means left-padding `0` until output len is 4
// Output: >>> unsigned_short_v: 0x0123
printf("\n>>> unsigned_short_v: 0x%04X", unsigned_short_v);
Rust
let u8_v = 0x0Au8;
let u16_v = 0x0Bu16;
println!("u8_v: 0x{u8_v:#02X?}, size: {}", core::mem::size_of::<u8>());
println!("u16_v: 0x{u16_v:#02X?}, size: {}", core::mem::size_of::<u16>());

How to format string (sequence of chars)

C

snprintf is the safe version of sprintf

const size_t BUFFER_SIZE = 100;
char buffer[BUFFER_SIZE];

char *my_name = "Wison Ye";
int my_age = 888;

int buffer_str_size =
    snprintf(buffer, BUFFER_SIZE, "%s, %i", my_name, my_age);
printf(
    "\nformatted_str: %s, formatted_buffer_size: %i, sizeof: %lu, strlen: "
    "%lu",
    buffer, buffer_str_size, sizeof(buffer), strlen(buffer));

// Output: formatted_str: Wison Ye, 888, formatted_buffer_size: 13, sizeof: 100, strlen: 13⏎
Rust
let u16_v = 0x0Bu16;
let formatted_str = format!("u16_v: 0x{u16_v:#02X?}, size: {}", core::mem::size_of::<u16>());
println!("formatted_str: {formatted_str}");

A-3. string related

string actually just a sequance of characters.

char my_name[] = "wisonye";

sizeof(my_name) is 8, as it includes the final \0 null-terminated character!!!

strlen(my_name) is 7, as it doesn’t count the final \0 null-terminated character!!!

But you **CANNOT** use sizeof on a char * (pointer, NOT char []), otherwise you always get back 4 (4bytes on 32bit) or 8 (8bytes on 64bit):

char *my_name_2 = "wisonye wisonye";
printf("\nsizeof(my_name_2): %lu", sizeof(my_name_2));
printf("\nstrlen(my_name_2): %lu", strlen(my_name_2));

// sizeof(my_name_2): 8
// strlen(my_name_2): 15⏎

Safey verison of `strncat`

//
// Safey verison of `strncat`:
//
// `max_dest_len` should be the `sizeof(char dest[])`
//
char *strncat_safe(char *dest, const char *src, size_t max_dest_len) {
    // `dest` and `src` both are `char *`, that's why you should use
    // `strlen` instead of `sizeof`. If you use `sizeof`, it always
    // return `4` or `8`, as that the size of a pointer (4 bytes on
    // 32bit, 8 bytes in 64bit)!!!
    size_t src_len = strlen(src);
    size_t current_dest_str_len = strlen(dest);

    // printf("\n\ndest_len: %lu, src_len: %lu, max_dest_len: %lu", current_dest_str_len, src_len, max_dest_len);

    // `-1` because you need to count the `\0` null-terminated character
    // to end the string.
    size_t available_dest_len = max_dest_len - 1;

    if (current_dest_str_len == 0 && available_dest_len >= src_len) {
        // printf("\n>>> 1");
        return strncat(dest, src, available_dest_len);
    }
    if (current_dest_str_len > 0 &&
        available_dest_len >= current_dest_str_len + src_len) {
        // printf("\n>>> 2");
        return strncat(dest, src, available_dest_len);
    } else {
        if (available_dest_len - current_dest_str_len > 0) {
            // printf("\n>>> 3");
            return strncat(dest, src,
                           available_dest_len - current_dest_str_len);
        } else {
            // printf("\n>>> 4");
            return dest;
        }
    }
}

A-4. Life time

In C, actually it has the lifetime concept and it works the same way with Rust:

  • Local variable will be destroyed after it’s out of the scope (code block/function body)
  • Return value by copying it, same with passing by value

So, let’s take a look at a few real-world examples:

Return a struct in funciton works:

typedef struct {
    char *first_name;
    char *last_name;
} Name2;

//
// This works: by returning a struct instance.
//
// It does the same thing of passing by value which means a copy of the struct
// instance.
//
// By proving this, you can print out the local var's address and compare to
// the outer caller return struct instance's address, they should be the
// different pointer!!!
//
Name2 create_your_name(char *first_name, char *last_name) {
    Name2 your_name = {first_name, last_name};

    printf("\n>>> (from create_your_name function) - `your_name` stack local var pointer: %p", &your_name);
    // >>> (from create_your_name function) - `your_name` stack local var pointer: 0x7ffeeecff090

    return your_name;
}

// Call it and compare the struct instance address and they're different
char first_name[] = "Wison";
char last_name[] = "Ye";
Name2 the_name_you_created = create_your_name(first_name, last_name);

printf("\n>>> `the_name_you_created` pointer: %p", &the_name_you_created);
// >>> `the_name_you_created` pointer: 0x7ffeeecff0d0

As you can see that the create_your_name return a new struct instance by copying it and it works.

0x7ffeee proves that it’s the stack frame local variable, as stack frame located at the very high address area.

If you doubt that why it works even it has the char * pointer???

That’s because the pointer is passed by outside, so here is the trick:

the_name_you_created.first_name –> char first_name[]

the_name_you_created.last_name –> char last_name[]

And both first_name and last_name still exists and available after the function (create_your_name) stack frame has been destroyed, that’s why it works:)

Return a struct in funciton that doesn’t work:

typedef struct {
    char *first_name;
    char *last_name;
} Name2;


//
// This won't work: by returning a struct instance but there is local stack
// address reference!!!
//
Name2 create_temp_name() {
    char temp_first_name[] = "No first name";
    char temp_last_name[] = "No last name";

    Name2 your_name = {temp_first_name, temp_last_name};
    printf("\n\n>>> (from create_temp_name function) - `your_name` stack local var pointer: %p", &your_name);
    // >>> (from create_temp_name function) - `your_name` stack local var pointer: 0x7ffeeecff068

    // After returning (or say by copying) the `your_name` struct instance,
    // `your_name.first_name` and `your_name.last_name` point to invalid memory
    // address!!!
    return your_name;
}

// Call it and compare the struct instance address and they're different
Name2 the_name_wont_work = create_temp_name();

printf("\n>>> `the_name_wont_work ` pointer: %p", &the_name_wont_work);
// `the_name_wont_work` pointer: 0x7ffeeecff0b0

As you can see that the create_temp_name return a new struct instance by copying it and it SHOULD work.

But in fact, it doesn’t work at all!!!

That’s because:

the_name_wont_work.first_name –> char temp_first_name[]

the_name_wont_work.last_name –> char temp_last_name[]

And both temp_first_name and temp_last_name won’t be exists and unavailable after the function (create_temp_name) stack frame has been destroyed, that’s why it won’t work:)

Yes, it compiles and runs, but ..... the values aren’t the values you think they’re and might crash in sometimes!!!

That’s why passing any stack memory pointer to outside world is super dangerous and it’s very difficult to debug!!!

Also, have a look at the ./c_demo_struct_stack_frame_analysis.txt, as it shows the function call stack details.

A-5.1 What is pointer and how it works

What you should know about a Pointer is:

  • It’s just an unsigned integer, that’s why you can convert an unsigned integer as a pointer.
  • That said the value of a pointer variable is an unsigned interger.
  • The meaning of pointer value (unsigned integer) is the memory address/location
  • The meaning of pointer value (unsigned integer) is the memory address/location, that’s why you need to dereference to get back the data the pointer points to (as pointer just store the data memory address/location, NOT the data itself)!!!
uint16_t data = 0xAA;

//
// `&` means `address of` (get back the address of xxx)
// Suppose that the `data` variable address is `0x100`
//
// `ptr`'s value is `0x100`, just an unsigned integer and it points to
// the memory location where the address is `0x100`
//
uint16_t *ptr = &data;

Here is the memory after the above code runs:

-----> 0x100 (data) | 0xAA | 0x00 |

0x103 (ptr)0x1000x00
points to

So, let’s deference the ptr and set it’s value to 0xBB:

//
// `*` means get back the memory/value from the given address
// `*ptr` => `*0x100` get back the memory value from `0x100`
// `*ptr =` => `*0x100 =` get back the memory from `0x100` and assign new value (overwrite) to that memory chunk
// `*ptr = 0xBB` => `*0x100 = 0xBB` get back the memory from `0x100` and assign `0xBB` to that memory chunk
//
*ptr = 0xBB; // -> Set `0x00BB` (uint16_t is 2 bytes) to the memory chunk whose address is `0x100`

After the code above runs, memory looks like this:

-----> 0x100 (data) | 0xBB | 0x00 |

0x103 (ptr)0x1000x00
points to

A-5.2 Pointer arithmetic

What ptr + x means:

It’s saying: add/move the pointer address to X unit of pointed type from the current memory location (address) that the pointer points to.

So, here is the pseudo formula: ptr + x => ptr + x * sizeof(POINTER_TYPE)

Here is the example:

//
// Force to convert the integer decimal value `100` as the pointer address
//
int *int_ptr = (int *)100;
printf("\n>>> int_ptr addr: %llu", (uint64_t)((void *)int_ptr));

// -> int_ptr += 1 * sizeof(int) // + 4 or say move 4 bytes
int_ptr++; 
printf("\n>>> int_ptr addr: %llu", (uint64_t)((void *)int_ptr));

// -> int_ptr += 1 * sizeof(int) // + 4 or say move 4 bytes
int_ptr += 1;
printf("\n>>> int_ptr addr: %llu", (uint64_t)((void *)int_ptr));

// -> int_ptr += 2 * sizeof(int) // + 8 or say move 8 bytes
int_ptr += 2;
printf("\n>>> int_ptr addr: %llu", (uint64_t)((void *)int_ptr));

And here is the result:

>>> int_ptr addr: 100
>>> int_ptr addr: 104
>>> int_ptr addr: 108
>>> int_ptr addr: 116

A-5.3 The relationship between Pointer and Array

Actually, pointer is just like an array in the other form, the C design assumes that you use a pointer like an array. That’s why the following code works:

u16 temp_arr[] = {1, 2, 3, 4, 5};

//
// `sizeof` is an operator, NOT a function!!!
//
usize arr_len = sizeof(temp_arr) / sizeof(temp_arr[0]);

//
// Use `pointer` to print the loop
//
u16 *loop_ptr = temp_arr;
for (usize index = 0; index < arr_len; index++) {
    printf("\n>>> (pointer_in_arr - loop 2) - %p: %u", loop_ptr + index,
            *(loop_ptr + index));
}

// >>> (pointer_in_arr - loop 2) - 0x820d9d686: 1
// >>> (pointer_in_arr - loop 2) - 0x820d9d688: 2
// >>> (pointer_in_arr - loop 2) - 0x820d9d68a: 3
// >>> (pointer_in_arr - loop 2) - 0x820d9d68c: 4
// >>> (pointer_in_arr - loop 2) - 0x820d9d68e: 5

You can found a fews things from the above code:

  • loop_ptr is a pointer to u16 type
  • loop_ptr + X:

    It’s saying: add/move the pointer address to X unit of pointed type from the current position (start from 0x820d9d686 on above print out sample).

    So, here is the pseudo formula: ptr + x => ptr + x * sizeof(POINTER_TYPE)

    That’s why loop_ptr + 1 -> loop_ptr + 1 * sizeof(u16), and it actually moved 2 bytes (0x820d9d686 + 2), as the pointer points to type u16!!!

For the temp_arr variable, actually the compile treats it as a pointer that points to the first element of the allocated array:

u16 temp_arr[] = {1, 2, 3, 4, 5};

// That said the `temp_arr` variable means `&temp_arr[0]`

and you can use that temp_arr as just a pointer. That’s why the following code works:

// `temp_arr` acts like the `loop_str` in above sample, as in fact, it just
// an pointer:)
for (usize index = 0; index < arr_len; index++) {
    printf("\n>>> (pointer_in_arr - loop 1.1) - %p: %u", temp_arr + index,
            *(temp_arr + index));
}

>>> (pointer_in_arr - loop 1.1) - 0x8206a92a6: 1
>>> (pointer_in_arr - loop 1.1) - 0x8206a92a8: 2
>>> (pointer_in_arr - loop 1.1) - 0x8206a92aa: 3
>>> (pointer_in_arr - loop 1.1) - 0x8206a92ac: 4
>>> (pointer_in_arr - loop 1.1) - 0x8206a92ae: 5

And one more thing to prove that you can swap the pointer and array (var name) is this sample:

u16 *loop_ptr_2 = temp_arr;
for (usize index = 0; index < arr_len; index++) {
    printf("\n>>> (pointer_in_arr - loop 1.1.) - %p: %u", loop_ptr_2 + index,
            loop_ptr_2[index]);
}

// >>> (pointer_in_arr - loop 1.1.) - 0x8205fda16: 1
// >>> (pointer_in_arr - loop 1.1.) - 0x8205fda18: 2
// >>> (pointer_in_arr - loop 1.1.) - 0x8205fda1a: 3
// >>> (pointer_in_arr - loop 1.1.) - 0x8205fda1c: 4
// >>> (pointer_in_arr - loop 1.1.) - 0x8205fda1e: 5

Plz pay attention to that loop_ptr_2[index], all the following codes present the same meaning: get the value that the pointer points to, AKA: dereference

  • temp_arr[index]
  • loop_ptr_2[index]
  • *(loop_ptr_2 + index)
  • *(temp_arr + index)

Again: An array variable is just a pointer, you can swap using them at any given time.

But the slight difference between the array variable and the pointer is that:

The compiler can check and detect the array boundary errors but NOT check on the pointer form

Consider the following code:

temp_arr[10] = 10;
loop_ptr_2[10] = 10;
*(loop_ptr_2 + 10) = 10;

Compiler produces the error on line of temp_arr[10] = 10 but not the rest of lines:

warning: array index 10 is past the end of the array (which contains 5 elements) [-Warray-bounds]
temp_arr[10] = 10;
^        ~~
note: array 'temp_arr' declared here
u16 temp_arr[] = {1, 2, 3, 4, 5};
^
1 warning generated.

A-6. The tricky things in C Pointer: constants pointer differences

A-6.1 const TYPE *var and TYPE const *var

For the pointer to constants, you can change the pointer var (address) value itself, but you can’t change the value it points to!!!

char a = 'a';
char b = 'b';

const char *a_ptr = &a;
// char const *a_ptr = &a;

// You can change the pointer (address) value itself
a_ptr = &b;

// But you CANNOT change the value it points to
// error: read-only variable is not assignable
a_ptr = 'c';

</br>

A-6.2 *const TYPE var

For the constants pointer, you can change the value it points to, but you can’t change the pointer (address) value itself!!!

char a = 'a';
char b = 'b';

char *const a_ptr = &a;
// char const *a_ptr = &a;

// You can change the value it points to
*a_ptr = 'c';

// But you CANNOT change the pointer value itself
// error: cannot assign to variable 'a_ptr' with const-qualified type 'char *const'
a_ptr = &b;

A-6.3 const TYPE *const var and TYPE const *const var

For the constants pointer to constants, you can’t change both!!!

char a = 'a';
char b = 'b';

const char *const a_ptr = &a;
// char const *const a_ptr = &a;

// You can't change both

// error: read-only variable is not assignable
*a_ptr = 'c';

// error: cannot assign to variable 'a_ptr' with const-qualified type 'const char *const'
a_ptr = &b;

A-7. Deal with va_list (Variable Argument List, AKA ...)

... or va_list is super useful in C, it gives you the ability to define a flexible parameter list function.

Here is how it works:

  • va_list declares a variable argument list instance (but doesn’t initial yet)
    va_list args
        
  • va_start initializes the va_list with the first argument
    va_list args
    va_start(args, FIRST_ARGUMENT_NAME_HERE)
        
  • va_arg gives you back the next argument

    As va_arg doesn’t know how many bytes the argument is and how to stop, that’s why you need to provide the T data type to help it read the next argument.

    va_list args
    va_start(args, FIRST_ARGUMENT_NAME_HERE)
    var_arg(args, T)
        
  • va_end end the va_list that you have to call
    va_list args
    va_start(args, FIRST_ARGUMENT_NAME_HERE)
    va_arg(args, T)
    va_end(args)
        

There are 2 major forms to use ...

A-7.1 First argument is the total number of the rest arguments

int add_numbers(int rest_param_count, ...) {
    int sum = 0;

    // Uninitialized `va_list` instance
    va_list args;

    // Init the `va_list` instance with the first parameter
    va_start(args, rest_param_count);

    // Loop the rest params
    for (int i = 0; i < rest_param_count; i++) {
        sum += va_arg(args, int);
    }

    // Done with using `va_list` instance
    va_end(args);
    return sum;
}

printf("add_numbers result: %d\n", add_numbers(5, 1, 1, 1, 1));

So, if you pass the wrong rest_param_count or passing the wrong number of the rest parameters, result is undefined behaviours!!!

A-7.1 NULL ended style

You don’t need to pass the total number of rest params as the first parameter anymore

int add_numbers_2(int first_number, ...) {
    int sum = first_number;

    // Uninitialized `va_list` instance
    va_list args;

    // Init the `va_list` instance with the first parameter
    va_start(args, first_number);

    // Keep looping the rest until it hits `NULL` (0)
    int next_number = va_arg(args, int);
    while (next_number != 0) {
        sum += next_number;
        next_number = va_arg(args, int);
    }

    // Done with using `va_list` instance
    va_end(args);
    return sum;
}

printf("add_numbers_2 result: %d\n", add_numbers_2(1, 1, 1, 1, NULL));

But it messes up if you have a 0 in your parameters before NULL.

A-8. Macro

The macro in C is a super powerful weapon that helps you to generate the most flexible source code.

A-8.1 How to only run the preprocessor stage

You can run CC with the -E flag to generate the source code that only apply the preprocessor stage before compiling it.

CC -E src/utils/vec.c | bat
clang -E src/utils/vec.c | bat

A-8.2 Comment and empty line in macro

You only can use /* */ comment in macro body, // won’t work!!!

If you want an empty line, just add a \ (multi line character) there.

#define MY_MACRO(PARAM1) \
    /* Here is the comment line 1 */ \
    /* Here is the comment line 2 */ \
    /* Follow by a empty line */\
    \
    printf("Just a macro sample.")

A-8.3 String in macro

When using a macro argument starts with # (in the macro body), it treats as a string. That’s why the #FORMAT_TYPE (in the following sample) will become a part of the printf format string!!!

#define MACRO_PARAM_AS_STRING(INTEGER, FORMAT_TYPE) \
    printf("Here is integer you provied: " #FORMAT_TYPE, INTEGER)

int main() {
    MACRO_PARAM_AS_STRING(888, %u);
}

The above code will expand as the following:

int main() {
    printf("Here is integer you provied: " "%u", 888);
}

// And it prints out:
// Here is integer you provied: 888⏎

A-8.4 Expression in macro

If you want the macro parameter support passing in an expression, then you should wrap the parameter with () (in the macro body).

When you want to put all code expanded by macro into a code block scope, wrap your code inside ({}).

Here is the sample:

#include <stdio.h>
#include <time.h>

#define GET_AND_PRINT_CURRENT_TIME(PRINT_PREFIX, USE_CUSTOM_FORMAT)          \
    ({                                                                       \
        time_t t = time(NULL);                                               \
        if (USE_CUSTOM_FORMAT) {                                             \
            struct tm tm = *localtime(&t);                                   \
            printf("\n>>> " #PRINT_PREFIX " %d-%02d-%02d %02d:%02d:%02d\n",  \
                   tm.tm_mday, tm.tm_mon + 1, tm.tm_year + 1900, tm.tm_hour, \
                   tm.tm_min, tm.tm_sec);                                    \
        } else {                                                             \
            printf("\n>>> " #PRINT_PREFIX " %s", ctime(&t));                 \
        }                                                                    \
    })

//
int main() {
    GET_AND_PRINT_CURRENT_TIME("The current time in custom format: ", 2 > 1);
    GET_AND_PRINT_CURRENT_TIME("The current time: ", 1 > 2);
}
A-8.4.1 Use __VA_ARGS__ macro to pass … into another macro
/**
 * Log
 */
void __log__(LogLevel log_level, const char *module_name,
             const char *function_name, const char *format_str, ...);

/**
 * Debug log
 */
#define DEBUG_LOG(MODULE_NAME, FUNCTION_NAME, format_str, ...) \
    __log__(LL_DEBUG, #MODULE_NAME, #FUNCTION_NAME, format_str, __VA_ARGS__)

A-8.5 How to write a macro that includes #ifdef

The answer is NO, you can't do that!!! and you have to define 2 macros with the same name and wrap them into a #ifdef #else #endif block like below:

#ifdef PRINT_VEC_DEBUG_LOG
#define ASSIGN_PUSH_VEC_ELEMENT(PTR_TYPE)                                      \
    PTR_TYPE *next_ptr = (self->len == 1)                                      \
                             ? (PTR_TYPE *)self->data                          \
                             : (PTR_TYPE *)self->data + self->len - 1;         \
    printf("\n>>> " #PTR_TYPE " >>> self->data: %p, next_ptr: %p", self->data, \
           next_ptr);                                                          \
    *next_ptr = *(PTR_TYPE *)value;
#else
#define ASSIGN_PUSH_VEC_ELEMENT(PTR_TYPE)                              \
    PTR_TYPE *next_ptr = (self->len == 1)                              \
                             ? (PTR_TYPE *)self->data                  \
                             : (PTR_TYPE *)self->data + self->len - 1; \
    *next_ptr = *(PTR_TYPE *)value;
#endif

A-8.6 Auto type infer in macro

Official doc

Auto type infer supports by typeof and __auto_type

#define SHOW_TYPE_OF_VAR(A, B, C, D, E, F) \
    ({                                     \
        typeof(A) a = (A);                 \
        typeof(B) b = (B);                 \
        typeof(C) c = (C);                 \
        typeof(D) d = (D);                 \
        typeof(E) e = (E);                 \
        typeof(F) f = (F);                 \
    })

#define SHOW_TYPE_OF_VAR_2(A, B, C, D, E, F) \
    ({                                       \
        __auto_type a = (A);                 \
        __auto_type b = (B);                 \
        __auto_type c = (C);                 \
        __auto_type d = (D);                 \
        __auto_type e = (E);                 \
        __auto_type f = (F);                 \
    })

//
int main() {
    printf("\n>>> [Auto type infer in macro]\n");

    SHOW_TYPE_OF_VAR(0xFF, 256, -100, 3.5, -4.5, 100000);
    SHOW_TYPE_OF_VAR_2(0xFF, 256, -100, 3.5, -4.5, 100000);
}

A-8.7 Useful macro: Get back the data type from a variable

That’s the _Generic selection at compile time, doc is here

#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>

#define TYPE_NAME(x) \
    _Generic((x),                                                   \
    _Bool: "_Bool",                                                 \
    unsigned char: "unsigned char",                                 \
    char: "char",                                                   \
    signed char: "signed char",                                     \
    short int: "short int",                                         \
    unsigned short int: "unsigned short int",                       \
    int: "int",                                                     \
    unsigned int: "unsigned int",                                   \
    long int: "long int",                                           \
    unsigned long int: "unsigned long int",                         \
    long long int: "long long int",                                 \
    unsigned long long int: "unsigned long long int",               \
    float: "float",                                                 \
    double: "double",                                               \
    long double: "long double",                                     \
    char *: "pointer to char",                                      \
    void *: "pointer to void",                                      \
    _Bool *: "pointer to Bool",                                     \
    unsigned char *: "pointer to unsigned char",                    \
    signed char *: "pointer to signed char",                        \
    short int *: "pointer to short int",                            \
    unsigned short int *: "pointer to unsigned short int",          \
    int *: "pointer to int",                                        \
    unsigned int *: "pointer to unsigned int",                      \
    long int *: "pointer to long int",                              \
    unsigned long int *: "pointer to unsigned long int",            \
    long long int *: "pointer to long long int",                    \
    unsigned long long int *: "pointer to unsigned long long int",  \
    float *: "pointer to float",                                    \
    double *: "pointer to double",                                  \
    long double *: "pointer to long double",                        \
    default: "other")

//
//
//
int main() {
    printf("\n>>> [ Get data type from variable ]");

    uint8_t u8_v = 100;
    uint16_t u16_v = 100;
    uint32_t u32_v = 100;
    uint64_t u64_v = 100;
    int8_t i8_v = 100;
    int16_t i16_v = 100;
    int32_t i32_v = 100;
    int64_t i64_v = 100;
    size_t sizet_v = 100;
    _Bool _Bool_v = true;
    unsigned char unsigned_char_v = 0x0A;
    char char_v = 'a';
    signed char signed_char_v = 'a';
    short int short_int_v = 100;
    unsigned short int unsigned_short_int_v = 100;
    int int_v = 100;
    unsigned int unsigned_int_v = 100;
    long int long_int_v = 100;
    unsigned long int unsigned_long_int_v = 100;
    long long int long_long_int_v = 100;
    unsigned long long int unsigned_long_long_int_v = 100;
    float float_v = 1.0;
    double double_v = 1.0;
    long double long_double_v = 1.0;
    char *pointer_to_char_v = NULL;
    void *pointer_to_void_v = NULL;
    _Bool *pointer_to_Bool_v = NULL;
    unsigned char *pointer_to_unsigned_char_v = NULL;
    signed char *pointer_to_signed_char_v = NULL;
    short int *pointer_to_short_int_v = NULL;
    unsigned short int *pointer_to_unsigned_short_int_v = NULL;
    int *pointer_to_int_v = NULL;
    unsigned int *pointer_to_unsigned_int_v = NULL;
    long int *pointer_to_long_int_v = NULL;
    unsigned long int *pointer_to_unsigned_long_int_v = NULL;
    long long int *pointer_to_long_long_int_v = NULL;
    unsigned long long int *pointer_to_unsigned_long_long_int_v = NULL;
    float *pointer_to_float_v = NULL;
    double *pointer_to_double_v = NULL;
    long double *pointer_to_long_double_v = NULL;

    printf("\n>>> Type of 'u8_v' is: %s", TYPE_NAME(u8_v));
    printf("\n>>> Type of 'u16_v' is: %s", TYPE_NAME(u16_v));
    printf("\n>>> Type of 'u32_v' is: %s", TYPE_NAME(u32_v));
    printf("\n>>> Type of 'u64_v' is: %s", TYPE_NAME(u64_v));
    printf("\n>>> Type of 'i8_v' is: %s", TYPE_NAME(i8_v));
    printf("\n>>> Type of 'i16_v' is: %s", TYPE_NAME(i16_v));
    printf("\n>>> Type of 'i32_v' is: %s", TYPE_NAME(i32_v));
    printf("\n>>> Type of 'i64_v' is: %s", TYPE_NAME(i64_v));
    printf("\n>>> Type of 'usizet_v' is: %s", TYPE_NAME(sizet_v));
    printf("\n>>> Type of '_Bool_v' is: %s", TYPE_NAME(_Bool_v));
    printf("\n>>> Type of 'unsigned_char_v': %s", TYPE_NAME(unsigned_char_v));
    printf("\n>>> Type of 'char_v': %s", TYPE_NAME(char_v));
    printf("\n>>> Type of 'signed_char_v': %s", TYPE_NAME(signed_char_v));
    printf("\n>>> Type of 'short_int_v': %s", TYPE_NAME(short_int_v));
    printf("\n>>> Type of 'unsigned_short_int_v': %s",
           TYPE_NAME(unsigned_short_int_v));
    printf("\n>>> Type of 'int_v': %s", TYPE_NAME(int_v));
    printf("\n>>> Type of 'unsigned_int_v': %s", TYPE_NAME(unsigned_int_v));
    printf("\n>>> Type of 'long_int_v': %s", TYPE_NAME(long_int_v));
    printf("\n>>> Type of 'unsigned_long_int_v': %s",
           TYPE_NAME(unsigned_long_int_v));
    printf("\n>>> Type of 'long_long_int_v': %s", TYPE_NAME(long_long_int_v));
    printf("\n>>> Type of 'unsigned_long_long_int_v': %s",
           TYPE_NAME(unsigned_long_long_int_v));
    printf("\n>>> Type of 'float_v': %s", TYPE_NAME(float_v));
    printf("\n>>> Type of 'double_v': %s", TYPE_NAME(double_v));
    printf("\n>>> Type of 'long_double_v': %s", TYPE_NAME(long_double_v));
    printf("\n>>> Type of '*pointer_to_char_v': %s",
           TYPE_NAME(pointer_to_char_v));
    printf("\n>>> Type of '*pointer_to_void_v': %s",
           TYPE_NAME(pointer_to_void_v));
    printf("\n>>> Type of '*pointer_to_Bool_v': %s",
           TYPE_NAME(pointer_to_Bool_v));
    printf("\n>>> Type of '*pointer_to_unsigned_char_v': %s",
           TYPE_NAME(pointer_to_unsigned_char_v));
    printf("\n>>> Type of '*pointer_to_signed_char_v': %s",
           TYPE_NAME(pointer_to_signed_char_v));
    printf("\n>>> Type of '*pointer_to_short_int_v': %s",
           TYPE_NAME(pointer_to_short_int_v));
    printf("\n>>> Type of '*pointer_to_unsigned_short_int_v': %s",
           TYPE_NAME(pointer_to_unsigned_short_int_v));
    printf("\n>>> Type of '*pointer_to_int_v': %s",
           TYPE_NAME(pointer_to_int_v));
    printf("\n>>> Type of '*pointer_to_unsigned_int_v': %s",
           TYPE_NAME(pointer_to_unsigned_int_v));
    printf("\n>>> Type of '*pointer_to_long_int_v': %s",
           TYPE_NAME(pointer_to_long_int_v));
    printf("\n>>> Type of '*pointer_to_unsigned_long_int_v': %s",
           TYPE_NAME(pointer_to_unsigned_long_int_v));
    printf("\n>>> Type of '*pointer_to_long_long_int_v': %s",
           TYPE_NAME(pointer_to_long_long_int_v));
    printf("\n>>> Type of '*pointer_to_unsigned_long_long_int_v': %s",
           TYPE_NAME(pointer_to_unsigned_long_long_int_v));
    printf("\n>>> Type of '*pointer_to_float_v': %s",
           TYPE_NAME(*pointer_to_float_v));
    printf("\n>>> Type of '*pointer_to_double_v': %s",
           TYPE_NAME(pointer_to_double_v));
    printf("\n>>> Type of '*pointer_to_long_double_v': %s",
           TYPE_NAME(pointer_to_long_double_v));
}

// >>> [ Get data type from variable ]
// >>> Type of 'u8_v' is: unsigned char
// >>> Type of 'u16_v' is: unsigned short int
// >>> Type of 'u32_v' is: unsigned int
// >>> Type of 'u64_v' is: unsigned long long int
// >>> Type of 'i8_v' is: signed char
// >>> Type of 'i16_v' is: short int
// >>> Type of 'i32_v' is: int
// >>> Type of 'i64_v' is: long long int
// >>> Type of 'usizet_v' is: unsigned long int
// >>> Type of '_Bool_v' is: _Bool
// >>> Type of 'unsigned_char_v': unsigned char
// >>> Type of 'char_v': char
// >>> Type of 'signed_char_v': signed char
// >>> Type of 'short_int_v': short int
// >>> Type of 'unsigned_short_int_v': unsigned short int
// >>> Type of 'int_v': int
// >>> Type of 'unsigned_int_v': unsigned int
// >>> Type of 'long_int_v': long int
// >>> Type of 'unsigned_long_int_v': unsigned long int
// >>> Type of 'long_long_int_v': long long int
// >>> Type of 'unsigned_long_long_int_v': unsigned long long int
// >>> Type of 'float_v': float
// >>> Type of 'double_v': double
// >>> Type of 'long_double_v': long double
// >>> Type of '*pointer_to_char_v': pointer to char
// >>> Type of '*pointer_to_void_v': pointer to void
// >>> Type of '*pointer_to_Bool_v': pointer to Bool
// >>> Type of '*pointer_to_unsigned_char_v': pointer to unsigned char
// >>> Type of '*pointer_to_signed_char_v': pointer to signed char
// >>> Type of '*pointer_to_short_int_v': pointer to short int
// >>> Type of '*pointer_to_unsigned_short_int_v': pointer to unsigned short int
// >>> Type of '*pointer_to_int_v': pointer to int
// >>> Type of '*pointer_to_unsigned_int_v': pointer to unsigned int
// >>> Type of '*pointer_to_long_int_v': pointer to long int
// >>> Type of '*pointer_to_unsigned_long_int_v': pointer to unsigned long int
// >>> Type of '*pointer_to_long_long_int_v': pointer to long long int
// >>> Type of '*pointer_to_unsigned_long_long_int_v': pointer to unsigned long long int
// >>> Type of '*pointer_to_float_v': float
// >>> Type of '*pointer_to_double_v': pointer to double
// >>> Type of '*pointer_to_long_double_v': pointer to long double

A-8.8 Useful macro: Is it the same type between 2 variables/values

//
//
//
#define IS_IT_THE_SAME_TYPE(a, b)                                            \
    ({                                                                       \
        char _a_type[50] = TYPE_NAME((a));                                   \
        char _b_type[50] = TYPE_NAME((b));                                   \
        _Bool is_same_str_non_case_sensitive = strcasecmp(_a_type, _b_type); \
        (is_same_str_non_case_sensitive == 0);                               \
    })

int main() {
    /* usize *aaa = NULL; */
    /* size_t *bbb = NULL; */
    /* char aaa[10] = "asdfasdf"; */
    /* char bbb[20] = "AAAA"; */
    uint8_t aaa[5] = {1, 2, 3, 4, 5};
    uint8_t bbb[3] = {9, 10, 11};
    printf("\n>>> aaa type is: %s",TYPE_NAME(aaa));
    printf("\n>>> bbb type is: %s",TYPE_NAME(bbb));

    _Bool is_same_type_between_a_and_b = IS_IT_THE_SAME_TYPE(aaa, bbb);

    if (is_same_type_between_a_and_b) {
        printf("\n>>>> Yes, a and b ARE the same type.");
    } else {
        printf("\n>>>> Yes, a and b ARE NOT the same type.");
    }

    return 0;
}

// >>> aaa type is: pointer to unsigned char
// >>> bbb type is: pointer to unsigned char
// >>>> Yes, a and b ARE the same type.⏎

A-8.9 Generic implementation by using macro

Let’s see what C deals with generic:)

The Result type here is just trying to show you how the magic word generic works under the hood.

Let’s take the Rust generic type Result<T,E> as an example.

Suppose you have the following rust code:

pub struct MyResult<T, E> {
    success: bool,
    ok_value: T,
    err_value: E,
}

fn main() {
    let result_1: MyResult<usize, u8> = MyResult::<usize, u8> {
         success: true,
         ok_value: 100,
         err_value: 0
    };

    let result_2: MyResult<f32, u8> = MyResult::<f32, u8> {
         success: true,
         ok_value: 1.0f32,
         err_value: 0
    };
}

When Rust compiles this code, it performs monomorphization. During the process, the rustc read the values that have need used in MyResult<T,E>, and produce 2 different types wit the concrete types like below:

The sample code comes from Generic Data Types in the Rust official guide (The Rust Programming Language):

// pseudo code

pub struct MyResult_usize_u8{
    success: bool,
    ok_value: usize,
    err_value: u8,
}

pub struct MyResult_f32_u8 {
    success: bool,
    ok_value: f32,
    err_value: u8,
}

So, that’s nearly duplicated code? YES, you’re right and that’s how it works:)

In C, you can do the same thing with the magical macro:)

Because the magical thing is all about the nearly duplicated code, that means you can’t use the regular include guard pattern like below to prevent the generic implementation .h file from being included more than once:

#ifndef __RESULT_H__
#define __RESULT_H__

//... Your code inside include guard

#endif

So, suppose that you want to implement the same MyResult type above in the generic way in C. That means you need 3 generic types:

  • MY_RESULT_TYPE as the typedef struct type name
  • MY_RESULT_SUCCESS_TYPE as the ok data type
  • MY_RESULT_ERROR_TYPE as the err data type

Let’s do it:

//
// Throw error if the caller doesn't define the following `type name` which
// uses to generate the concrete type struct definition
//
#if !defined(MY_RESULT_TYPE) || !defined(MY_RESULT_SUCCESS_TYPE) || \
    !defined(MY_RESULT_ERROR_TYPE)
#error Missing MY_RESULT_TYPE or MY_RESULT_SUCCESS_TYPE or MY_RESULT_ERROR_TYPE definition
#endif

//
// Define macros that uses to create concrete type struct definition
//
#define MY_RESULT_CONCAT(tag, method) tag##_##method
#define MY_RESULT_METHOD2(tag, method) MY_RESULT_CONCAT(tag, method)
#define MY_RESULT_METHOD(method) MY_RESULT_METHOD2(MY_RESULT_TYPE, method)

//
// Generic (result type) struct
//
typedef struct {
    _Bool success;
    MY_RESULT_SUCCESS_TYPE *ok;
    MY_RESULT_ERROR_TYPE *err;
} MY_RESULT_TYPE;

//
// Similar to `Result::Ok(T)` and allocate on the heap
//
MY_RESULT_TYPE *MY_RESULT_METHOD(Ok)(MY_RESULT_SUCCESS_TYPE *ok) {
    MY_RESULT_TYPE *r = malloc(sizeof(MY_RESULT_TYPE));
    r->success = true;
    r->ok = ok;
    r->err = NULL;

    return r;
}

//
// Similar to `Result::Err(T)` and allocate on the heap
//
MY_RESULT_TYPE *MY_RESULT_METHOD(Err)(MY_RESULT_ERROR_TYPE *err) {
    MY_RESULT_TYPE *r = malloc(sizeof(MY_RESULT_TYPE));
    r->success = false;
    r->ok = NULL;
    r->err = err;

    return r;
}

#undef MY_RESULT_TYPE
#undef MY_RESULT_SUCCESS_TYPE
#undef MY_RESULT_ERROR_TYPE
#undef MY_RESULT_CONCAT
#undef MY_RESULT_METHOD2
#undef MY_RESULT_METHOD

Ok, let’s explain step-by-step:

  • Check the caller (includer) to see whether it defines the required type macros or not:
    //
    // Throw error if the caller doesn't define the following `type name` which
    // uses to generate the concrete type struct definition
    //
    #if !defined(MY_RESULT_TYPE) || !defined(MY_RESULT_SUCCESS_TYPE) || \
            !defined(MY_RESULT_ERROR_TYPE)
    #error Missing MY_RESULT_TYPE or MY_RESULT_SUCCESS_TYPE or MY_RESULT_ERROR_TYPE definition
    #endif
        

    The directive #error causes the preprocessor to report a fatal error, then you can see the error when you compile the project.

  • Define the helper macro to define XX_YY method function name
    //
    // Define macros that uses to create concrete type struct definition
    //
    #define MY_RESULT_CONCAT(struct_name, method_name) struct_name##_##method_name
    #define CREATE_STRUCT_METHOD_HELPER(struct_name, method_name) MY_RESULT_CONCAT(struct_name, method_name)
    #define CREATE_STRUCT_METHOD(method_name) CREATE_STRUCT_METHOD_HELPER(MY_RESULT_TYPE, method_name)
        

    Suppose the caller (includer) source file has the following macros:

    #define MY_RESULT_TYPE EndpointApiResult
        

    So, the CREATE_STRUCT_METHOD(Ok) macro call will be expanded as *EndpointApiResult_Ok. That’s the way to generate the generic struct method name:)

  • The final important part is to undefine the caller (includer)’s macros
    #undef MY_RESULT_TYPE
    #undef MY_RESULT_SUCCESS_TYPE
    #undef MY_RESULT_ERROR_TYPE
    #undef MY_RESULT_CONCAT
    #undef CREATE_STRUCT_METHOD_HELPER
    #undef CREATE_STRUCT_METHOD
        
  • So, from now on, every source code is able to include this .h to create their own concrete type version of MyResult struct and method.

    Here is the example to show it:

    #define MY_RESULT_TYPE EndpointApiResult
    #define MY_RESULT_SUCCESS_TYPE char
    #define MY_RESULT_ERROR_TYPE uint16_t
    
    #include "utils/result.h"
    
    char *success_result = "Got something back:)";
    uint16_t fail_result = 404;
    
    //
    EndpointApiResult *simulate_call_api_success(_Bool simulate_success) {
        EndpointApiResult *result = (simulate_success)
                                        ? EndpointApiResult_Ok(success_result)
                                        : EndpointApiResult_Err(&fail_result);
    
        return result;
    }
        

    As you can see above, a few things to pay attention to :

    • Those 3 #define should be written before the #include "utils/result.h", as preprocessor handle source file from top to bottom.
    • Before the #include "utils/result.h" be evaluated, the EndpointApiResult doesn’t exists. It only exists after the preprocessor finishs:

      You can run the CC -E src/result_demo.c to see the preprocessor result:

      typedef struct {
          _Bool success;
          char *ok;
          uint16_t *err;
      } EndpointApiResult;
      
      EndpointApiResult *EndpointApiResult_Ok(char *ok) {
          EndpointApiResult *r = malloc(sizeof(EndpointApiResult));
          r->success = 1;
          r->ok = ok;
          r->err = ((void*)0);
      
          return r;
      }
      
      EndpointApiResult *EndpointApiResult_Err(uint16_t *err) {
          EndpointApiResult *r = malloc(sizeof(EndpointApiResult));
          r->success = 0;
          r->ok = ((void*)0);
          r->err = err;
      
          return r;
      }
              

      That’s the way how C deals with generic:)

For more details, open src/utils/result.h and src/result_demo.c to have a look and run the following command to give it a try:

make c_demo_result && ./c_demo_result

# >>> [ Result type demo ]
# Result {
#         success: true
#         ok: Got something back:)
#         err: NULL
# }
# 
# Result {
#         success: false
#         ok: NULL
#         ok: 404
# }