< back to thoughts

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...

you have wasted 0s on this site