Skip to content

CeeSafe for C and C++ programmers ​

CeeSafe compiles to C, calls C directly, and is designed to live inside C codebases, kernels included. What it adds is a compiler that tracks who owns each piece of memory and who is looking at it, and rejects the programs that would use it wrongly. What it removes is undefined behaviour.

At a glance ​

C / C++CeeSafeNotes
int32_t x = 1;let x: i32 = 1; (or var to change it)
T *, const T *borrow mut T, borrow T in safe code; *mut T, *T in unsafereferences are never null and never dangle
malloc / free, new / deleteBox.create(v); freed by its ownerallocation failure is a Result, not NULL or an exception
std::unique_ptr<T>Box<T>
std::vector<T>Vector<T>growing it while an element is referenced is a compile error
std::string, std::string_viewString, StringViewUTF-8 only
std::optional<T>T?
destructors, RAIIdrop { … } blocks; destruction at scope endmoves leave nothing behind to destroy twice
std::move(x)assignment moves by default; move x to end a use earlyusing a moved value is a compile error
exceptions, errno, return codesResult<T, E> and ?
assert, abortpanics: bounds, overflow, unwrapalways on
templatesgenerics with interface boundschecked once, before use; no SFINAE
#include, the preprocessorpackages and importno macros, no conditional compilation
struct layoutthe same as C's, in declaration order@repr(C) to cross the boundary
volatileVolatile<T>, p.readVolatile()
-fsanitize=address,undefinedneeded only for unsafe code; ceesafec run --interp checks it

The same program ​

cpp
#include <cstdio>
#include <vector>

int main() {
    std::vector<int> v = {1, 2, 3};
    int &first = v[0];
    v.push_back(4);          // may reallocate: `first` now dangles
    std::printf("%d\n", first);
}

In C++ this compiles and has undefined behaviour. In CeeSafe it does not compile:

ceesafe
fn main() {
    var v = Vector<i32>.create();
    v.push(1)!;
    let first = borrow v[0];
    v.push(4)!;
    Console.writeLine(first);
}
text
main.cee:5:5: error[N0241]: cannot borrow 'v' as mutable while 'v[0]' is borrowed
  |
5 |     v.push(4)!;
  |     ^
  = note: 'v[0]' is borrowed here (4:5)
  = note: the borrow is used later here (6:5)

The fixes are the ones you would reach for anyway: copy the value (let first = v[0];), or take the reference after the last push.

No undefined behaviour ​

Every operation that is undefined in C is defined in CeeSafe, by a check or by a rule:

Undefined in CIn CeeSafe
signed overflowstops the program (wrappingAdd and friends where wrapping is meant)
out-of-bounds accessstops the program
use after free, double freecompile errors
reading uninitialised memorya compile error
null dereferencethere are no null references; optionals must be checked
shift by the width or morestops the program
unsequenced modificationsevaluation order is left to right
strict aliasing violationsthe generated C never relies on them

The generated C is written so that it has no undefined behaviour itself, and is warning-free under strict flags, so the C compiler's optimiser has nothing to exploit.

Working with an existing C codebase ​

  • Call C functions from an extern "C" block, inside unsafe.
  • Export CeeSafe functions with @export; ceesafec writes ceesafe_exports.h for your C code to include.
  • Use your codebase's own structs from its headers (@header, @ctype) instead of re-declaring them: the C compiler checks every field CeeSafe uses.
  • Build a freestanding object with --profile freestanding --emit obj and link it with your build.

See Unsafe code and C and Kernel code.

Things you will miss ​

The preprocessor, inline assembly, unions (enums with payloads replace tagged unions), bit fields, pointer arithmetic in safe code, varargs, goto, and in C++, classes with inheritance and virtual functions, operator overloading, overloads, exceptions, and constexpr evaluation.

Pre-1.0: the language and its tools are still changing. CeeSafe forked from Nova 0.6.0.