Skip to content

Instantly share code, notes, and snippets.

@M1cha
Last active March 20, 2024 08:04
Show Gist options
  • Save M1cha/a3a67e07232281c77c47d77fbabe9403 to your computer and use it in GitHub Desktop.
Save M1cha/a3a67e07232281c77c47d77fbabe9403 to your computer and use it in GitHub Desktop.

C lifetime and ownership documentation

Goals

  • Provide a guide for how and when to document lifetime and ownership in C
  • Prevent bugs caused by wrong assumptions of API users
  • Prevent having to read and understand the whole library code when the API user notices the lack of documentation.
  • Make it more likely that library developers notice a change lifetime requirements. They should check if the documentation is still correct after their change.

Non-Goals

  • Implement a linter or borrow-checker for C

What's a lifetime?

The lifetime of a pointer tells you how long you're allowed to use the data that it points to. It can be different for different scopes - even if the pointer itself is the same.

For example, a global variable inside main.c lives forever and you may declare that file to be the owner of that data. But when main.c calls a library function with a pointer to that data as an argument, that function shouldn't use it forever. How long exactly it wants to use it is what we want to document.

What's ownership?

If you own data you can use it forever. That can be because the variable is global or because you allocated data from the heap. And unless you want to keep it around forever, some piece of code needs to free it. That should be the owner.

But even without dynamic memory allocation there's an owner. The owner ultimately decides who is allowed to hold a reference to the data and has to make sure that references are handed out and used in a safe manner. If the owner wants to free the data, it has to make sure that nothing else is still holding a reference to it.

Rules

  • Undocumented lifetimes are equal to Function Scope, no ownership
  • Undocumented ownership means that the caller stays the owner.
  • If you want to use the data after the returning from the function, document how long you'll do so.
  • Document if and how the behavior changes depending on the return value of the function.
  • If you take ownership, document if you will ever free the data and how you'll do so.
  • If you don't take ownership, document which threads or interrupts will access the data and when.

Recommendations

These may not always be possible, wanted or required. Don't enforce them if you think it's a waste of time or if it would make your code worse.

  • provide a way to make the library stop using unowned data
  • provide a way to get back ownership of owned data

The reason for allowing undocumented lifetimes and ownership is to prevent putting lifetime-documentation all over the place which may distract from the code.

Examples

To simplify the samples, the documentation is unrealistically minimal. There's no brief and no parameter documentation that's unrelated to lifetimes or ownership. I also use the unusual single-line documentation /// to further decrease the number of lines inside sample-code.

Function Scope, no ownership

void fn(uint32_t *number) {
    *number = 42;
}

Implications

  • The function does not use the data after returning
  • The Caller remains the owner of the data
  • The caller may have to free the data if it was allocated

Use data beyond the function scope without changing ownership

static uint32_t *global_number;

/// @param  number Lifetime: The pointer is stored in a global variable.
///                          Consequitive calls replace the previous pointer.
///                          The data will be accessed by threads internal to this library.
void fn(uint32_t *number) {
    global_number = number;
}

Implications

  • The caller remains the owner of the data
  • The caller may have to free the data if it was allocated
  • If the caller wants to continue accessing the data, it has to make sure it is safe to do so.

Change ownerhip (conditionally)

static uint32_t *global_number;

/// @param  number Ownership: On success, this function takes ownership of the variable and will use it forever.
///                           On failure, the ownership remains unchanged.
int fn(uint32_t *number) {
    if (global_number) {
        return -1;
    }

    global_number = number;
    return 0;
}

Implications

  • If the data was allocated, the callee has to free the data, if necessary.
  • If the function succeeds, the callee must not use the data ever again
  • If the function fails, the callee remains the owner and can still use the data. It may also have to free it.

Change ownerhip (unconditionally)

static uint32_t *global_number;

/// @param  number Ownership: This function takes ownership of the variable and will use it forever.
///                           The data must be heap-allocated and this function will call `free` on it.
int fn(uint32_t *number) {
    if (global_number) {
        free(number);
        return -1;
    }

    global_number = number;
    return 0;
}

Implications

  • The callee can't use the data anymore - no matter the return value of the function
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment