{"article":{"slug":"c-for-rust-programmers","title":"C for Rust Programmers","subtitle":null,"summary":"A guide to the C programming language written from the perspective of someone who learned Rust first. BD103 walks through the surprising and unexpected parts of C for Rust programmers, including booleans and headers, strings and pointer lengths, integer types like size_t, the arrow operator, and the memory safety pitfalls Rust usually prevents.","content_type":"tutorial","language":"en","canonical_url":"https://bd103.dev/blog/2026-10-07-c-for-rust-programmers/","author":{"name":"BD103","url":"https://bd103.dev/","person_slug":null,"person_url":null},"authored_by":"human","publisher":{"name":"BD103's Blog","url":"https://bd103.dev/","listing_slug":null,"listing":null},"topics":[{"name":"Rust","slug":"rust","url":"https://listedarticles.com/topics/rust"},{"name":"Programming","slug":"programming","url":"https://listedarticles.com/topics/programming"},{"name":"Systems Programming","slug":"systems-programming","url":"https://listedarticles.com/topics/systems-programming"}],"about_listings":[],"cover_image_url":null,"license":"all-rights-reserved","word_count":2046,"reading_minutes":9,"published_at":"2026-10-07T00:00:00.000Z","added_at":"2026-10-07T17:13:33.946Z","updated_at":"2026-10-07T17:13:33.946Z","added_via":"api","contributor":{"type":"agent","name":"ListedStartups Using Bot","registered":true},"profile_url":"https://listedarticles.com/articles/c-for-rust-programmers","markdown_url":"https://listedarticles.com/articles/c-for-rust-programmers.md","example":false,"citation":"BD103, BD103's Blog. \"C for Rust Programmers.\" 7 Oct 2026. https://bd103.dev/blog/2026-10-07-c-for-rust-programmers/ (all-rights-reserved)","access":{"human_view":"preview","full_text_available":true,"source_url":"https://bd103.dev/blog/2026-10-07-c-for-rust-programmers/"},"body_markdown":"# C for Rust Programmers\n\nThe very first systems programming language I ever learned was Rust. This is uncommon compared to many other programmers; you're more likely to find someone who learned C or C++ first before coming to Rust. As such, there are plenty of \"Rust for C Programmers\" articles on the internet, but little to no \"C for Rust Programmers\" articles out there.\n\nWell, I'm about to change that! I've been learning C and C++ recently, and *holy cow those languages are quirky*. This blog post is a collection of eyebrow-raising details I learned while teaching myself C. (No C++ today, I'm not ready to dig into that can of worms.) This is not a substitute for a proper C tutorial, you'll need to Google one of those yourself. Rather, it's a list of things to keep in mind when working in the language.\n\nWith the premise set, let's draw the curtain and see what C has to offer!\n\n## Boolean Is Not* Built-In\n\nThe original version of C did not have a primitive type for booleans, programs instead used the integers 0 and 1. This was changed in [C99](https://en.wikipedia.org/wiki/C99), which added booleans in the optional [`<stdbool.h>`](https://en.cppreference.com/c/header/stdbool) header<sup>[\\[1\\]](https://bd103.dev/blog/2026-10-07-c-for-rust-programmers/#fn-_bool)</sup>:\n\n```\n#include <stdbool.h>\nint main() {\n    bool yes = true;\n    bool no = false;\n    return 0;\n}\n```\nEven still, `true` and `false` are not literals or keywords like in other languages. Instead, they are definitions that expand to 1 and 0:\n\n```\n#define true 1\n#define false 0\n```\nThis was changed again in [C23](<https://en.wikipedia.org/wiki/C23_(C_standard_revision)>)<sup>[\\[2\\]](https://bd103.dev/blog/2026-10-07-c-for-rust-programmers/#fn-c23)</sup>, so now booleans are true language primitives, but if you're compiling for earlier versions you'll need to include `<stdbool.h>`.\n\n## Null-Terminated Strings\n\nA Rust [`&str`](https://doc.rust-lang.org/stable/std/primitive.str.html) is 16 bytes: 8 bytes for the memory address and 8 bytes for the string length. This is because `str` is a dynamically sized type, and uses [pointer metadata](https://bd103.dev/blog/2023-08-06-ptr-metadata/) to track the length of the string.\n\n```\n// While a normal reference uses only 8 bytes...\nassert_eq!(std::mem::size_of::<&u8>(), 8);\n// ...strings use 16 bytes.\nassert_eq!(std::mem::size_of::<&str>(), 16);\n```\nThis approach makes fetching the string length extremely efficient, but requires more memory per `&str` reference. C uses a different approach: it doesn't store the size of its strings separately, but instead terminates every single string with a null byte (`\\0`). This is an intentional trade off that results in a few things:\n\n- C programs don't need to keep track of an extra `size` variable alongside the string<sup>[\\[3\\]](https://bd103.dev/blog/2026-10-07-c-for-rust-programmers/#fn-extra-size-variable)</sup>\n- Every single string needs room at the end for a null byte\n  - This means empty strings `\"\"` still take up one byte of memory!\n- This means empty strings \n- Null bytes cannot easily be used in the middle of a string without messing up [`<string.h>`](https://cppreference.com/c/header/string) 's functions\n- Forgetting the null terminator can result in out-of-bound reads\n\nIn practice, this requires you to remember to allocate extra space for the null terminator and insert it at the end of strings. For example, here is a program that reverses a string in C:\n\n```\nchar* reverse(char* forward) {\n    // Calculate the string's length, excluding the null terminator.\n    unsigned long len = strlen(forward);\n    // Allocate enough room for the string and its null terminator.\n    char* reversed = malloc(len + 1);\n    for (int i = 0; i < len; i++) {\n        reversed[i] = forward[len - 1 - i];\n    }\n    // Add the null terminator at the end.\n    reversed[len] = '\\0';\n    return reversed;\n}\n```\nNote lines 5 and 12, which take special measure to account for the null terminator. For reference, the corresponding Rust function<sup>[\\[4\\]](https://bd103.dev/blog/2026-10-07-c-for-rust-programmers/#fn-rust-reverse-string)</sup> doesn't need to do so:\n\n```\nfn reverse(forward: &[u8]) -> Box<[u8]> {\n    let len = forward.len();\n    let mut reversed = Box::<[u8]>::new_uninit_slice(len);\n    for i in 0..len {\n        reversed[i].write(forward[len - 1 - i]);\n    }\n    unsafe { reversed.assume_init() }\n}\n```\n## Target-Dependent Integer Widths\n\nC's integer types are not guaranteed to use an exact number of bits, instead the width varies depending on the target platform.\n\n| Type | C Standard | 64-bit Unix | 64-bit Windows | \n|---|---|---|---|\n| `char` | at least 8 bits | 8 bits | 8 bits | \n| `short` | at least 16 bits | 16 bits | 16 bits | \n| `int` | at least 16 bits | 32 bits | 32 bits | \n| `long` | at least 32 bits | 64 bits | 32 bits | \n| `long long` | at least 64 bits | 64 bits | 64 bits | \n\n[via cppreference.com](https://cppreference.com/c/language/arithmetic_types)\n\n`long` being 64 bits on Unix and 32 bits on Windows particularly irks me. I recommend following the advice that [a friend of mine](https://github.com/magicalbat) gave me a few years ago: if you care about cross-platform compatibility, only use the fixed width integers provided by [`<stdint.h>`](https://cppreference.com/c/header/stdint):\n\n| Rust | C | \n|---|---|\n| `u8` | `uint8_t` | \n| `u16` | `uint16_t` | \n| `u32` | `uint32_t` | \n| `u64` | `uint64_t` | \n| `usize` | `size_t`<sup>[\\[5\\]](https://bd103.dev/blog/2026-10-07-c-for-rust-programmers/#fn-size_t-and-ptrdiff_t)</sup> | \n| `i8` | `int8_t` | \n| `i16` | `int16_t` | \n| `i32` | `int32_t` | \n| `i64` | `int64_t` | \n| `isize` | `ptrdiff_t`<sup>[\\[5\\]](https://bd103.dev/blog/2026-10-07-c-for-rust-programmers/#fn-size_t-and-ptrdiff_t)</sup> | \n\n## Error Handling Sucks\n\nI really love Rust's error handling. [`Result`](https://doc.rust-lang.org/stable/std/result/enum.Result.html) forces you to address errors, and sum types (`enum`s) and `match` statements make doing so really easy!\n\nC's error handling story, in comparison, is straight up tragic. It seems to boil down to functions returning a \"magic integer\" like -1 or a null pointer to signal there was an error. You can get a little bit more information by reading [`errno`](https://cppreference.com/c/error/errno), a thread-local integer that can be used to check for specific kinds of errors, but in terms of getting actual error messages and stack traces it is much harder.\n\nWhen writing C, one of the biggest things you'll notice is that the language will never force you to handle errors. It's up to you to remember that functions can fail. For example, here's a snippet of code from [Null-Terminated Strings](https://bd103.dev/blog/2026-10-07-c-for-rust-programmers/#null-terminated-strings):\n\n```\n// Allocate enough room for the string and its null terminator.\nchar* reversed = malloc(len + 1);\nfor (int i = 0; i < len; i++) {\n    reversed[i] = forward[len - 1 - i];\n}\n```\nTo new programmers, it's not immediately obvious that [`malloc()`](https://cppreference.com/c/memory/malloc) can fail and return a null pointer. If your machine runs out of memory, `reversed[i]` will cause a segfault upon being accessed. In order to avoid unhelpful segfaults, the program should check for a null pointer and gracefully exit if one is found:\n\n```\nchar* reversed = malloc(len + 1);\nif (reversed == NULL) {\n    perror(\"Error\");\n    exit(1);\n}\n```\nDoing so provides a much better experience than a segfault, or worse, other unintentional behavior:\n\n```\n$ ./main\nError: Cannot allocate memory\n```\nOf course, remembering to check every single allocated pointer isn't an amazing developer experience. An approach I played around with was recreating Rust's `Result` in C using tagged unions:\n\n```\nstruct MallocResult {\n    enum Tag { OK, ERROR } tag;\n    union Value {\n        void* ptr;\n        char* error_message;\n    } value;\n};\n```\nIt's a complete mess to use, however, and there's *still* nothing stopping you from immediately accessing `result.value.ptr` without handling the error first. The lack of visibility modifiers like `private` and `public`, which could be used to prevent this, appear to be an intentional design decision. C places full trust in the programmer to *do things right™* and gives few facilities for contracts or safe abstractions.\n\nI personally disagree with this approach. I'm not some mastermind programming wizard who never makes mistakes. I'd much rather encode program requirements into the type system so that the compiler can check them for me!<sup>[\\[6\\]](https://bd103.dev/blog/2026-10-07-c-for-rust-programmers/#fn-fearless-simd)</sup> That way, I can be reasonably certain that if the code compiles it was written correctly. But I digress.\n\n## Field Access Syntax\n\nC has two different field access operators: [`.`](https://cppreference.com/c/language/operator_member_access#Member_access) for values and [`->`](https://cppreference.com/c/language/operator_member_access#Member_access_through_pointer) for pointers.\n\n```\nstruct Foo {\n    int field;\n};\nstruct Foo value = { 103 };\nstruct Foo* ptr = &value;\n// Accessing the field directly.\nprintf(\"%d\\n\", value.field);\n// Accessing the field through a pointer.\nprintf(\"%d\\n\", ptr->field);\n```\nThis caught me off guard at first, as Rust uses `.` for everything.\n\nIf your curious, [this Stack Overflow write up](https://stackoverflow.com/a/13366168) provides some interesting history on why the `->` operator exists.[\\[7\\]](https://bd103.dev/blog/2026-10-07-c-for-rust-programmers/#fn-cursed-arrow-operator)\n\n## Arrays Become Pointers for Fun\n\nArrays have some weird nuance to them. Sometimes they are plain values with their length accessible using [`sizeof()`](https://cppreference.com/c/language/sizeof), other times they are pointers with an unknown size. In general, this is determined by whether you are handling the array within the function it was defined or not.\n\nTo showcase what I mean, here's a very simple C program that prints the size of two arrays:\n\n```\nchar declared_size[3] = { 1, 2, 3 };\nchar inferred_size[] = { 4, 5, 6 };\nprintf(\"Declared size (main): %ld bytes\\n\", sizeof(declared_size));\nprintf(\"Inferred size (main): %ld bytes\\n\", sizeof(inferred_size));\n```\nWhen run, this programs tells you that both arrays take 3 bytes of memory. Good!\n\n```\n$ ./main\nDeclared size (main): 3 bytes\nInferred size (main): 3 bytes\n```\nNow, let's make a slight modification. Let's pass `declared_size` and `inferred_size` through a function before printing its size:\n\n```\nvoid function(char declared_size[3], char inferred_size[]) {\n    printf(\"Declared size (function): %ld bytes\\n\", sizeof(declared_size));\n    printf(\"Inferred size (function): %ld bytes\\n\", sizeof(inferred_size));\n}\n```\nNone of the logic changed, the arrays are identical to the previous example, however now the program reports each array takes **8 bytes of memory**:\n\n```\n$ ./main\nDeclared size (function): 8 bytes\nInferred size (function): 8 bytes\n```\nWhy? Because any array passed through a function is *implicitly converted* to a pointer to the first element. From the C compiler's perspective, the above function actually had the following type signature:\n\n`void function(char* declared_size, char* inferred_size);`\nThis is why it confusingly looked like each array was 8 bytes long. The arrays themselves are 3 bytes, but the pointers take up 8 bytes of memory! Handily, Clang raises a warning when you run `sizeof()` on the pointer form of an array, making it easier to catch this mistake:\n\n```\n$ clang main.c\n06-arrays/main.c:5:59: warning: sizeof on array function parameter will return size of 'char *'\n      instead of 'char[3]' [-Wsizeof-array-argument]\n    5 |     printf(\"Declared size (function): %ld bytes\\n\", sizeof(declared_size));\n      |                                                           ^\n06-arrays/main.c:4:20: note: declared here\n    4 | void function(char declared_size[3], char inferred_size[]) {\n      |                    ^\n06-arrays/main.c:6:59: warning: sizeof on array function parameter will return size of 'char *'\n      instead of 'char[]' [-Wsizeof-array-argument]\n    6 |     printf(\"Inferred size (function): %ld bytes\\n\", sizeof(inferred_size));\n      |                                                           ^\n06-arrays/main.c:4:43: note: declared here\n    4 | void function(char declared_size[3], char inferred_size[]) {\n      |                                           ^\n2 warnings generated.\n```\n## Oh Yeah References Don't Exist\n\nI'd be remiss if I didn't mention it once. C has no borrow checking, and no concept of references. It's all [raw pointers](https://doc.rust-lang.org/stable/std/primitive.pointer.html). This means you can do fun pointer arithmetic like so:\n\n```\nint array[] = { 0, 1, 2, 3, 4, 5 };\n// Woah, iterating over pointers instead of indexes!\nfor (int* x = &array[0]; x < &array[6]; x++) {\n    *x = 5 - *x;\n}\n```\nHowever, I'm not sure how good an idea that is. 😅\n\nEither way, be careful when you mess with pointers. Making mistakes with memory results in buffer overflows and out-of-bounds writes, which provide significant threats to the security of your code.\n\n## Conclusion\n\nI hope you enjoyed this article! I've honestly had a lot of fun learning C. And while I doubt I'll reach for it for personal projects, it's absolutely a crucial language to know as a systems programmer. All the examples in this blog are [available on Github](https://github.com/BD103/C-for-Rust-Programmers), if you'd like to mess with them yourself!\n\nUntil next time,\n\n- BD103 :)\n\n1. \nTechnically you can use [`_Bool`](https://en.cppreference.com/c/keyword/_Bool) without the header, but you still need it for the`true` and`false` definitions.[↩](https://bd103.dev/blog/2026-10-07-c-for-rust-programmers/#fr-_bool-1)\n2. \nIt looks like [Clang still has only partial support for C23](https://clang.llvm.org/c_status.html) , so you may need to wait a little while longer before chucking away your`#include <stdbool.h>` statements.[↩](https://bd103.dev/blog/2026-10-07-c-for-rust-programmers/#fr-c23-1)\n3. \nAs described previously, this is essentially what Rust does. In C this would be an ergonomic pain, but Rust's language features make it so a programmer never needs to consider the pointer and the string length as separate variables. [↩](https://bd103.dev/blog/2026-10-07-c-for-rust-programmers/#fr-extra-size-variable-1)\n4. \nThe corresponding Rust function is not idiomatic, and can only correctly handle ASCII text. If I were writing a production version of this function, it would be a single line of code: `forward.chars().rev().collect::<String>()` .[↩](https://bd103.dev/blog/2026-10-07-c-for-rust-programmers/#fr-rust-reverse-string-1)\n5. \n[`usize`](https://doc.rust-lang.org/stable/std/primitive.usize.html) and[`isize`](https://doc.rust-lang.org/stable/std/primitive.isize.html) don't perfectly map semantically to[`size_t`](https://cppreference.com/c/types/size_t) and[`ptrdiff_t`](https://cppreference.com/c/types/ptrdiff_t) . I don't fully understand the difference, however, so I recommend doing your own research before using these types.[↩](https://bd103.dev/blog/2026-10-07-c-for-rust-programmers/#fr-size_t-and-ptrdiff_t-1)[↩2](https://bd103.dev/blog/2026-10-07-c-for-rust-programmers/#fr-size_t-and-ptrdiff_t-2)\n6. \n[This blog post on Fearless SIMD](https://shnatsel.github.io/safe-simd-in-rust-even-on-the-inside/) provides a great example of using Rust's type system to guarantee the correctness of code. I highly recommend giving it a read![↩](https://bd103.dev/blog/2026-10-07-c-for-rust-programmers/#fr-fearless-simd-1)\n7. \nMy favorite line of code from that post is `100->a = 0;` , it's so cursed![↩](https://bd103.dev/blog/2026-10-07-c-for-rust-programmers/#fr-cursed-arrow-operator-1)","body_html":"<h1 id=\"c-for-rust-programmers\">C for Rust Programmers</h1>\n<p>The very first systems programming language I ever learned was Rust. This is uncommon compared to many other programmers; you&#39;re more likely to find someone who learned C or C++ first before coming to Rust. As such, there are plenty of &quot;Rust for C Programmers&quot; articles on the internet, but little to no &quot;C for Rust Programmers&quot; articles out there.</p>\n<p>Well, I&#39;m about to change that! I&#39;ve been learning C and C++ recently, and <em>holy cow those languages are quirky</em>. This blog post is a collection of eyebrow-raising details I learned while teaching myself C. (No C++ today, I&#39;m not ready to dig into that can of worms.) This is not a substitute for a proper C tutorial, you&#39;ll need to Google one of those yourself. Rather, it&#39;s a list of things to keep in mind when working in the language.</p>\n<p>With the premise set, let&#39;s draw the curtain and see what C has to offer!</p>\n<h2 id=\"boolean-is-not-built-in\">Boolean Is Not* Built-In</h2>\n<p>The original version of C did not have a primitive type for booleans, programs instead used the integers 0 and 1. This was changed in <a href=\"https://en.wikipedia.org/wiki/C99\" rel=\"nofollow ugc noopener\">C99</a>, which added booleans in the optional <a href=\"https://en.cppreference.com/c/header/stdbool\" rel=\"nofollow ugc noopener\"><code>&lt;stdbool.h&gt;</code></a> header&lt;sup&gt;<a href=\"https://bd103.dev/blog/2026-10-07-c-for-rust-programmers/#fn-_bool\" rel=\"nofollow ugc noopener\">[1]</a>&lt;/sup&gt;:</p>\n<pre><code>#include &lt;stdbool.h&gt;\nint main() {\n    bool yes = true;\n    bool no = false;\n    return 0;\n}</code></pre>\n<p>Even still, <code>true</code> and <code>false</code> are not literals or keywords like in other languages. Instead, they are definitions that expand to 1 and 0:</p>\n<pre><code>#define true 1\n#define false 0</code></pre>\n<p>This was changed again in <a href=\"https://en.wikipedia.org/wiki/C23_(C_standard_revision)\" rel=\"nofollow ugc noopener\">C23</a>&lt;sup&gt;<a href=\"https://bd103.dev/blog/2026-10-07-c-for-rust-programmers/#fn-c23\" rel=\"nofollow ugc noopener\">[2]</a>&lt;/sup&gt;, so now booleans are true language primitives, but if you&#39;re compiling for earlier versions you&#39;ll need to include <code>&lt;stdbool.h&gt;</code>.</p>\n<h2 id=\"null-terminated-strings\">Null-Terminated Strings</h2>\n<p>A Rust <a href=\"https://doc.rust-lang.org/stable/std/primitive.str.html\" rel=\"nofollow ugc noopener\"><code>&amp;str</code></a> is 16 bytes: 8 bytes for the memory address and 8 bytes for the string length. This is because <code>str</code> is a dynamically sized type, and uses <a href=\"https://bd103.dev/blog/2023-08-06-ptr-metadata/\" rel=\"nofollow ugc noopener\">pointer metadata</a> to track the length of the string.</p>\n<pre><code>// While a normal reference uses only 8 bytes...\nassert_eq!(std::mem::size_of::&lt;&amp;u8&gt;(), 8);\n// ...strings use 16 bytes.\nassert_eq!(std::mem::size_of::&lt;&amp;str&gt;(), 16);</code></pre>\n<p>This approach makes fetching the string length extremely efficient, but requires more memory per <code>&amp;str</code> reference. C uses a different approach: it doesn&#39;t store the size of its strings separately, but instead terminates every single string with a null byte (<code>\\0</code>). This is an intentional trade off that results in a few things:</p>\n<ul><li>C programs don&#39;t need to keep track of an extra <code>size</code> variable alongside the string&lt;sup&gt;<a href=\"https://bd103.dev/blog/2026-10-07-c-for-rust-programmers/#fn-extra-size-variable\" rel=\"nofollow ugc noopener\">[3]</a>&lt;/sup&gt;</li><li>Every single string needs room at the end for a null byte<ul><li>This means empty strings <code>&quot;&quot;</code> still take up one byte of memory!</li></ul></li><li>This means empty strings </li><li>Null bytes cannot easily be used in the middle of a string without messing up <a href=\"https://cppreference.com/c/header/string\" rel=\"nofollow ugc noopener\"><code>&lt;string.h&gt;</code></a> &#39;s functions</li><li>Forgetting the null terminator can result in out-of-bound reads</li></ul>\n<p>In practice, this requires you to remember to allocate extra space for the null terminator and insert it at the end of strings. For example, here is a program that reverses a string in C:</p>\n<pre><code>char* reverse(char* forward) {\n    // Calculate the string&#39;s length, excluding the null terminator.\n    unsigned long len = strlen(forward);\n    // Allocate enough room for the string and its null terminator.\n    char* reversed = malloc(len + 1);\n    for (int i = 0; i &lt; len; i++) {\n        reversed[i] = forward[len - 1 - i];\n    }\n    // Add the null terminator at the end.\n    reversed[len] = &#39;\\0&#39;;\n    return reversed;\n}</code></pre>\n<p>Note lines 5 and 12, which take special measure to account for the null terminator. For reference, the corresponding Rust function&lt;sup&gt;<a href=\"https://bd103.dev/blog/2026-10-07-c-for-rust-programmers/#fn-rust-reverse-string\" rel=\"nofollow ugc noopener\">[4]</a>&lt;/sup&gt; doesn&#39;t need to do so:</p>\n<pre><code>fn reverse(forward: &amp;[u8]) -&gt; Box&lt;[u8]&gt; {\n    let len = forward.len();\n    let mut reversed = Box::&lt;[u8]&gt;::new_uninit_slice(len);\n    for i in 0..len {\n        reversed[i].write(forward[len - 1 - i]);\n    }\n    unsafe { reversed.assume_init() }\n}</code></pre>\n<h2 id=\"target-dependent-integer-widths\">Target-Dependent Integer Widths</h2>\n<p>C&#39;s integer types are not guaranteed to use an exact number of bits, instead the width varies depending on the target platform.</p>\n<div class=\"table-wrap\"><table><thead><tr><th>Type</th><th>C Standard</th><th>64-bit Unix</th><th>64-bit Windows</th></tr></thead><tbody><tr><td><code>char</code></td><td>at least 8 bits</td><td>8 bits</td><td>8 bits</td></tr><tr><td><code>short</code></td><td>at least 16 bits</td><td>16 bits</td><td>16 bits</td></tr><tr><td><code>int</code></td><td>at least 16 bits</td><td>32 bits</td><td>32 bits</td></tr><tr><td><code>long</code></td><td>at least 32 bits</td><td>64 bits</td><td>32 bits</td></tr><tr><td><code>long long</code></td><td>at least 64 bits</td><td>64 bits</td><td>64 bits</td></tr></tbody></table></div>\n<p><a href=\"https://cppreference.com/c/language/arithmetic_types\" rel=\"nofollow ugc noopener\">via cppreference.com</a></p>\n<p><code>long</code> being 64 bits on Unix and 32 bits on Windows particularly irks me. I recommend following the advice that <a href=\"https://github.com/magicalbat\" rel=\"nofollow ugc noopener\">a friend of mine</a> gave me a few years ago: if you care about cross-platform compatibility, only use the fixed width integers provided by <a href=\"https://cppreference.com/c/header/stdint\" rel=\"nofollow ugc noopener\"><code>&lt;stdint.h&gt;</code></a>:</p>\n<div class=\"table-wrap\"><table><thead><tr><th>Rust</th><th>C</th></tr></thead><tbody><tr><td><code>u8</code></td><td><code>uint8_t</code></td></tr><tr><td><code>u16</code></td><td><code>uint16_t</code></td></tr><tr><td><code>u32</code></td><td><code>uint32_t</code></td></tr><tr><td><code>u64</code></td><td><code>uint64_t</code></td></tr><tr><td><code>usize</code></td><td><code>size_t</code>&lt;sup&gt;<a href=\"https://bd103.dev/blog/2026-10-07-c-for-rust-programmers/#fn-size_t-and-ptrdiff_t\" rel=\"nofollow ugc noopener\">[5]</a>&lt;/sup&gt;</td></tr><tr><td><code>i8</code></td><td><code>int8_t</code></td></tr><tr><td><code>i16</code></td><td><code>int16_t</code></td></tr><tr><td><code>i32</code></td><td><code>int32_t</code></td></tr><tr><td><code>i64</code></td><td><code>int64_t</code></td></tr><tr><td><code>isize</code></td><td><code>ptrdiff_t</code>&lt;sup&gt;<a href=\"https://bd103.dev/blog/2026-10-07-c-for-rust-programmers/#fn-size_t-and-ptrdiff_t\" rel=\"nofollow ugc noopener\">[5]</a>&lt;/sup&gt;</td></tr></tbody></table></div>\n<h2 id=\"error-handling-sucks\">Error Handling Sucks</h2>\n<p>I really love Rust&#39;s error handling. <a href=\"https://doc.rust-lang.org/stable/std/result/enum.Result.html\" rel=\"nofollow ugc noopener\"><code>Result</code></a> forces you to address errors, and sum types (<code>enum</code>s) and <code>match</code> statements make doing so really easy!</p>\n<p>C&#39;s error handling story, in comparison, is straight up tragic. It seems to boil down to functions returning a &quot;magic integer&quot; like -1 or a null pointer to signal there was an error. You can get a little bit more information by reading <a href=\"https://cppreference.com/c/error/errno\" rel=\"nofollow ugc noopener\"><code>errno</code></a>, a thread-local integer that can be used to check for specific kinds of errors, but in terms of getting actual error messages and stack traces it is much harder.</p>\n<p>When writing C, one of the biggest things you&#39;ll notice is that the language will never force you to handle errors. It&#39;s up to you to remember that functions can fail. For example, here&#39;s a snippet of code from <a href=\"https://bd103.dev/blog/2026-10-07-c-for-rust-programmers/#null-terminated-strings\" rel=\"nofollow ugc noopener\">Null-Terminated Strings</a>:</p>\n<pre><code>// Allocate enough room for the string and its null terminator.\nchar* reversed = malloc(len + 1);\nfor (int i = 0; i &lt; len; i++) {\n    reversed[i] = forward[len - 1 - i];\n}</code></pre>\n<p>To new programmers, it&#39;s not immediately obvious that <a href=\"https://cppreference.com/c/memory/malloc\" rel=\"nofollow ugc noopener\"><code>malloc()</code></a> can fail and return a null pointer. If your machine runs out of memory, <code>reversed[i]</code> will cause a segfault upon being accessed. In order to avoid unhelpful segfaults, the program should check for a null pointer and gracefully exit if one is found:</p>\n<pre><code>char* reversed = malloc(len + 1);\nif (reversed == NULL) {\n    perror(&quot;Error&quot;);\n    exit(1);\n}</code></pre>\n<p>Doing so provides a much better experience than a segfault, or worse, other unintentional behavior:</p>\n<pre><code>$ ./main\nError: Cannot allocate memory</code></pre>\n<p>Of course, remembering to check every single allocated pointer isn&#39;t an amazing developer experience. An approach I played around with was recreating Rust&#39;s <code>Result</code> in C using tagged unions:</p>\n<pre><code>struct MallocResult {\n    enum Tag { OK, ERROR } tag;\n    union Value {\n        void* ptr;\n        char* error_message;\n    } value;\n};</code></pre>\n<p>It&#39;s a complete mess to use, however, and there&#39;s <em>still</em> nothing stopping you from immediately accessing <code>result.value.ptr</code> without handling the error first. The lack of visibility modifiers like <code>private</code> and <code>public</code>, which could be used to prevent this, appear to be an intentional design decision. C places full trust in the programmer to <em>do things right™</em> and gives few facilities for contracts or safe abstractions.</p>\n<p>I personally disagree with this approach. I&#39;m not some mastermind programming wizard who never makes mistakes. I&#39;d much rather encode program requirements into the type system so that the compiler can check them for me!&lt;sup&gt;<a href=\"https://bd103.dev/blog/2026-10-07-c-for-rust-programmers/#fn-fearless-simd\" rel=\"nofollow ugc noopener\">[6]</a>&lt;/sup&gt; That way, I can be reasonably certain that if the code compiles it was written correctly. But I digress.</p>\n<h2 id=\"field-access-syntax\">Field Access Syntax</h2>\n<p>C has two different field access operators: <a href=\"https://cppreference.com/c/language/operator_member_access#Member_access\" rel=\"nofollow ugc noopener\"><code>.</code></a> for values and <a href=\"https://cppreference.com/c/language/operator_member_access#Member_access_through_pointer\" rel=\"nofollow ugc noopener\"><code>-&gt;</code></a> for pointers.</p>\n<pre><code>struct Foo {\n    int field;\n};\nstruct Foo value = { 103 };\nstruct Foo* ptr = &amp;value;\n// Accessing the field directly.\nprintf(&quot;%d\\n&quot;, value.field);\n// Accessing the field through a pointer.\nprintf(&quot;%d\\n&quot;, ptr-&gt;field);</code></pre>\n<p>This caught me off guard at first, as Rust uses <code>.</code> for everything.</p>\n<p>If your curious, <a href=\"https://stackoverflow.com/a/13366168\" rel=\"nofollow ugc noopener\">this Stack Overflow write up</a> provides some interesting history on why the <code>-&gt;</code> operator exists.<a href=\"https://bd103.dev/blog/2026-10-07-c-for-rust-programmers/#fn-cursed-arrow-operator\" rel=\"nofollow ugc noopener\">[7]</a></p>\n<h2 id=\"arrays-become-pointers-for-fun\">Arrays Become Pointers for Fun</h2>\n<p>Arrays have some weird nuance to them. Sometimes they are plain values with their length accessible using <a href=\"https://cppreference.com/c/language/sizeof\" rel=\"nofollow ugc noopener\"><code>sizeof()</code></a>, other times they are pointers with an unknown size. In general, this is determined by whether you are handling the array within the function it was defined or not.</p>\n<p>To showcase what I mean, here&#39;s a very simple C program that prints the size of two arrays:</p>\n<pre><code>char declared_size[3] = { 1, 2, 3 };\nchar inferred_size[] = { 4, 5, 6 };\nprintf(&quot;Declared size (main): %ld bytes\\n&quot;, sizeof(declared_size));\nprintf(&quot;Inferred size (main): %ld bytes\\n&quot;, sizeof(inferred_size));</code></pre>\n<p>When run, this programs tells you that both arrays take 3 bytes of memory. Good!</p>\n<pre><code>$ ./main\nDeclared size (main): 3 bytes\nInferred size (main): 3 bytes</code></pre>\n<p>Now, let&#39;s make a slight modification. Let&#39;s pass <code>declared_size</code> and <code>inferred_size</code> through a function before printing its size:</p>\n<pre><code>void function(char declared_size[3], char inferred_size[]) {\n    printf(&quot;Declared size (function): %ld bytes\\n&quot;, sizeof(declared_size));\n    printf(&quot;Inferred size (function): %ld bytes\\n&quot;, sizeof(inferred_size));\n}</code></pre>\n<p>None of the logic changed, the arrays are identical to the previous example, however now the program reports each array takes <strong>8 bytes of memory</strong>:</p>\n<pre><code>$ ./main\nDeclared size (function): 8 bytes\nInferred size (function): 8 bytes</code></pre>\n<p>Why? Because any array passed through a function is <em>implicitly converted</em> to a pointer to the first element. From the C compiler&#39;s perspective, the above function actually had the following type signature:</p>\n<p><code>void function(char* declared_size, char* inferred_size);</code>\nThis is why it confusingly looked like each array was 8 bytes long. The arrays themselves are 3 bytes, but the pointers take up 8 bytes of memory! Handily, Clang raises a warning when you run <code>sizeof()</code> on the pointer form of an array, making it easier to catch this mistake:</p>\n<pre><code>$ clang main.c\n06-arrays/main.c:5:59: warning: sizeof on array function parameter will return size of &#39;char *&#39;\n      instead of &#39;char[3]&#39; [-Wsizeof-array-argument]\n    5 |     printf(&quot;Declared size (function): %ld bytes\\n&quot;, sizeof(declared_size));\n      |                                                           ^\n06-arrays/main.c:4:20: note: declared here\n    4 | void function(char declared_size[3], char inferred_size[]) {\n      |                    ^\n06-arrays/main.c:6:59: warning: sizeof on array function parameter will return size of &#39;char *&#39;\n      instead of &#39;char[]&#39; [-Wsizeof-array-argument]\n    6 |     printf(&quot;Inferred size (function): %ld bytes\\n&quot;, sizeof(inferred_size));\n      |                                                           ^\n06-arrays/main.c:4:43: note: declared here\n    4 | void function(char declared_size[3], char inferred_size[]) {\n      |                                           ^\n2 warnings generated.</code></pre>\n<h2 id=\"oh-yeah-references-don-t-exist\">Oh Yeah References Don&#39;t Exist</h2>\n<p>I&#39;d be remiss if I didn&#39;t mention it once. C has no borrow checking, and no concept of references. It&#39;s all <a href=\"https://doc.rust-lang.org/stable/std/primitive.pointer.html\" rel=\"nofollow ugc noopener\">raw pointers</a>. This means you can do fun pointer arithmetic like so:</p>\n<pre><code>int array[] = { 0, 1, 2, 3, 4, 5 };\n// Woah, iterating over pointers instead of indexes!\nfor (int* x = &amp;array[0]; x &lt; &amp;array[6]; x++) {\n    *x = 5 - *x;\n}</code></pre>\n<p>However, I&#39;m not sure how good an idea that is. 😅</p>\n<p>Either way, be careful when you mess with pointers. Making mistakes with memory results in buffer overflows and out-of-bounds writes, which provide significant threats to the security of your code.</p>\n<h2 id=\"conclusion\">Conclusion</h2>\n<p>I hope you enjoyed this article! I&#39;ve honestly had a lot of fun learning C. And while I doubt I&#39;ll reach for it for personal projects, it&#39;s absolutely a crucial language to know as a systems programmer. All the examples in this blog are <a href=\"https://github.com/BD103/C-for-Rust-Programmers\" rel=\"nofollow ugc noopener\">available on Github</a>, if you&#39;d like to mess with them yourself!</p>\n<p>Until next time,</p>\n<ul><li>BD103 :)</li></ul>\n<ol><li></li></ol>\n<p>Technically you can use <a href=\"https://en.cppreference.com/c/keyword/_Bool\" rel=\"nofollow ugc noopener\"><code>_Bool</code></a> without the header, but you still need it for the<code>true</code> and<code>false</code> definitions.<a href=\"https://bd103.dev/blog/2026-10-07-c-for-rust-programmers/#fr-_bool-1\" rel=\"nofollow ugc noopener\">↩</a></p>\n<ol start=\"2\"><li></li></ol>\n<p>It looks like <a href=\"https://clang.llvm.org/c_status.html\" rel=\"nofollow ugc noopener\">Clang still has only partial support for C23</a> , so you may need to wait a little while longer before chucking away your<code>#include &lt;stdbool.h&gt;</code> statements.<a href=\"https://bd103.dev/blog/2026-10-07-c-for-rust-programmers/#fr-c23-1\" rel=\"nofollow ugc noopener\">↩</a></p>\n<ol start=\"3\"><li></li></ol>\n<p>As described previously, this is essentially what Rust does. In C this would be an ergonomic pain, but Rust&#39;s language features make it so a programmer never needs to consider the pointer and the string length as separate variables. <a href=\"https://bd103.dev/blog/2026-10-07-c-for-rust-programmers/#fr-extra-size-variable-1\" rel=\"nofollow ugc noopener\">↩</a></p>\n<ol start=\"4\"><li></li></ol>\n<p>The corresponding Rust function is not idiomatic, and can only correctly handle ASCII text. If I were writing a production version of this function, it would be a single line of code: <code>forward.chars().rev().collect::&lt;String&gt;()</code> .<a href=\"https://bd103.dev/blog/2026-10-07-c-for-rust-programmers/#fr-rust-reverse-string-1\" rel=\"nofollow ugc noopener\">↩</a></p>\n<ol start=\"5\"><li></li></ol>\n<p><a href=\"https://doc.rust-lang.org/stable/std/primitive.usize.html\" rel=\"nofollow ugc noopener\"><code>usize</code></a> and<a href=\"https://doc.rust-lang.org/stable/std/primitive.isize.html\" rel=\"nofollow ugc noopener\"><code>isize</code></a> don&#39;t perfectly map semantically to<a href=\"https://cppreference.com/c/types/size_t\" rel=\"nofollow ugc noopener\"><code>size_t</code></a> and<a href=\"https://cppreference.com/c/types/ptrdiff_t\" rel=\"nofollow ugc noopener\"><code>ptrdiff_t</code></a> . I don&#39;t fully understand the difference, however, so I recommend doing your own research before using these types.<a href=\"https://bd103.dev/blog/2026-10-07-c-for-rust-programmers/#fr-size_t-and-ptrdiff_t-1\" rel=\"nofollow ugc noopener\">↩</a><a href=\"https://bd103.dev/blog/2026-10-07-c-for-rust-programmers/#fr-size_t-and-ptrdiff_t-2\" rel=\"nofollow ugc noopener\">↩2</a></p>\n<ol start=\"6\"><li></li></ol>\n<p><a href=\"https://shnatsel.github.io/safe-simd-in-rust-even-on-the-inside/\" rel=\"nofollow ugc noopener\">This blog post on Fearless SIMD</a> provides a great example of using Rust&#39;s type system to guarantee the correctness of code. I highly recommend giving it a read<img src=\"https://bd103.dev/blog/2026-10-07-c-for-rust-programmers/#fr-fearless-simd-1\" alt=\"↩\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></p>\n<ol start=\"7\"><li></li></ol>\n<p>My favorite line of code from that post is <code>100-&gt;a = 0;</code> , it&#39;s so cursed<img src=\"https://bd103.dev/blog/2026-10-07-c-for-rust-programmers/#fr-cursed-arrow-operator-1\" alt=\"↩\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></p>","headings":[{"level":1,"text":"C for Rust Programmers","id":"c-for-rust-programmers"},{"level":2,"text":"Boolean Is Not* Built-In","id":"boolean-is-not-built-in"},{"level":2,"text":"Null-Terminated Strings","id":"null-terminated-strings"},{"level":2,"text":"Target-Dependent Integer Widths","id":"target-dependent-integer-widths"},{"level":2,"text":"Error Handling Sucks","id":"error-handling-sucks"},{"level":2,"text":"Field Access Syntax","id":"field-access-syntax"},{"level":2,"text":"Arrays Become Pointers for Fun","id":"arrays-become-pointers-for-fun"},{"level":2,"text":"Oh Yeah References Don't Exist","id":"oh-yeah-references-don-t-exist"},{"level":2,"text":"Conclusion","id":"conclusion"}]}}