- Rust-like containers in C
- Creates a
panic()andErrorsystem withOption<T>s andResult<T>s - lib/containers.c
#include <containers.c>
Result(char*) read_file(char* path) {
FILE* file = fopent(path, "r"); // line 4
fseekt(file, 0, SEEK_END);
long size = ftellt(file);
rewind(file);
char* buffer = malloct(size + 1);
freadt(buffer, 1, size, file);
buffer[size] = 0;
fcloset(file);
return Ok(buffer);
}
int main() {
char* content = unwrap(read_file("test/text.txt")); // line 19
puts(content);
}
test/text.txtis missing
error at read_file:4: No such file or directory
panicked at main() [line 19]
test/text.txthashello world
hello world
#include <containers.c>
#include <assert.h>
Option(char) first_letter(char* string) {
if(string != NULL && string[0] >= 'a' && string[0] <= 'z') {
return Some(string[0]);
}
return None;
}
int main(int argc, char** argv) {
assert(argc == 2);
Option(char) opt_letter = rescope(first_letter(argv[1]));
if_let(Some(letter), opt_letter) {
printf("the first letter is %c\n", letter);
} else {
printf("the first letter is not a lowercase letter\n");
}
}
./a.out hello
the first letter is h
./a.out 35
the first letter is not a lowercase letter
Whenever a container is created, it is defined as a dangling pointer that is bounded by the scope it is created in. We can see how this works by expanding a macro that creates a container.
Some(42)This macro creates an Option(int)
((Option) (typeof(42)[1]) { 42 }) // expandedLooking at this at first may seem a bit confusing, but expanding the typeof(42) and the Option type might make it a bit clearer
((void*) (int[1]) { 42 })The main part of this segment (int[1]) { 42 } creates an array on the stack of size 1 and sets the first value to 42. When converted to a value or a pointer, this part of the code will return the address of that heap memory (or a pointer to the value 42). The void* cast is there to keep the compiler from warning you about then casting this type to an Option(T)
But why go through all of this trouble when there are simpler ways to create containers? Well here are the reason why two of these simpler ways would not work.
- One way you may think of is using a struct like this
struct Option__int {
bool is_some;
int value;
};While that may seem like it works, you then have to think about how you are going to design a macro that does this. At first you could try just creating an unnamed struct everytime you call the Option(T) macro, something like this
#define Option(T) struct { bool is_some; T value; }This approach has a major flaw though: C compilers do not like when you try to cast two anonymous struct types to eachother
// in code like this, you will get an error that these two variables
// don't have the same type
Option(int) a;
Option(int) b = a; // error!- Ok, so why go through the trouble of stack allocating values and leaving dangling pointers? I mean you can just use
mallocand the pointer will be alive as long as you need.
The main issue I have with malloc is that it is slow. Especially for a data type that will be created and destroyed many times during a programs life-cycle, and often only holds a few bytes of data, it seems overkill to allocate memory on the heap. Not only is it slow, but its also anoying to then have to free these types which you will have so many of.
Another concern I have is what if malloc fails? Maybe I create another container for malloc inside said container, but won't I need malloc for that? And malloc has already failed, won't it just fail again? Overall, the idea just seems too tedious to deal with.
I know the name sounds scary, and it leaves a lot to be scared about, especially with undefined behaviour, but they have their upsides.
For one, these pointers are to memory on the stack, which is where most of your variables are stored. Compared to the heap allocated pointers from malloc, these are a lot faster. The main issue with them is their volatility, which often leads to undefined behaviour.
So how does one get around this? Well lets start with an example
int* create_dangling_pointer() {
int scoped_variable = 67;
return &scoped_variable;
}When this function is run, it puts the value 67 on the stack, then returns a pointer to this value, then the memory where that value is stored is "freed", allowing the next called function to use that memory. We can see this undefined behaviour if we call this function, then another, then print out the value
int main() {
int* dangling_pointer = create_dangling_pointer();
puts("messing with dangling_pointer");
printf("dangling_pointer's value: %d\n", *dangling_pointer);
}The program should in theory print out 67, but this is the result instead
messing with dangling_pointer
dangling_pointer's value: 1
If you remember, there is a line in Example 2 like this
Option(char) opt_letter = rescope(first_letter(argv[1]));This line uses the rescope() macro. What this does is it clones the dangling pointer's value and creates a new pointer attached to the current scope. This is the equivalent (in our previous code) of doing something like this
int main() {
int new_pointer_value = *create_dangling_pointer();
int* new_pointer = &new_pointer_value;
}Because the dangling pointer was dereferenced right after the function was called, and before another function could take over that memory on the stack, the value was able to be taken safely (ish) from the dangling pointer.
If you want to know more about the code, read it yourself in lib/containers.c. If you are using this and you encounter any issues, please feel free to create an issue on the Github repository.