This is a collection of just general rules and guidelines that I employ for writing C to make it more readable and enjoyable for specifically myself. If you want to use this as a guide, feel free to do so, but please don't take it as gospel, and feel free to modify it to your liking. I also want to note that this is not a comprehensive style guide, and there are many things that I don't cover here, but I will try to cover the most important ones.
Warning
This guide does not serve to teach best practices or how to write C, it assumes that you already know most of the best practices and idioms of C. This document serves only as a guide on how to structure and format your code and write in a custom opinionated way.
- Preferably Standard C17, but C11 is also acceptable.
- Preferably GCC or Clang. Never MSVC
This section describes some necessary flags that can be helpful for compiling C code for when you are running either a release build or development build.
-std=c17 -O2 -Wall -Wextra -Wpedantic -Wshadow -Wcast-align \
-flto -D_FORTIFY_SOURCE=2 -fstack-protector-strong -march=native
Additionally here in both release and development environments,
-Wshadowis there to make sure that local variables are not shadowing any other global or file scope variables.
-std=c17 -O1 -g -Wall -Wextra -Wpedantic -Wshadow -Wcast-align \
-fsanitize=address,undefined -fno-omit-frame-pointer
In the presence of a proper address-sanitization system, the
-fno-stack-protectorflag can be omitted, as it is not necessary to disable stack protection when using address sanitization.Additionally, the following flags can also be added for debugging purposes:
-fsanitize=address -fsanitize=undefined -fsanitize-trap=all
This is based on the requirement of the project, often times -O2 is a good choice, but using -O0 is preferred for debug builds. Strictly avoid using -O3 as it can lead to unexpected behavior and can make debugging difficult especially when linking with libraries that may not be compatible with the aggressive optimizations performed by -O3 (e.g. -fpredictive-commoning, -funroll-loops which can break certain code and also increase binary size).
This section describes some necessary flags that can be helpful for linking standard libraries and other dependencies.
- Math:
-lm - Realtime:
-lrt - OpenMP:
-fopenmp - Dynamic Loading:
-ldl
Please note that Math and Realtime libraries are not directly linked by default on Linux.
None.
None.
To improve readability, I prefer to use a common set of types, functions, and macros in a single-header library. This library provides a consistent redefined type system that matches the Coding Style guidelines outlined in this document. This allows for easier readability and maintainability of code across different projects.
/*
Copyright (c) 2026, Jiaming Meng (jackm@exoad.net)
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef SHARED_H
#define SHARED_H
#include <stdint.h>
#include <stdbool.h>
#include <stdio.h>
#include <math.h>
typedef int32_t Int32;
typedef int64_t Int64;
typedef uint32_t UInt32;
typedef uint64_t UInt64;
typedef int8_t Int8;
typedef uint8_t UInt8;
typedef char Utf8;
typedef uintptr_t UPtr;
typedef size_t Size;
typedef float Float32;
typedef double Float64;
typedef int16_t Int16;
typedef uint16_t UInt16;
typedef void Void;
typedef FILE CFile;
typedef bool Bool;
typedef Void* Any;
#define CharSeq const Utf8*
#define CharSeq16 const Utf16*
#define CharSeq32 const Utf32*
#if defined(_WIN32) || defined(_WIN64)
#if defined(build)
#define api __declspec(dllexport)
#elif defined(link)
#define api __declspec(dllimport)
#else
#define api
#endif
#else
#define api
#endif
#ifdef __cplusplus
#define linkage extern "C"
#else
#define linkage extern
#endif
#define expose linkage api
#ifdef __GNUC__
#define pure __attribute__((pure))
#define hot __attribute__((hot))
#define flatten __attribute__((flatten))
#define constfx __attribute__((const))
#define never __attribute__((noreturn))
#define deprecated __attribute__((deprecated))
#define unused __attribute__((unused))
#define packed __attribute__((packed))
#define never __attribute__((noreturn))
#else
#define pure
#define hot
#define flatten
#define constfx
#define never
#define deprecated
#define unused
#define packed
#define never
#endif
#define null NULL
#define simple static inline
#define PI 3.14159265358979323846f
#define sizeOf sizeof
#define true ((Bool)1)
#define false ((Bool)0)
#define METHOD(Type) Void Type##_
#define DEFER(label) if (true) { goto label; } else
#define print(...) fprintf(stdout, __VA_ARGS__)
#define println(...) do { print(__VA_ARGS__); print("\n"); } while(0)
simple Bool stringEquals(CharSeq a, CharSeq b)
{
if(a == b)
{
return true;
}
if(a == null || b == null)
{
return false;
}
while(*a && *b)
{
if(*a != *b)
{
return false;
}
a++;
b++;
}
return *a == *b;
}
#ifdef USE_DYNAMIC_LIBRARY_COMPAT_LAYER
#ifdef _WIN32
#include <windows.h>
#define RTLD_LAZY 0
typedef HMODULE DLHandle;
static Int8 dlerrorBuffer[256];
CharSeq dlerror()
{
return dlerrorBuffer;
}
Any dlopen(CharSeq name, Int32 flags) {
(Void) flags;
HMODULE h = LoadLibraryA(name);
if(!h)
{
DWORD err = GetLastError();
sprintf(dlerrorBuffer, "LoadLibrary failed for %s: error %lu", name, err);
}
else
{
dlerrorBuffer[0] = '\0';
}
return h;
}
Any dlsym(Any handle, CharSeq symbol) {
Any p = GetProcAddress((HMODULE)handle, symbol);
if(!p)
{
DWORD err = GetLastError();
sprintf(dlerrorBuffer, "GetProcAddress failed for %s: error %lu", symbol, err);
}
else
{
dlerrorBuffer[0] = '\0';
}
return p;
}
Int32 dlclose(Any handle) {
if(FreeLibrary((HMODULE) handle))
{
dlerrorBuffer[0] = '\0';
return 0;
}
else
{
DWORD err = GetLastError();
sprintf(dlerrorBuffer, "FreeLibrary failed: error %lu", err);
return -1;
}
}
#else
#include <dlfcn.h>
typedef Any DLHandle;
#endif
#endif
#endifGoing forward, all documentation will utilize symbols defined in this header for consistency
There is a new
<stdnoreturn.h>or through the usage of_Noreturnkeyword, thenevermacro modifier will be changed in the future to reflect this.
Definition of
Int8forcharis an unstable and risky implementation and requires another change if you are using this for ARM rather than on x86.By default, the above
shared.his meant for x86 machines where the basechartype is a signed type. On the other hand, on ARM machines, thechartype is often unsigned. Additionally, there is a plan to migrate to the following types:typedef int8_t Int8; // Guaranteed signed 8-bit typedef uint8_t UInt8; // Guaranteed unsigned 8-bit typedef char Utf8; // Specifically for text/strings #define CharSeq const Utf8*
In general, camelCase is used for variables, functions, and named values, while PascalCase is used for types. While SCREAMING_SNAKE_CASE is used for macros and constants/enum members.
If you need to namespace something, prefer using just a simple prefix following the style of that entity. For example, if you want to namespace a function named foo with your project name bar you would use camelCase to name it barFoo.
Prefer camelCase.
Int32 main(Void)
{
Int32 myVariable = 0; // OK
Int32 my_variable = 0; // BAD
Int32 MyVariable = 0; // BAD
}This rule also applies to function parameters.
Prefer camelCase.
static Int32 myGlobalVariable = 0; // OK
static Int32 my_global_variable = 0; // BAD
static Int32 MyGlobalVariable = 0; // BAD
Int32 myGlobalVariable2 = 0; // OK
Int32 my_global_variable2 = 0; // BAD
Int32 MyGlobalVariable2 = 0; // BADAdditionally, do not use Hungarian Notation (i.e. using
g_prefix to mark global variables)
Prefer camelCase.
extern Int32 myExternVariable; // OK
extern Int32 my_extern_variable; // BAD
extern Int32 MyExternVariable; // BADAdditionally, do not use Hungarian Notation (i.e. using
g_prefix to mark global variables)
Free functions are just normal functions that are not associated with any type (i.e. not trying to emulate methods in OOP/part of an object system).
Prefer camelCase.
Int32 myFunction(Int32 arg) // OK
{
}
Int32 my_function(Int32 arg) // BAD
{
}
Int32 MyFunction(Int32 arg) // BAD
{
}This rule applies to all identifiers for type names of:
- Structs
- Unions
- Enum (This rule does not apply to enum members)
- Typedefs
- Macro (Prefer
typedefwhen possible)
Prefer PascalCase.
typedef struct MyStruct // OK
{
Int32 member;
} MyStruct;
typedef struct my_struct // BAD
{
Int32 member;
} my_struct;Prefer SCREAMING_SNAKE_CASE. Additionally, the name of the enum member should be prefixed with the name of the enum type where you take the camelCase version of the enum type name and convert it to SCREAMING_SNAKE_CASE and use that as a prefix for all enum members. This is to avoid name collisions between enum members and also to make it easier to read and understand the code.
typedef enum OperatingSystem
{
OPERATING_SYSTEM_WINDOWS, // See here that the 'OPERATING_SYSTEM_' prefix is used for all enum members to align with the enum type name 'OperatingSystem'
OPERATING_SYSTEM_LINUX,
OPERATING_SYSTEM_MACOS,
...
} OperatingSystem;Prefer SCREAMING_SNAKE_CASE.
#define MAX(a, b) ((a) > (b) ? (a) : (b)) // OK
#define max(a, b) ((a) > (b) ? (a) : (b)) // BAD
#define Max(a, b) ((a) > (b) ? (a) : (b)) // BAD
#define find_max(a, b) ((a) > (b) ? (a) : (b)) // BADPrefer PascalCase.
#define MyType Int32 // OK
#define my_type Int32 // BADIf it is possible, prefer to use
typedefinstead of#definefor type-like macros to have better type safety and better debugging.
Prefer PascalCase
typedef Int32 (*Callback)(Int32 arg); // OK
typedef Int32 (*cb)(Int32 arg); // BAD - Although 'Cb' is fine, but it is not readableThis section describes how to structure your code in terms of indentation, braces placement, and other formatting rules.
There is no exact rule, prefer to use whatever your editor supports.
Do not use identation for nested macros. As moving them around is very confusing as they are not directly related to the C language itself.
#ifdef SOME_FLAG
#include "somefile.h"
#define OK 1
#else
#include "anotherfile.h"
#endif // OK
#ifdef SOME_FLAG
# include "somefile.h"
# define OK 1
#else
# include "anotherfile.h"
#endif // BAD - this can make long nested macros seem like 'include', 'define' are normal C keywords or some other macros.
#ifdef SOME_FLAG
#include "somefile.h"
#define OK 1
#else
#include "anotherfile.h"
#endif // BADPrefer the Allman style for braces placement. This means that the opening brace is placed on a new line, aligned with the control statement or function declaration, and the closing brace is also placed on a new line, aligned with the opening brace.
Int32 main(Void)
{
if(condition)
{
// code
}
else
{
// code
}
}In the case where lets say the body is empty, there should be no spacing between the braces and they must be on separate lines.
Int32 main(Void)
{
} // OK
Int32 main(Void)
{} // BADThe same goes for structs, place the opening brace on a new line and the closing brace on a new line.
typedef struct MyStruct
{
Int32 member;
} MyStruct;Note that the closing brace is on the same line as the type name.
Enums should follow the same style as structs.
Spacing within expressions should be maximized around binary operators while unary operators and parentheses do not require this.
Int32 someNumber = a + ~c * (ab / -c); // OK
Int32 anotherNumber=a+~c*(ab/c); // BADWhen using compound literals or casted r‑value initializers, always include a space between the closing parenthesis of the cast and the opening brace of the literal. This improves readability and visually separates the cast from the initializer body.
SomeType x = (SomeType) { ... }; // OK
SomeType x = (SomeType){ ... }; // BAD
SomeType x = (SomeType){...}; // BAD- All control structures should not have a space between the opening keyword and the opening parenthesis
if(...) // OK
if (...) // BAD
for(...) // OK
for (...) // BAD
else if(...) // OK
else if (...) // BAD- In a
do-whileloop, thewhilemust rest on the same line as the closing brace.
Avoid single line multi-declarations as in Int8 a, b, c;. Use a new line for each declaration to maximize clarity even if they are the same type.
The placement of the * can be confusing as it is not related to the type in C and is instead related to the variable name. This means
it is confusing that: Int32* a, b, c; is not the same as Int32 *a, *b, *c;. In order to mitigate this, refer to the fact that multiple variables should
not be declared in a single-line fashion.
Prefer to specify the * on the identifier itself:
Int32* a; // GOOD
Int32 *b; // NOT PREFERREDWith the aforementioned declaration style of variables and pointers, the placement of const is also trivial:
- Pointer to Constant:
const <TypeName>* <variableName> - Constant Pointer:
<TypeName>* const <variableName>
Nothing changed, follow all other rules for naming and formatting.
- Avoid VLAs and prefer fixed-size arrays or dynamic allocation
Int32 array[10]; // OK
Int32 array[] = (Int32[]) { 1, 3, 4 }; //OK
Int32 array[n]; // BADPrefer to declare structs in the following format:
typedef struct <StructTag>
{
<MemberType> <memberName>;
...
} <TypeName>;Here <StructTag> is not optional and it must match the <TypeName>. This is to allow for forward declarations and also to make it easier to read and understand the code.
Note: It is not acceptable to use
structas a tagged-type directly without a typedef, always usetypedeffor structs to avoid having to writestructkeyword everywhere and also to allow for a more consistent style.
Prefer to declare enums in the following format:
typedef enum <EnumTag>
{
<ENUM_MEMBER_1>,
<ENUM_MEMBER_2>,
...
} <TypeName>;Here <EnumTag> is not optional and it must match the <TypeName>. This is to allow for forward declarations and also to make it easier to read and understand the code.
Note: It is not acceptable to use
enumas a tagged-type directly without a typedef, always usetypedeffor enums to avoid having to writeenumkeyword everywhere and also to allow for a more consistent style.
- Preference to use member style initialization is optional, but strongly preferred to for structs that have more than
$2$ members. - Prefer to always specify the type of the rvalue compound literal (i.e.
MyStruct x = (MyStruct) { ... };). This allows for long embedded structs to be easily readable.
- Within a header file, defining functions should not require the
externkeyword. - Whenever possible, unless to create an
stbstyle library, separate implementation into.cfiles. - For simple definitions that preferably should live in the header, prefer to use the
simplemodifier macro. - For a function definition with 2 or less parameters, it is not necessary to specify their names.
- For a function definition with no parameters (e.g.
main), prefer to specify withVoid(i.e.main(Void)) to be explicit. - Always declare a return type. Some older versions of C will default to
int/Int32as the base type. If it returns nothing useVoidand if it never returns, prependnever.
There are some builtin values such as null, false, and true which are macros that wrap their respective standard C equivalent.
Prefer to use these over NULL, 0/FALSE, 1/TRUE respectively.
- Always parenthesize macro parameters and the entire macro body:
#define MAX(a,b) ((a) > (b) ? (a) : (b))
C allows for treating things like ((Void*)0) as simply just false which means if(someVar) and if(someVar != null) will produce the same meaning. This is confusing at times, even with C not having native nullability.
Always compare pointers directly if they are null:
Int32* myVar = ...;
if(myVar != null) // OK
{
...
}
if(myVar) // BAD
{
...
}Although generally frowned upon, learn to manage it where necessary in order to avoid forgetting memory cleanup (i.e. simulating the defer keyword in languages like Go).
The macro DEFER(label) simulates RAII/defer cleanup bahavior using goto internally.
Void* data = malloc(n);
if(data == null)
{
return;
}
DEFER(cleanup);
// work …
cleanup:
free(data);
data = null;- Standard & System libraries first using
<>if preferable - First party headers (that the project uses) using
"" - Project headers using
"" - Miscenallous Non-C headers using
""
Prefer to use old-fashioned include guards over #pragma once even if supported:
#ifdef MY_FILE_H
#define MY_FILE_H
//...
#endifHere the naming of the macro MY_FILE_H should follow that you can append a prefix for your project or not followed by the actual file name in all caps and then the file extension (preferably .h).
OOP is fun and there is a decent way to emulate it with C through pure composition.
Why not polymorphism?
C does not provide native polymorphism. While it is technically possible to emulate polymorphic behavior using struct embedding and function pointers, this pattern is verbose, fragile, and visually unclean. It should be avoided unless absolutely necessary (e.g., implementing a framework or plugin system).
typedef struct Shape { Void (*draw)(struct Shape* self); // using struct tag here is very ugly and voids a stylistic rule. } Shape;UB is foul here, for example to initialize the function pointer for a simple struct can lead to UB:
typedef struct Shape { Void (*draw)(struct Shape* self); } Shape; typedef struct Circle { Shape base; Float32 radius; } Circle; Void circleDraw(Shape* s) { Circle* self = (Circle*) s; // assumes s is a circle, even with proper naming (described below), it is not checked by the compiler self->radius += 1.0f; // UB if 's' is not a Circle } Int32 main(Void) { Shape s = { .draw = circleDraw }; s.draw(&s); // UB }
- All methods/object functions must be named using
camelCaseand be prefixed with the struct's type name followed by inPascalCasethe name of the method.
typedef struct Vec2 { ... } Vec2;
Void vec2Add(Vec2* a, Vec* b, Vec* sum); // OK
Void addVec2(Vec2* a, Vec* b, Vec* sum); // BADThis is to allow for fast lookup within something like intellisense
- All methods/object functions must accept a pointer as the first parameter. You can name this parameter
self, but it is not mandatory.
typedef struct Vec2 { ... } Vec2;
Void vec2ToString(Vec2*, Int8*); // OK
Void vec2ToString(Int8*, Vec2*); // BADThis is to make sure that all of the calls are consistent to signify that the first element is the 'self' reference.
Although there is no direct getter and setter pattern, you can enforce a promise to the programmer by applying const to the first reference parameter.
Float32 vec2Magnitude(const Vec2* self); // promising that this method is readonly
Void vec2Scale(Vec2* self, Float32 scalar); // says that this method will modify the referenceConstructors and destructors must follow naming schemes that allow them to be easily distinguished:
- All constructor functions must be prefixed with
newfollowed by thePascalCasestruct type name. They should preferably return a pointer to the newly allocated object.
typedef struct Vec2 { ... } Vec2;
Vec2* newVec2(Float32, Float32); // OK
Vec2* createVec2(Float32, Float32); // BAD
Vec2* vec2(Float32, Float32); // BAD - Ill adivsed as there could be a macro that does the same thing for something else- All destructor functions must be prefixed with
deletefollowed by thePascalCasestruct type name. They should preferably returnVoid.
typedef struct Vec2 { ... } Vec2;
Void deleteVec2(Vec2*); // OK
Void freeVec2(Vec2*); // BAD - Ill advised.Delete was chosen as it sounds better than free and looks similar to C++.
Make sure to also employ defensive measures against dangling pointers after calling the destructor by setting the original pointer to
null. (i.e. immediately after deleting, callptr = null;)
All memory allocation done by constructors must be handled by the caller respectively with the appropriate destructor call.
Updated: February 17, 2026
:)