Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

In C, is it ever safe to cast a variadic function pointer to a function pointer with finite arguments?

I want to create a function pointer to a function that will handle a subset of cases for a function that takes a variable parameter list. The use case is casting a function that takes ... to a function that takes a specific list of parameters, so you can deal with variable parameters without dealing with va_list and friends.

In the following example code, I'm casting a function with a variable parameters to a function with a hard-coded parameter list (and vice versa). This works (or happens to work), but I don't know if's a coincidence due to the calling convention in use. (I tried it on two different x86_64-based platforms.)

#include <stdio.h>
#include <stdarg.h>

void function1(char* s, ...)
{
    va_list ap;
    int tmp;

    va_start(ap, s);
    tmp = va_arg(ap, int);
    printf(s, tmp);
    va_end(ap);
}

void function2(char* s, int d)
{
    printf(s, d);
}

typedef void (*functionX_t)(char*, int);
typedef void (*functionY_t)(char*, ...);

int main(int argc, char* argv[])
{
    int x = 42;

    /* swap! */
    functionX_t functionX = (functionX_t) function1;
    functionY_t functionY = (functionY_t) function2;

    function1("%d\n", x);
    function2("%d\n", x);
    functionX("%d\n", x);
    functionY("%d\n", x);

    return 0;
}

Is this undefined behavior? If yes, can anyone give an example of a platform where this won't work, or a way to tweak my example in such a way that it will fail, given a more complex use case?


Edit: To address the implication that this code would break with more complex arguments, I extended my example:

#include <stdio.h>
#include <stdarg.h>

struct crazy
{
    float f;
    double lf;
    int d;
    unsigned int ua[2];
    char* s;
};

void function1(char* s, ...)
{
    va_list ap;
    struct crazy c;

    va_start(ap, s);
    c = va_arg(ap, struct crazy);
    printf(s, c.s, c.f, c.lf, c.d, c.ua[0], c.ua[1]);
    va_end(ap);
}

void function2(char* s, struct crazy c)
{
    printf(s, c.s, c.f, c.lf, c.d, c.ua[0], c.ua[1]);
}

typedef void (*functionX_t)(char*, struct crazy);
typedef void (*functionY_t)(char*, ...);

int main(int argc, char* argv[])
{
    struct crazy c = 
    {
        .f = 3.14,
        .lf = 3.1415,
        .d = -42,
        .ua = { 0, 42 },
        .s = "this is crazy"
    };


    /* swap! */
    functionX_t functionX = (functionX_t) function1;
    functionY_t functionY = (functionY_t) function2;

    function1("%s %f %lf %d %u %u\n", c);
    function2("%s %f %lf %d %u %u\n", c);
    functionX("%s %f %lf %d %u %u\n", c);
    functionY("%s %f %lf %d %u %u\n", c);

    return 0;
}

It still works. Can anyone point out a specific example of when this would fail?

$ gcc -Wall -g -o varargs -O9 varargs.c
$ ./varargs
this is crazy 3.140000 3.141500 -42 0 42
this is crazy 3.140000 3.141500 -42 0 42
this is crazy 3.140000 3.141500 -42 0 42
this is crazy 3.140000 3.141500 -42 0 42
like image 248
mpontillo Avatar asked Jul 30 '12 23:07

mpontillo


2 Answers

Yes, this is undefined behavior.

It just so happens to work because the pointers are lining up for your current compiler, platform and parameter types. Try doing this with doubles and other types and you'll probably be able to reproduce weird behavior.

Even if you don't, this is very risky code.

I am assuming you are annoyed by the varargs. Consider defining a common set of parameters in a union or struct, and then pass that.

like image 37
Mastermnd Avatar answered Oct 23 '22 23:10

Mastermnd


Casting the pointer to a different function pointer type is perfectly safe. But the only thing that the language guarantees is that you can later cast it back to the original type and obtain the original pointer value.

Calling the function through a pointer forcefully converted to incompatible function pointer type leads to undefined behavior. This applies to all incompatible function pointer types, regardless of whether they are variadic or not.

The code you posted produces undefined behavior: not at the point of the cast, but at the point of the call.


Trying to chase examples "where it would fail" is a pointless endeavor, but it should be easy anyway, since parameter passing conventions (both low-level and language-level) are vastly different. For example, the code below normally will not "work" in practice

void foo(const char *s, float f) { printf(s, f); }

int main() {
  typedef void (*T)(const char *s, ...);
  T p = (T) foo;
  float f = 0.5;
  p("%f\n", f);
}

Prints zero instead of 0.5 (GCC)

like image 200
AnT Avatar answered Oct 23 '22 22:10

AnT