Try it online: Interactive Playground - See null-safety warnings in real-time
Nullsafe C adds NULL checks to catch errors at compile-time. It is 100% compatible with existing C codebases and can be used incrementally to identify safety issues at compile-time.
This provides the following benefits:
- Catches errors at compile-time rather than runtime, reducing crashes
- Improves developer experience by shifting errors left and showing issues in the IDE as you code
- Makes code more readable and maintainable, including annotations with
_Nonnull - Adds type checking that more modern languages have (Rust, TypeScript, Kotlin)
Nullsafe C analyzes pointer nullability using flow-sensitive analysis. By default, unannotated pointers have unspecified nullability (no warnings on existing code), but you can configure different defaults to suit your needs.
Gradual Adoption: Flow-sensitive analysis is only enabled for functions that have nullability annotations or are inside a #pragma assume_nonnull region. This allows you to migrate one function at a time - add annotations to critical functions first, then expand gradually.
It provides safety in two ways:
The first is by semantic analysis: if you test a pointer with if(p), then it knows that branch contains a non-null pointer. This works for local variables, function parameters, and struct member accesses (e.g., if (node->next) narrows node->next to non-null).
The second is by using Clang's Nullability attributes, in particular _Nonnull. If a pointer is marked as _Nonnull the compiler will require a pointer it knows it not null is passed to it. This can be done either by passing a _Nonnull-annotated pointer, or by doing type narrowing.
If using a compiler other than clang, you can add #define _Nonnull as a no-op. You will not get the same compile checks as with Nullsafe C (clang fork), but the compilation will still succeed without error.
Note that this does not change any of the generated code. There are no performance impacts, it strictly does compile-time checks.
With -fflow-sensitive-nullability -fnullability-default=nullable, unannotated pointers are treated as nullable:
void unsafe(int *data) {
*data = 42; // warning: dereferencing nullable pointer of type 'int * _Nullable'
}Try it in the interactive playground
Type narrowing:
void safe(int *data) {
if (data) {
*data = 42; // OK - data is non-null here
}
}Try it in the interactive playground
Annotated with _Nonnull:
void safe_typed(int *_Nonnull data) {
*data = 42; // OK - we know data is not null so we can dereference it
}Try it in the interactive playground
Arrow operator (C++):
struct Entity {
int value() const { return 0; }
};
Entity* _Nullable getHead();
void buggy() {
Entity* head = getHead();
head->value(); // warning: dereferencing nullable pointer
}
void fixed() {
Entity* head = getHead();
if (!head) return;
head->value(); // OK - narrowed to nonnull after check
}curl -fsSL https://raw.githubusercontent.com/cs01/llvm-project/null-safe-c-dev/install.sh | bashOr download manually from releases.
On mac you may need to do the following:
brew install zstd
xcode-select --install # If not already installedBuilds not available at this time, you must clone and build locally.
Each release includes:
clang- The Null-Safe C compiler with flow-sensitive null checkingclangd- Language server for IDE integration (VSCode, vim, Neovim, Emacs, etc.)
Once installed, configure your editor to use the null-safe clangd. Install the clangd extension from llvm and set the path to the clangd binary you just downloaded.
VS Code:
// settings.json
{
"clangd.path": "/path/to/null-safe-clang/bin/clangd"
}Neovim/vim:
require('lspconfig').clangd.setup({
cmd = { '/path/to/null-safe-clang/bin/clangd' }
})This gives you real-time null-safety warnings as you type!
Note that this is not a comprehensive solution, since null pointer dereferences are just one category of memory safety bugs.
| Safety Issue | Standard C | Null-Safe Clang (null checking) |
|---|---|---|
| Null pointer dereferences | ❌ Unsafe | ✅ Fixed |
| Buffer overflows | ❌ Unsafe | ❌ Unsafe |
| Use-after-free | ❌ Unsafe | ❌ Unsafe |
| Double-free | ❌ Unsafe | ❌ Unsafe |
| Uninitialized memory | ❌ Unsafe | ❌ Unsafe |
Although this doesn't fix all memory safety issues, it catches Null pointer dereferences for free. For additional memory safety, consider a language other than C.
While Null-Safe Clang doesn't solve all memory safety issues in C, null pointer dereferences are a significant problem:
- Many memory safety bugs involve null pointer dereferences
- Easier to adopt than rewriting in Rust (100% compatible with existing C code)
- Complements other efforts (combine with
-fbounds-safetyfor buffer safety) - Incremental deployment (warnings by default, can enable per-file)
Nullability checking is controlled by two independent flags:
1. -fflow-sensitive-nullability - Enables flow-sensitive nullability analysis
- Must be enabled to get any nullability checking
- When disabled, no nullability analysis is performed
2. -fnullability-default=<mode> - Sets the default nullability for unannotated pointers
unspecified(default): No warnings for unannotated code. Flow analysis only activates inside#pragma clang assume_nonnullregions, ideal for gradual migrationnullable: Unannotated pointers are nullable, forces null checks everywhere (defensive)nonnull: Unannotated pointers are non-null, flow analysis activates everywhere (ergonomic)
# Large existing codebase - gradual migration (no warnings by default)
clang -fflow-sensitive-nullability -fnullability-default=unspecified mycode.c
# New defensive project - force null checks everywhere
clang -fflow-sensitive-nullability -fnullability-default=nullable mycode.c
# New ergonomic project - assume pointers are safe
clang -fflow-sensitive-nullability -fnullability-default=nonnull mycode.c
# With null-safe standard library headers
clang -fflow-sensitive-nullability -fnullability-default=nullable \
-I/path/to/clang/nullsafe-headers/include mycode.c
# Treat nullability issues as errors
clang -fflow-sensitive-nullability -fnullability-default=nullable \
-Werror=nullability mycode.c- Configurable defaults: Choose between
unspecified(gradual migration),nullable(defensive), ornonnull(ergonomic) - Flow-sensitive narrowing:
if (p)provespis non-null in that scope - Arrow operator checking:
p->memberwarns ifpis nullable (not just*p) - Early-exit patterns: Understands
return,goto,break,continue - Pointer arithmetic:
q = p + 1preserves narrowing fromp - Type checking through function calls, returns, and assignments
- Works with Typedefs
- Pragma support:
#pragma clang assume_nonnull begin/endfor API boundaries (function signatures only, not local variables) - Function calls preserve narrowing: Since functions receive a copy of pointer arguments, they cannot modify the original pointer variable
- Null-safe headers: Annotated C standard library in
clang/nullsafe-headers/ - IDE integration:
clangdbuilt from this fork has the same logic and warnings as clang
✅ SUPPORTED - Flow-sensitive narrowing now works for struct member accesses!
struct Node {
struct Node * _Nullable next;
};
void process(struct Node * _Nonnull node);
void example(struct Node *_Nonnull node) {
if (node->next) {
process(node->next); // ✓ OK - node->next is narrowed to nonnull
}
}How it works: The analyzer tracks struct member nullability through control flow by maintaining a separate narrowing map for (base_variable, field) pairs. When you check node->next, it narrows that specific member access.
Limitations:
- Only works when the base variable is a local variable or parameter
- Pointer aliasing isn't tracked (modifying
*pwon't be detected ifpaliases another tracked member)
This is safe because the base variable cannot change between the check and use within the same scope.
Nullability-annotated headers for string.h, stdlib.h, and stdio.h are available in clang/nullsafe-headers/. See clang/nullsafe-headers/README.md for details.
Several popular C projects have been annotated with null-safety to demonstrate gradual migration:
A null-safe version of the cJSON library is available at https://github.com/cs01/cJSON (branch cs01/nullsafe).
The migration uses #pragma clang assume_nonnull regions for API boundaries (function signatures).
Compile with null checking:
git clone -b cs01/nullsafe https://github.com/cs01/cJSON
cd cJSON
clang -fflow-sensitive-nullability -fnullability-default=unspecified \
-I/path/to/clang/nullsafe-headers/include -fsyntax-only cJSON.cNote: Uses -fnullability-default=unspecified mode because the file has #pragma clang assume_nonnull regions. The pragma only affects function parameters/returns (API boundaries), not local variables, enabling gradual adoption.
The migration demonstrates:
- File-scoped
#pragma clang assume_nonnull begin/endfor API defaults - Explicit
_Nullableannotations where pointers can be null (struct members, function pointers) - Compatibility with existing C89 code
- Zero runtime overhead
- Real bugs found: The null-safety checker identifies ~20 potential null pointer dereferences
Current limitations:
- Some errors involve struct member accesses (e.g.,
item->childwhich is_Nullable) being passed to functions expecting nonnull - Due to the struct member narrowing limitation, these require the local variable workaround
- This is representative of real-world codebases during gradual migration
Understanding the warnings:
-Wnullability-completeness: Warns about pointers without explicit nullability inside#pragma assume_nonnullregions. These should be annotated with_Nullableor_Nonnullto be explicit.-Wnullability: Actual null-safety violations (dereferencing nullable pointers, passing nullable to nonnull, etc.). These are potential bugs!
Suppress completeness warnings if needed (after reviewing them):
clang -Wno-nullability-completeness ...Additional real-world projects are being evaluated for gradual migration strategies.
See GRADUAL_MIGRATION.md for detailed migration strategies and best practices.