In this model, ownership is checked statically when variables go out of scope and before assignment.
"Owner pointers" must be uninitialized or null at the end of their scope.
Basically, the nullable state needs to be tracked at compile time, and nullable pointers,despite being a separate feature, reuse the same flow analysis.
For the impatient reader, a simplified way to think about it is to compare it with C++'s unique_ptr.
The difference is that, instead of runtime code being executed at the end of the scope (a destructor), we perform a compile-time check to ensure that the owner pointer is not referring to any object. The same before assignment.
So we get the same guarantees as C++ RAII, with some extras. In C++, the user has to adopt unique_ptr and additional wrappers (for example, for FILE). In this model, it works directly with malloc, fopen, etc., and is automatically safe, without the user having to opt in to "safety" or write wrappers. Safety is the default, and the safety requirements are propagated automatically.
It is interesting to note that propagation also works very well for struct members. Having an owner pointer as a struct member requires the user to provide a correct "destructor" or free the member manually before the struct object goes out of scope.
#pragma safety enable
#include <stdio.h>
int main()
{
FILE *_Owner _Opt f = fopen("file.txt", "r");
if (f)
{
fclose(f);
}
}
At the end of the scope of f, it can be in one of two possible states: "null" or "moved" (f is moved in the fclose call).
These are the expected states for an owner pointer at the end of its scope, so no warnings are issued.
Removing _Owner _Opt we have exactly the same code as users write today. But with the same or more guarantees than C++ RAII.
When exploring the design of nullable pointers in C and comparing them with other languages like C# and TypeScript, which have constructors, I realized that C might benefit from a way to represent transient states, the state equivalent of when object is being constructed.
The C++ mutable keyword came to mind as a potential solution.
During the object creation (or destruction), the instance is considered to be in a transitional state, where the usual constraints—such as non-nullable pointers and immutability—are lifted.
Once the transitional phase is over and the object is returned, the contract that governs the object (such as immutability of name and non-nullability of pointers) is fully reinstated.
We copy paste code then we add pragma safety enable
This enables two features ownership and nullable checks. Ownership will check if the fclose is called for instance, also checks double free etc, while nullable checks will check for de-referencing null pointers.
New qualifiers _Opt and _Owner are used but they can be empty macros, allowing the same code to be compiled without cake.
A 'C' -> C compiler which preserves most source code unchanged (i.e. would be the identity transform on some input) and which implements something like constexpr on functions (by running the interpreter during the transform) could be argued to be a forward looking C implementation. Specifically C23 has constexpr, but in an extremely limited form, and aspires to extend that to be more useful later.
Equally one which replaces 'auto' with the name of the type (and similar desugaring games) is still a C to C compiler, just running as a C23 to C99 or whatever. Resolve the branch in _Generic before emitting code as part of downgrading C11.
The lifetime annotations are an interesting one because they're a different language which, if it typechecks, can be losslessly converted into C (by dropping the annotations on the way out).
I'm not sure where in that design space the current implementation lies. In particular folding preprocessed code back into code that has the #defines and #includes in is a massive pain and only really valuable if you want to lean into the round trip capability.
auto, typeof, _Generic are implemented in cake.
Sometimes when they are used inside macros the macros needs to be expanded.
Then cake has
#pragma expand MACRO. for this task.
Sample macro NEW using c23 typeof.
#include <stdlib.h>
#include <string.h>
static inline void* allocate_and_copy(void* s, size_t n) {
void* p = malloc(n);
if (p) {
memcpy(p, s, n);
}
return p;
}
#define NEW(...) (typeof(__VA_ARGS__)*) allocate_and_copy(&(__VA_ARGS__), sizeof(__VA_ARGS__))
#pragma expand NEW
struct X {
const int i;
};
int main() {
auto p = NEW((struct X) {});
}
The generated code is
#include <stdlib.h>
#include <string.h>
static inline void* allocate_and_copy(void* s, size_t n) {
void* p = malloc(n);
if (p) {
memcpy(p, s, n);
}
return p;
}
#define NEW(...) (typeof(__VA_ARGS__)*) allocate_and_copy(&(__VA_ARGS__), sizeof(__VA_ARGS__))
#pragma expand NEW
struct X {
const int i;
};
int main() {
struct X * p = (struct X*) allocate_and_copy(&((struct X) {0}), sizeof((struct X) {0}));
}
Rust needs to add some runtime checks when calling destructors in scenarios where some object may or may not be moved.
In C++ for instance, for smart pointers, the destructor will have a
"if p!= NULL". Then if the smart pointer was moved, it makes the pointer null and the destructor checks at runtime for it.
Cake implements defer as an extension, where ownership and defer work together. The flow analysis must be prepared for defer.
int * owner p = calloc(1, sizeof(int));
defer free(p);
However, with ownership checks, the code is already safe. This may also change the programmer's style, as generally, C code avoids returns in the middle of the code.
In this scenario, defer makes the code more declarative and saves some lines of code. It can be particularly useful when the compiler supports defer but not ownership.
One difference between defer and ownership checks, in terms of safety, is that the compiler will not prompt you to create the defer. But, with ownership checks, the compiler will require an owner object to hold the result of malloc, for instance. It cannot be ignored.
The same happens with C++ RAII. If you forgot to free something at our destructor or forgot to create the destructor, the compiler will not complain.
In cake ownership this cannot be ignored.
struct X {
FILE * owner file;
};
int main(){
struct X x = {};
//....
} //error x.file not freed
>Can you ask Github Co-pilot to look at C code and answer the question "What is >the length of the array 'buf' passed to this function"? That tells you how to >express the array in a language where arrays have enforced lengths, whicn >includes both C++ and Rust
this is the way you tell C what is the size of array.
You can write that in C, but it doesn't really do anything. It's equivalent to
void f(int n, int a[]) {
}
Why? So that you can write
void f(int n, int m, int a[n][m]) {
}
which declares a 2-dimensional array parameter. In that case, the "m" is used to compute the position in the array for a 2D array. The "m" doesn't do anything.
This is equivalent to writing
void f(int n, int m, int a[][m]) {
}
This is C's minimal multidimensional array support, known by few and used by fewer.
Over a decade ago, I proposed that sizes in parameters should be checkable and readable I worked out how to make it work.[1] But I didn't have time for the politics of C standards.
Do you have source on this syntax? Does the `[n]` actually do anything here? Fooling around in godbolt, `void f(int n, int a[n]) {` is the same as `void f(int n, int a[]) {` and doesn't appear to change assembly or generate any warnings/errors with improper usage.
The major difference is when the array is multi-dimensional. If you don't have VLAs then you can only set the inner dimensions at compile time, or alternatively use pointer-based work-arounds.
Even in the case of one-dimensional arrays, a compiler or a static analyzer can take advantage of the VLA size information to insert run-time checks in debug mode, or to perform compile-time checks.
"Owner pointers" must be uninitialized or null at the end of their scope.
Basically, the nullable state needs to be tracked at compile time, and nullable pointers,despite being a separate feature, reuse the same flow analysis.
For the impatient reader, a simplified way to think about it is to compare it with C++'s unique_ptr.
The difference is that, instead of runtime code being executed at the end of the scope (a destructor), we perform a compile-time check to ensure that the owner pointer is not referring to any object. The same before assignment.
So we get the same guarantees as C++ RAII, with some extras. In C++, the user has to adopt unique_ptr and additional wrappers (for example, for FILE). In this model, it works directly with malloc, fopen, etc., and is automatically safe, without the user having to opt in to "safety" or write wrappers. Safety is the default, and the safety requirements are propagated automatically.
It is interesting to note that propagation also works very well for struct members. Having an owner pointer as a struct member requires the user to provide a correct "destructor" or free the member manually before the struct object goes out of scope.
#pragma safety enable
#include <stdio.h>
int main() { FILE *_Owner _Opt f = fopen("file.txt", "r"); if (f) { fclose(f); } }
At the end of the scope of f, it can be in one of two possible states: "null" or "moved" (f is moved in the fclose call).
These are the expected states for an owner pointer at the end of its scope, so no warnings are issued.
Removing _Owner _Opt we have exactly the same code as users write today. But with the same or more guarantees than C++ RAII.