why I still write C
Only third to Holy-C and x86 Assembly, C is my favorite language to program in.
I started using C in high school, and it has been my primary programming language ever since.
I keep coming back to C for a few reasons:
1. It gives me a deep understanding of how computers work at a low level, which helps me with low-level research.
2. C has a small standard library and minimal abstractions (compared to languages like Rust or Go), which allows me to write very efficient and minimal code.
3. There is a one-to-one mapping between C code and assembly (for the most part), which makes it easier to reason about what the code is doing under the hood.
4. It is widely supported and used in many domains (e.g. embedded, OS development, security research), so it is a valuable skill to have.
5. It's elegant <3.
Low Level Understanding
C allows me to understand how memory management, pointers, and data structures work at a low level. Crucial for security research, but also just satisfying to understand how things work under the hood. Example:
Pointer Arithmetic
#include <stdio.h>
int main() {
int arr[4] = {10, 20, 30, 40};
int *p = arr;
printf("Base address: %p\n", (void*)p);
printf("p + 1 (int*): %p\n", (void*)(p + 1));
printf("p + 2 (byte*): %p\n", (void*)(p + 2));
printf("\nValues using pointer arithmetic:\n");
printf("*(p + 0) = %d\n", *(p + 0));
printf("*(p + 1) = %d\n", *(p + 1));
printf("*(p + 2) = %d\n", *(p + 2));
char* byte_ptr = (char*)arr;
printf("\nByte-level movement:\n");
printf("Base Address: %p\n", (void*)byte_ptr);
printf("byte_ptr + 1: %p\n", (void*)(byte_ptr + 1));
printf("byte_ptr + 2: %p\n", (void*)(byte_ptr + 2));
return 0;
}
Efficiency & Minimalism
C is a very efficient programming language, as it provides direct access to hardware through the use of machine code instructions, with minimal overhead resulting from compilation. With C, you have complete control over memory allocation and deallocation (no garbage collector), meaning that there are no hidden abstractions slowing down the performance of your code. Because you have such a high level of control, operating systems, embedded systems, etc., are still written in C decades after its introduction, due to the need for fast-performing solutions. Example:
Bump Allocator
#include <unistd.h>
static char pool[4096];
static size_t offset = 0;
void *bump_alloc(size_t size) {
if (offset + size > sizeof(pool))
return (void *)0;
void *ptr = pool + offset;
offset += size;
return ptr;
}
int main() {
char *name = bump_alloc(32);
int *nums = bump_alloc(4 * sizeof(int));
const char msg[] = "allocated from pool\n";
write(STDOUT_FILENO, msg, sizeof(msg) -1); // direct syscall, no stdio overhead
return 0;
}
One-to-One Mapping with Assembly
C code can usually be converted directly into machine code and executed as it appears; in fact, every line of C code corresponds to at least one assembly language instruction. When you write C code, you don't have to worry about an interpreter, virtual machine, or any additional level of abstraction between the two programming languages. You essentially write C and the hardware executes what you wrote. Therefore, because of this 1:1 mapping from C to assembly language, performance is very easy to predict since you have an accurate idea of how the processor will execute the underlying machine code.
Example: C to Assembly
C representation:
#include <stdio.h>
int multiply(int a, int b) {
return a * b;
}
int main() {
int x = 5;
int y = 10;
int result = multiply(x, y);
printf("%d\n", result);
return 0;
}
x86-Assembly representation:
section .data
fmt db "%d", 10, 0
section .text
global main
multiply:
imul edi, esi
mov eax, edi
ret
main:
push rbp
mov rbp, rsp
sub rsp, 16
mov DWORD PTR [rbp-4], 5
mov DWORD PTR [rbp-8], 10
mov edi, 5
mov esi, 10
call multiply
mov DWORD PTR [rbp-12], eax
mov esi, eax
lea rdi, [fmt]
call printf
mov eax, 0
pop rbp
ret
Elegance <3
Just a quick example of how C can be elegant in its simplicity, while still being powerful and efficient:
Type-agnostic Quicksort
#include <stdio.h>
#include <string.h>
void swap(void *a, void *b, size_t size){
char tmp[size];
memcpy(tmp, a, size);
memcpy(a, b, size);
memcpy(b, tmp, size);
}
void qsort_generic(void *base, size_t n, size_t size,
int (*cmp)(const void *, const void *)) {
if (n <= 1) return;
char *arr = base;
char *pivot = arr + (n - 1) * size;
size_t i = 0;
for (size_t j = 0; j < n - 1; j++) {
if (cmp(arr + j * size, pivot) < 0)
swap(arr + (i++) * size, arr + j * size, size);
}
swap(arr + i * size, pivot, size);
qsort_generic(arr, i, size, cmp);
qsort_generic(arr + (i + 1) * size, n - i - 1, size, cmp);
}
// any type add here
int cmp_int(const void *a, const void *b) { return *(int*)a - *(int*)b; }
int cmp_str(const void *a, const void *b) { return strcmp(*(char**)a, *(char**)b); }
int main() {
int nums[] = {42, 7, 1, 99, 3, 23, 8};
size_t n = sizeof nums / sizeof *nums;
qsort_generic(nums, n, sizeof(int), cmp_int);
for (size_t i = 0; i < n; i++) printf("%d ", nums[i]);
printf("\n");
const char *words[] = {"lambda", "closure", "monad", "functor", "apply"};
n = sizeof words / sizeof *words;
qsort_generic(words, n, sizeof(char*), cmp_str);
for (size_t i = 0; i < n; i++) printf("%s ", words[i]);
printf("\n");
}
More to add. In Progress...