xhartae/chapters/chapter_0.c

281 lines
16 KiB
C

/*
Copyright (c) 2023 : Ognjen 'xolatile' Milan Robovic
Xhartae is free software! You will redistribute it or modify it under the terms of the GNU General Public License by Free Software Foundation.
And when you do redistribute it or modify it, it will use either version 3 of the License, or (at yours truly opinion) any later version.
It is distributed in the hope that it will be useful or harmful, it really depends... But no warranty what so ever, seriously. See GNU/GPLv3.
*/
#ifndef CHAPTER_0_SOURCE // These two, and "#endif" at the end of the file are header guards, we'll talk about them more when the time comes!
#define CHAPTER_0_SOURCE
#include "chapter_0.h" // We're pasting macros, enumerations and function declarations from header file "chapter_0.h" into this file, at this location.
/*
Function 'in' will perform read system call, literally reading 'size' bytes from standard input, which is terminal in most kernels unless it's redirected to some other file
descriptor, and store it at 'data' memory address, if there's enough space in it. Since this is one of the core functions, if it fails, we want to abort the program and see what
we did wrong... Maybe there wasn't enough space in 'data', maybe 'size' was negative and it overflowed because read system call internally uses 'size_t / unsigned long int', which
is 64 bits wide, maybe we made some other mistake? Just abort, find the error and fix it.
*/
void in (void * data, int size) {
fatal_failure (data == NULL, "in: Failed to read from standard input, data is null pointer."); // This function is defined below, but we can call it here.
fatal_failure (size == 0, "in: Failed to read from standard input, size is zero."); // That's because we declared it previously. Look at 'out' function.
(void) read (STDIN_FILENO, data, (unsigned long int) size); // I cast to void type return value of read and write, because I don't really care about it.
}
/*
Similar to 'in' function and read system call, write system call will store 'size' bytes from 'data' memory address into standard output, which is usually what you see in your
terminal. Now, I won't talk much about teletypes, virtual terminals, file descriptor redirections and similar stuff, because I'm not very knowledgable about them. What matters is,
you have a working operating system, terminal and compiler, and you can make things happen. Once you learn C better than me, and start writing your own multi-threaded kernel, core
libraries, compilers and what not, you'll care about those things. I'll briefly talk about function structure for 'reallocate' function soon.
*/
void out (void * data, int size) {
fatal_failure (data == NULL, "out: Failed to write to standard output, data is null pointer."); // Notice how we can use function 'fatal_failure' before its' definition.
fatal_failure (size == 0, "out: Failed to write to standard output, size is zero."); // That's because we declared it in 'chapter_0.h' header file.
(void) write (STDOUT_FILENO, data, (unsigned long int) size);
}
/*
Function 'echo' is just a simplification of function 'out', because we'll be using it a lot, notice that it must accept null terminated strings, which are sort of C-style thing. I
really like them, because you don't need to always know size of the string in order to iterate it, but it requires some mental overhead in order to use them without creating hard
to find bugs, which is why newer programming languages consider them unsafe. They're not unsafe, they need to be used like intended.
*/
void echo (char * string) {
out (string, string_length (string)); // This function fails when we pass a string that's not null terminated, and we don't care to check for errors...
}
void fatal_failure (int condition, char * message) { // We use this function to abort the program if condition is met and to print the message.
if (condition == TRUE) { // If the variable 'condition' is not equal to 0, we execute the code in curly braces.
echo ("[\033[1;31mExiting\033[0m] "); // Simply printing the message using our 'echo' function, but we also use some colours, more on that later.
echo (message); // Also, notice how "this or that" is implicity 'char *' type... Maybe it's too early to explain it at this point.
echo ("\n"); // This will only print a new line, we'll see how to use it later.
exit (EXIT_FAILURE); // This is the function (and '_exit' system call) that aborts the program with a return code.
} // If condition isn't met, function will just return, and nothing is printed, execution continues.
}
void limit (int * value, int minimum, int maximum) { // This is somewhat similar to limiting a variable to some values, inclusively, we'll use it later.
if ( value == NULL) { return; } // We shouldn't dereference null pointer, but also don't want to abort the program for small mistake.
if (* value <= minimum) { * value = minimum; } // You can also align similar consecutive statements like this, we'll see it more often in switch statement later on...
if (* value >= maximum) { * value = maximum; } // If we pass a null pointer to this function, it won't do anything, just return.
}
/*
Memory management is a whole new topic that's too complex to cover it now in details, and it's the source of most security vunrabilities and hidden bugs. For now, just remember
that every program can read, write or execute only parts of the memory that the kernel allows it. Program can request new memory or release old memory, so some other programs can
use it. We'll learn to use program called Valgrind to find and fix memory related bugs in later chapters. We rely on functions 'calloc', 'realloc' and 'free' from <stdlib.h>
header file, and we'll avoid 'malloc', and use 'realloc' carefully, because they leave new memory uninitialized.
We're internally using function 'calloc' to request new memory, function 'realloc' to enlarge existing memory and function 'free' to release old memory, data that won't be used
later in the program. It's important to "free" all "malloc/calloc/realloc"-ed memory when program finishes successfully, and in some special cases, even when program fails and
aborts. It's important for safety to do so, think of it like open and close braces, if you have some allocations, you should deallocate them later.
Some examples of using them directly (not wrapping them like I like to do) are:
@C
char * data = NULL;
data = malloc (20 * sizeof (* data)); // Allocates 20 bytes of memory for 'data'.
data = calloc (20, sizeof (* data)); // Allocates 20 bytes also, but initializes them to 0 value.
data = realloc (data, 20 * sizeof (* data)); // When 'data' is null pointer, it will be same as 'malloc', else it will reallocate more memory (for correct usage).
// Also, it's best to just use 'calloc', but it complicates some other tasks.
free (data); // Deallocates memory, we'll talk about "double free" later.
@
*/
void * allocate (int size) {
char * data = NULL;
data = calloc ((unsigned long int) size, sizeof (* data));
fatal_failure (data == NULL, "standard : allocate : Failed to allocate memory, internal function 'calloc' returned null pointer.");
return ((void *) data);
}
/*
Now, lets see that code formatting in action, with briefly describing function structure in C programming language. Our function is called "reallocate", its' inputs (arguments)
are "data" with type 'void *' (pointer to any type of memory address) and "size" with type 'int' (integer), and its' output is also 'void *' (some memory address). All code
between first '{' and last connected '}' is part of that function. We're using function 'realloc' inside, but we check for error (it return 'NULL' on error), then we print message
and abort the program if there was an error, it there wasn't, we return new enlarged chunk of memory, changing the "data" variable.
*/
void * reallocate (void * data, int size) {
data = realloc (data, (unsigned long int) size);
fatal_failure (data == NULL, "standard : reallocate: Failed to reallocate memory, internal function 'realloc' returned null pointer.");
return (data);
}
void * deallocate (void * data) {
if (data != NULL) {
free (data);
}
return (NULL);
}
/*
This program is intended to be a book-like guide for this source code, which is also a book. We'll deal with strings a lot, and they're a good example of code formatting which is
the main topic of chapter zero. In function 'string_length' we have for loop without a body, some people prefer to put '{}' or ';' in same or next line, to express the intention
that the loop shouldn't have a body (code block {}). I just put ';' on the same line. Also, functions 'string_*' could depend on functions 'string_*_limit', but we won't do that
now, and since we've already declared them in header file "chapter_0.h" we can define them and call them in whatever order we want. Nice.
@C
// Simple example of how we could make it dependable on 'string_*_limit' function...
int string_compare (char * string_0, char * string_1) {
return (string_0, string_1, string_length (string_0));
}
@
*/
int string_length (char * string) {
int length;
fatal_failure (string == NULL, "string_length: String is null pointer.");
for (length = 0; string [length] != CHARACTER_NULL; ++length); // Since in C, strings are null terminated, looping until we see null character is strings' length.
return (length);
}
/*
Now, I've implemented "unlimited" versions of string comparison, copying and concatenation different from "limited" versions. They correspond with standard library functions
'strcmp', 'strcpy', 'strcat', 'strncmp', 'strncpy' and 'strncat' found in header file <string.h>. In "unlimited" versions, I rely on the fact that we want to apply the operation
on entire strings, that those strings are null terminated and I used that in my advantage. For example, function 'string_compare' could be something like this:
@C
int string_compare (char * string_0, char * string_1) {
int offset;
fatal_failure (string_0 == NULL, "string_compare: Destination string is null pointer.");
fatal_failure (string_1 == NULL, "string_compare: Source string is null pointer.");
for (offset = 0; (string_0 [offset] != CHARACTER_NULL) && (string_1 [offset] != CHARACTER_NULL); ++offset) {
if (string_0 [offset] != string_1 [offset]) {
return (FALSE);
}
}
return (TRUE);
}
@
And I used this approach below to show that you can solve the problem using different solutions... You'll notice that "limited" versions have variable 'offset' of type integer. We
use it to interate the strings, while in "unlimited" versions, we iterate on pointers to those strings, which are pushed to the stack. Both versions work, both versions give the
same results, you can use any of them.
*/
int string_compare (char * string_0, char * string_1) {
fatal_failure (string_0 == NULL, "string_compare: Destination string is null pointer."); // This will be seen in next 5 functions too, we don't want NULL here.
fatal_failure (string_1 == NULL, "string_compare: Source string is null pointer.");
for (; (* string_0 != CHARACTER_NULL) && (* string_1 != CHARACTER_NULL); ++string_0, ++string_1) { // We iterate until either string reaches the null character.
if (* string_0 != * string_1) { // In case that characters at the same offset are different:
return (FALSE); // > We return FALSE, 0, since strings aren't the same...
}
}
if (* string_0 != * string_1) { // Now, we'll do one last termination check.
return (FALSE);
}
return (TRUE); // Otherwise, strings are same, we return TRUE, 1.
}
char * string_copy (char * string_0, char * string_1) {
char * result = string_0; // We need to save pointer to destination string before changing it.
fatal_failure (string_0 == NULL, "string_copy: Destination string is null pointer.");
fatal_failure (string_1 == NULL, "string_copy: Source string is null pointer.");
for (; * string_1 != CHARACTER_NULL; ++string_0, ++string_1) { // This time and in next function, we iterate only source string.
* string_0 = * string_1; // And we assign character at the same offset to destination string (aka copy it).
}
* string_0 = CHARACTER_NULL; // Copying null termination, since the loop stopped on that condition.
return (result); // Lastly, we return the destination string, in order to be able to bind functions.
}
char * string_concatenate (char * string_0, char * string_1) {
char * result = string_0;
fatal_failure (string_0 == NULL, "string_concatenate: Destination string is null pointer.");
fatal_failure (string_1 == NULL, "string_concatenate: Source string is null pointer.");
string_0 += string_length (string_0); // We'll first offset destination string to the end of it.
// Because we want to start copying from the end, aka concatenate it.
for (; * string_1 != CHARACTER_NULL; ++string_0, ++string_1) { // The rest of the function is same as string_copy, so:
* string_0 = * string_1; // We could even use it here, but that defies the purpose of learning now.
}
* string_0 = CHARACTER_NULL; // Again, assign null termination.
return (result);
}
/*
As for "limited" versions of previous 3 functions, they do the same thing, but are capped to some variable 'limit'. These functions have their own use-case, for example, if
strings aren't null terminated, if you're not sure that they are null terminated, if we're dealing with binary (not textual) data (casted to char *), and many more cases.
*/
int string_compare_limit (char * string_0, char * string_1, int limit) {
int offset;
fatal_failure (string_0 == NULL, "string_compare_limit: Destination string is null pointer.");
fatal_failure (string_1 == NULL, "string_compare_limit: Source string is null pointer.");
for (offset = 0; offset < limit; ++offset) {
if (string_0 [offset] != string_1 [offset]) {
return (FALSE);
}
}
return (TRUE);
}
char * string_copy_limit (char * string_0, char * string_1, int limit) {
int offset;
fatal_failure (string_0 == NULL, "string_copy_limit: Destination string is null pointer.");
fatal_failure (string_1 == NULL, "string_copy_limit: Source string is null pointer.");
if ((limit <= 0) || (string_1 == NULL)) {
return (string_0);
}
for (offset = 0; offset < limit; ++offset) {
string_0 [offset] = string_1 [offset];
}
return (string_0);
}
char * string_concatenate_limit (char * string_0, char * string_1, int limit) {
int offset, length_0, length_1;
fatal_failure (string_0 == NULL, "string_concatenate_limit: Destination string is null pointer.");
fatal_failure (string_1 == NULL, "string_concatenate_limit: Source string is null pointer.");
if ((limit <= 0) || (string_1 == NULL)) {
return (string_0);
}
length_0 = string_length (string_0);
length_1 = string_length (string_1);
for (offset = 0; (offset < length_1) && (offset < limit); ++offset) {
string_0 [length_0 + offset] = string_1 [offset];
}
return (string_0);
}
#endif