This is my personal C
utilities which contain the following modules:
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:
// `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)
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:)
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:)
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:)
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:)⏎
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:)⏎
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);
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)⏎
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:
- Create
struct HeapString
instance on the heap, attach thecleanup
attribute to that variable, then theauto_free_string
function gets call when it out of scope.__attribute__((cleanup(auto_free_string))) String abc = HS_from_empty();
- 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 auctalchar *
on the heap
- One for creating
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);
}
Handy logging implementation.
It’s only available when ENABLE_DEBUG_LOG
macro is defined!!!
Use to print the single variable’s value, only for debugging purposes.
LOG_VAR(VAR_NAME)
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
/**
* 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, ...)
#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:)
Handle convertion between char *
and u8[]
/*
* 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);
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
/*
* 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);
// `+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 - ------------------
Handy memory utils.
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.
PRINT_MEMORY_BLOCK(TYPE_NAME, VAR_NAME)
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 - --------
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.
PRINT_MEMORY_BLOCK_FOR_SMART_TYPE(TYPE_NAME, VAR_NAME, TYPE_SIZE)
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
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(®2) ? "Yes" : "No");
printf("\n>>> reg2->adc enabled: %s", is_adc_enabled(®2) ? "Yes" : "No");
printf("\n>>> reg2->i2c enabled: %s", is_i2c_enabled(®2) ? "Yes" : "No");
printf("\n>>> reg2->spi enabled: %s", is_spi_enabled(®2) ? "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⏎
High resolution timer utils
/*
* 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);
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
MAKE_UNIQUE_PTR
simulates the std::make_unique
in C++
:
MAKE_UNIQUE_PTR(VAR_DEFINE, DESTRUCTOR)
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==
Handy macros to handle bits, only available when ENABLE_DEBUG_LOG
macro is defined!!!
PRINT_BITS(VAR_NAME)
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
Check whether the given bit is 1 or not
BIT_IS_1(VAR_NAME, WHICH_BIT)
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
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:
// `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
// `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
This project has 2 cmake
setups for different purposes:
cmake/CMakelists.txt
Use
C
compiler to compilemain.c
and support to usememory leaking tools
to check memory leaking issue on the pureC
binary.cmake/unit_test/CMakelists.txt
Use
Unity
to do 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
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
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).
./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
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.
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
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
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
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 SYSTEM | MACRO PRESENT | NOTES |
---|---|---|
Windows 32 bit + 64 bit | _WIN32 | for all Windows OS |
Windows 64 bit | _WIN64 | Only for 64 bit Windows |
Apple | __APPLE__ | for all Apple OS |
Apple | __MACH__ | alternative to above |
iOS embedded | TARGET_OS_EMBEDDED | include TargetConditionals.h |
iOS stimulator | TARGET_IPHONE_SIMULATOR | include TargetConditionals.h |
iPhone | TARGET_OS_IPHONE | include TargetConditionals.h |
MacOS | TARGET_OS_MAC | include TargetConditionals.h |
Android | __ANDROID__ | subset of linux |
Unix based OS | __unix__ | |
Linux | __linux__ | subset of unix |
POSIX based | _POSIX_VERSION | Windows with Cygwin |
Solaris | __sun | |
HP UX | __hpux | |
BSD | BSD | all BSD flavors |
DragonFly BSD | __DragonFly__ | |
FreeBSD | __FreeBSD__ | |
NetBSD | __NetBSD__ | |
OpenBSD | __OpenBSD__ |
Here is the C Date Types
C | Rust |
---|---|
[ Integer ] | |
char | i8 |
unsigned char | u8 |
short | i16 |
unsigned short | u16 |
int | i32 |
unsigned int | u32 |
long | i64 |
unsigned long | u64 |
size_t | u64/usize (But u32 in FreeBSD ) |
[ Floating point ] | |
float | f32 |
double | f64 |
[ Boolean ] | |
_Bool | bool |
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);
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>());
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⏎
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}");
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`:
//
// `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;
}
}
}
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:
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:)
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.
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) | 0x100 | 0x00 |
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) | 0x100 | 0x00 |
points to |
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
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 apointer to u16
typeloop_ptr + X
:It’s saying: add/move the pointer address to
X unit of pointed type
from the current position (start from0x820d9d686
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 typeu16
!!!
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.
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>
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;
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;
...
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 theva_list
with the first argumentva_list args va_start(args, FIRST_ARGUMENT_NAME_HERE)
va_arg
gives you back the next argumentAs
va_arg
doesn’t know how many bytes the argument is and how to stop, that’s why you need to provide theT
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 theva_list
that you have to callva_list args va_start(args, FIRST_ARGUMENT_NAME_HERE) va_arg(args, T) va_end(args)
There are 2 major forms to use ...
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!!!
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
.
The macro in C
is a super powerful weapon that helps you to generate the most flexible source code.
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
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.")
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⏎
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);
}
/**
* 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__)
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
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);
}
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
//
//
//
#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.⏎
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 thetypedef struct
type nameMY_RESULT_SUCCESS_TYPE
as theok
data typeMY_RESULT_ERROR_TYPE
as theerr
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 ofMyResult
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, theEndpointApiResult
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:)
- Those 3
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
# }