HackerTrans
トップ新着トレンドコメント過去質問紹介求人

thradams

no profile record

投稿

Ownership model and nullable pointers for C

cakecc.org
3 ポイント·投稿者 thradams·7 か月前·1 コメント

[untitled]

1 ポイント·投稿者 thradams·2 年前·0 コメント

[untitled]

1 ポイント·投稿者 thradams·2 年前·0 コメント

[untitled]

1 ポイント·投稿者 thradams·2 年前·0 コメント

Fixing C source code from the internet using cake static analyzer

youtube.com
12 ポイント·投稿者 thradams·2 年前·2 コメント

Static Ownership Checks for C

thradams.com
2 ポイント·投稿者 thradams·3 年前·1 コメント

Experimental static ownership checks for C

thradams.com
4 ポイント·投稿者 thradams·3 年前·1 コメント

コメント

thradams
·7 か月前·議論
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.
thradams
·2 年前·議論
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.
thradams
·2 年前·議論
A two-minute video explaining the concepts of nullable pointers and pointer ownership in Cake, using a simple example.
thradams
·2 年前·議論
Cake is a open source compiler and static analyzer in development. (Not production quality yet.)

This video shows how cake can help programmers to create safe code just fixing warnings.

https://youtu.be/X5tmkF16UMQ

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.
thradams
·2 年前·議論
I think at "Keep the language small and simple" it should say avoid "two ways of doing something"

( The sample I have is 0, NULL and nullptr where nullptr is something new. Two ways of doing something makes the language complex. )
thradams
·2 年前·議論
Cake disabled checks in a few linked list functions like pop_front pop_back.
thradams
·2 年前·議論
Cake is not porting Rust semantics. It works on classical C code, like the first sample using fopen.

    #include <ownership.h>
    #include <stdio.h>

    int main()
    {
      FILE *owner f = fopen("file.txt", "r"); 
      if (f)
        fclose(f);
    }
But comparisons are inevitable, and I also think there are lessons learned in Rust.

C programmers uses contracts, these contracts are part of documentation of some API. For instance, if you call fopen you must call fclose.

All we need is to create contracts that the compiler can read and verify automatically.
thradams
·2 年前·議論
(question about rust.. this is not implemented in cake yet)

Let's say I have to objects on the heap. A and B. A have a "view" to B.

Then we put a prompt for the user. (or dynamic condition) "Which object do you want to delete first A or B?" Then user select B. How this can be checked at compile time?
thradams
·2 年前·議論
The ownership checks are new in Cake, less than one year. So the answer is no, just cake is using. And there is a lot of work to do..in flow analysis etc.

The cake source itself is moving to another experimental feature that is nullable types.

Similar of C# https://learn.microsoft.com/en-us/dotnet/csharp/nullable-ref...
thradams
·2 年前·議論
(I think preprocessor is the place where memory is used and released all the time while expanding macros.)
thradams
·2 年前·議論
This "dynamic drop semantics" does not exist in cake.

    int * owner p = malloc(sizeof(int));
    if (condition) 
       free(p);
    free(p); // error p may be initialized/moved.
to fix

    int * owner p = malloc(sizeof(int));
    if (condition) 
    { 
      free(p);
      p = 0;
    }
    free(p);
thradams
·2 年前·議論
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}));     
    }
thradams
·2 年前·議論
The idea is to keep cake aligned with C, not a language fork. But Cake itself could have a fork to Cake++. :D
thradams
·2 年前·議論
(by the way, embed is not working on web version because of include directory bug - it is an open issue and regression)
thradams
·2 年前·議論
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.
thradams
·2 年前·議論
One issue I see with this approach (compiler leaking memory) is, for instance, if the requirements change and you need to utilize the compiler as a lib or service. For example, if the Cake source is used within a web browser compiled with Emscripten, leaking memory with each compilation would lead to a continuous increase in memory usage.

Additionally, compilers often offer the option to compile multiple files. Therefore, we cannot afford to leak memory with each file compilation.

Initially I was planning a global allocator for cake source. It had a lot of memory leaks that would be solved in the future.

When ownership checks were added it was a perfect candidate for fixing leaks. (actually I also had this in mind)
thradams
·2 年前·議論
In cake there is no temporal hole, we cannot reuse the deleted object. This prevents double free and use after free.

    int main() {   
       struct X * owner p = malloc(sizeof(struct X));
        
       p->text = malloc(10);

       free(p->text); //object text destroyed

       //p->text is on uninitialized state. 
       //cannot be used (except assignment)

       struct X x2 = {0};

       *p = x2; //x2 MOVED TO *p

       x_delete(p);
thradams
·2 年前·議論
Thanks for the rust sample. It looks very similar. Can the allocator be customized?

As I said I am not Rust specialist.

Also, in my understanding is that in Rust, sometimes a dynamic state is created when the object may or may not be moved.

In cake ownership this needs to me explicit ( and the destructor is not generated)

I also had a look at Rust in lifetime annotations. This concept may be necessary but I am avoiding it.

Consider this sample.

   struct X {  
     struct Y * pY;  
   };  
   struct Y {  
     char * owner name;  
   };  
An object Y pointed by pY must live longer than object X. (Cake is not checking this scenario yet)

Also (classic Rust sample)

    int * max(int * p1, int * p2) {  
      return *p1 > *p2 ? p1 : p2;
    }

    int main(){  
       int * p = NULL;
       int a  = 1;
      {
         int b = 2;
         p = max(&a,  &b);
      }
      printf("%d", *p);
    }
This is not implemented yet but I want to make the lifetime of p be the smallest scope. (this is to avoid lifetime annotations)

   int * p = NULL;
   int a  = 1;
   {
      int b = 2;
      p = max(&a,  &b);
   } //p cannot be used beyond this point*
thradams
·2 年前·議論
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
thradams
·2 年前·議論
>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.

    void f(int n, int a[n]) {
    }