简体   繁体   中英

C: Passing array to pointer function

I'm not sure if the question has asked before, but I couldn't find any similar topics.

I'm struggeling with the following piece of code. The idea is to extend r any time later on without writing lots of if-else statements. The functions ( func1 , func2 ...) either take zero or one arguments.

void func1() {
    puts("func1");
}

void func2(char *arg){
    puts("func2");
    printf("with arg %s\n", arg);
}

struct fcall {
    char name[16];
    void (*pfunc)();
};

int main() {
    const struct fcall r[] = {
        {"F1", func1},
        {"F2", func2}
    };
    char param[] = "someval";

    size_t nfunc = RSIZE(r); /* array size */
    for(;nfunc-->0;) {
        r[nfunc].pfunc(param);
    }
    return 0;
}

The code above assumes that all functions take the string argument, which is not the case. The prototype for the pointer function is declared without any datatype to prevent the incompatible pointer type warning.

Passing arguments to functions that do not take any parameters usually results in too few arguments . But in this case the compiler doesn't 'see' this ahead, which also let me to believe that no optimization is done to exclude these unused addresses from being pushed onto the stack. (I haven't looked at the actual assemble code).

It feels wrong someway and that's usually a recipe for buffer overflows or undefined behaviour. Would it be better to call functions without parameters separately? If so, how much damage could this do?

The way to do it is typedef a function with 1 argument, so the compiler could verify if you pass the correct number of arguments and that you do not pass something absolutely incompatible (eg a struct by value). And when you initialize your array, use this typedef to cast function types.

void func1(void) { ... }

void func2(char *arg) { ... }

void func3(int arg) { ... }

typedef uintptr_t param_t;    
typedef void (*func_t)(param_t);

struct fcall {
    char name[16];
    func_t pfunc;
};

const struct fcall r[] = {
    {"F1", (func_t) func1},
    {"F2", (func_t) func2}
    {"F3", (func_t) func3}
};

...

r[0].pfunc((param_t) "foo");
r[1].pfunc((param_t) "bar");
r[2].pfunc((param_t) 1000);

Here param_t is defined as uintpr_t . This is an integer type big enough to store a pointer value. For details see here: What is uintptr_t data type . The caveat is that the calling conventions for param_t should be compatible with the function arguments you use. This is normally true for all integer and pointer types. The following sample is going to work, all the type conversions are compatible with each other in terms of calling conventions:

// No problem here.
void ptr_func(struct my_struct *ptr) {
    ...
}
...
struct my_struct struct_x;
((func_t) &ptr_func)((param_t) &struct_x);

But if you are going to pass a float or double argument, then it might not work as expected.

// There might be a problem here. Depending on the calling
// conventions the value might contain a complete garbage,
// as it might be taken from a floating point register that
// was not set on the call site.
void float_func(float value) {
    ...
}
...
float x = 1.0;
((func_t) &float_func)((param_t) x);

In this case you might need to define a function like this:

// Problem fixed, but only partially. Instead of garbage
// there might be rounding error after the conversions.
void float_func(param_t param) {
    float value = (float) param;
    ...
}
...
float x = 1.234;
((func_t) &float_func)((param_t) x);

The float is first being converted to an integer type and then back. As a result the value might be rounded. An obvious solution would be to take an address of x and pass it to modified a function float_func2(float *value_ptr) . The function would dereference its pointer argument and get the actual float value.

But, of course, being hardcore C-programmers we do not want to be obvious, so we are going to resort to some ugly trickery.

// Problem fixed the true C-programmer way.
void float_func(param_t param) {
    float value = *((float *) &param);
    ...
}
...
float x = 1.234;
((func_t) &float_func)(*((param_t *) &x));

The difference of this sample compared to passing a pointer to float, is that on the architecture (like x86-64) where parameters are passed on registers rather than on the stack, a smart enough compiler can make float_func do its job using registers only, without the need to load the parameter from the memory.

One option is for all the functions accept a char * argument, and your calling code to always pass one. The functions that don't need an argument need not use the argument they receive.

To be clean (and avoid undefined behaviour), if you must have some functions that accept no argument and some functions that accept an argument, use two lists and register/call each type of function separately.

If the behaviour is undefined there's no telling how much damage could be caused.

It might blow up the planet. Or it might not.

So just don't do it, OK?

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM