Why cleanup attribute of GCC/Clang can not be used with function parameter
Asked Answered
F

1

6

Despite the cleanup attribute is an extension that supported by GCC/Clang only, I think it s the nearest approximation to RAII in pure C. e.g.

#define loc_str __attribute__((cleanup(free_loc_str)))

void free_loc_str(char **str)
{ if(str && *str) free(*str); }

int main(void)
{
    loc_str char *s = malloc(10);
    return 0;  // Great! s is freed when it exit its scope   
}

Though, the attribute works only with auto scope but not function parameter. i.e.

void func(loc_str char *str)
{
    return; // XXX - str will not be freed (compiled without any warning) 
}

I already know above situation, but, why? Is there any reason to create such restriction?

-- Update --

A full story that trigger this question:

I tried to create a shared pointer(or smart pointer) for C. Following is a non-thread safe and simplified snippet

struct impl_t;
struct impl_t* ctor();
void dtor(struct impl_t* inst);

struct shared_ptr_s
{
    struct impl_t* inst;
    int *use_cnt;
};

void free_shared(struct shared_ptr_s* ptr)
{
    if(!ptr) return;
    if(0 == --(*ptr->use_cnt)) {
        dtor(ptr->inst);
        free(ptr->use_cnt);
    }
    ptr->inst = 0;
    ptr->use_cnt = 0;
}

#define shared_ptr struct shared_ptr_s __attribute__((cleanup(free_shared)))

void func(shared_ptr sp)
{
    // shared_ptr loc_sp = sp;  // works but make no sense
    return; // sp will not be freed since cleanup function is not triggered
}

int main(void)
{
    shared_ptr sp = { 
        .inst = ctor(), 
        .use_cnt = malloc(sizeof(int)) 
    };
    ++*sp.use_cnt; // please bear this simplification.

    {    
        ++*sp.use_cnt;
        shared_ptr sp2 = sp;
    } // sp.inst is still there since use_cnt > 0 

    ++*sp.use_cnt;
    func(sp); // leak!

    return 0;
}

That's why I wish the cleanup attribute can work with function parameter - eliminate manually free as much as possible.

Florous answered 27/1, 2014 at 14:41 Comment(3)
The return type of main should be int (I think you already know that because you have a return 0; at the end)Abutment
loc_str char *s = malloc(10); func(s); return 0; Double-free bug.Levitate
@Abutment Tanks for your correction.Florous
A
7

Presumably because, in case of function parameters, the thinking is that it's the caller's, and not the callee's, responsibility to manage memory for the arguments. If you do need the callee to free the argument upon exit, the workaround is simple enough, just make a local copy of the argument that is adorned by the cleanup attribute.

void func(char *str)
{
    loc_str char *str1 = str;
    return; 
} // now str1 will be free when func exits

Of course, in this case, do not use the cleanup attribute on the argument passed to func() by the caller, or you'll have a double free on your hands.


For your use case, I'd suggest creating a macro that increments the use count and declares a local variable of type shared_ptr.

#define SHARED_PTR_GET_ADD_REF(sp_in, sp_name) ++(*sp_in.use_cnt); \
                                               shared_ptr sp_name = sp_in; 

Use that macro everywhere you need to increment use count. So your example, with debug statements, would look like this:

#include <stdlib.h>
#include <stdio.h>

struct shared_ptr_s
{
//    struct impl_t* inst;
    int *use_cnt;
};

typedef struct shared_ptr_s shared_ptr_t;           // unadorned type
#define shared_ptr struct shared_ptr_s __attribute__((cleanup(free_shared)))

#define SHARED_PTR_GET_ADD_REF(sp_in, sp_name) ++(*sp_in.use_cnt); \
                                               printf("add use_cnt = %d\n", *sp_in.use_cnt); \
                                               shared_ptr sp_name = sp_in; 

void free_shared(struct shared_ptr_s* ptr)
{
    if(!ptr) return;
    printf("del use_cnt = %d\n", *ptr->use_cnt - 1); 
    if(0 == --(*ptr->use_cnt)) {
//        dtor(ptr->inst);
        printf("freeing %p\n", (void *)ptr->use_cnt);
        free(ptr->use_cnt);
    }
//    ptr->inst = 0;
    ptr->use_cnt = 0;
}

void func(shared_ptr_t sp)
{
    SHARED_PTR_GET_ADD_REF(sp, sp_loc);
    return;
}

int main(void)
{
    shared_ptr_t sp = {         // original type does not use __attribute__(cleanup)
//        .inst = ctor(), 
        .use_cnt = malloc(sizeof(int)) 
    };
    SHARED_PTR_GET_ADD_REF(sp, sp_loc);

    func(sp_loc);

    return 0;
}

Live demo

Abutment answered 27/1, 2014 at 15:14 Comment(3)
Your answer works good, but I'm still confused about why the restriction is there - if a function declare cleanup attribute within its parameters, such info should be enough for caller to keep in mind the callee will try to cleanup parameters. Nevertheless the shared pointer is explicitly managed.Florous
@Florous Unfortunately, the gcc docs don't offer any rationale why you cannot mark a function parameter as __attribute__(cleanup(...)). As I was guessing earlier, they might've thought it would be easy to run into double free() issues had that been supported, especially when using a #define like you are for that parameter's type.Abutment
Yes, they stated the restriction without further explanation. Maybe I should raise this question on their mail-lists. Thank you!Florous

© 2022 - 2024 — McMap. All rights reserved.