简体   繁体   中英

C, pointer to function cast, unclear code

From this comment of Mike Ash: https://www.mikeash.com/pyblog/friday-qa-2010-01-29-method-replacement-for-fun-and-profit.html#comment-3abf26dd771b7bf2f28d04106993c07b

Here is the code:

void Tester(int ign, float x, char y) 
{ 
    printf("float: %f char: %d\n", x, y); 
} 

int main(int argc, char **argv) 
{ 
    float x = 42; 
    float y = 42; 
    Tester(0, x, y); 

    void (*TesterAlt)(int, ...) = (void *)Tester; 
    TesterAlt(0, x, y); 

    return 0; 
}

The casting he's doing in the main function is very unclear to me.

TesterAlt is a pointer to a function returning void, which is the same return type of the function Tester. He assign to this function pointer, the function Tester, but he is casting the latter return type to a pointer of type void (I'm not sure of this).

If I compile the code changing that line:

void (*TesterAlt)(int, ...) = (void)Tester;

I get a compiler error:

initializing 'void (*)(int, ...)' with an expression of incompatible type 'void'
void (*TesterAlt)(int, ...) = (void) Tester;

Why he's doing this casting? And what his syntax mean?

Edit: I've not been very clear with my original question, I don't understand this syntax and how I must read it.

(void *)Tester;

From what I know Tester is casted to "a pointer to void", but it looks that my interpretation is wrong. If it is not a pointer to void then how do you read that code and why?

You get this error message because you cannot do anything useful with an expression which has been cast to (void) . The (void *) cast in the original code refers to the pointer itself, not to the return type.

Indeed the (void *)Tester is a cast from the function pointer Tester to a void pointer. This is a pointer which just points to the given address, but has no useful information on it.

A cast to (void)Tester is a cast to a "void type" - which results in an expression which you just cannot assign to anything.

Let's return to (void *)Tester - you can use this pointer by casting it back to the proper type. But what is "proper" in this sense? Well, "proper" means that the function signatures of the original function and the pointer type used later must be identical. Violating this requirement does not lead to a compile time error, but to undefined behaviour on execution time.

One might think that a signature with has one int and then the ellipsis would cover a case with a fixed argument count, but that is not the case. There are indeed systems out there such as the AVR platform which would call a void ()(int ign, float x, char y) purely with registers, while a void ()(int, ...) would be called by pushing the arguments to the stack.

Have a look at this code:

int va(int, ...);
int a(int, int, char);

int test() {
    int (*b)(int, int, char) = va;
    int (*vb)(int, ...) = a;
    a(1, 2, 3);
    va(1, 2, 3);
    b(1, 2, 3);
    vb(1, 2, 3);
}

(note that I changed float to int ...)

On assigning b and vb , I swap the respective function prototypes. The result of this is that by referring to b , I indeed call va , but the compiler assumes a wrong function prototype. The same holds for vb and a .

Note that while on x86, this might work (I didn't check it), the AVR assembly I get from this code is like

    # a(1, 2, 3):
    ldi r24,lo8(gs(va))
    ldi r25,hi8(gs(va))
    std Y+2,r25
    std Y+1,r24
    ldi r24,lo8(gs(a))
    ldi r25,hi8(gs(a))
    std Y+4,r25
    std Y+3,r24
    ldi r20,lo8(3)
    ldi r22,lo8(2)
    ldi r23,0
    ldi r24,lo8(1)
    ldi r25,0
    rcall a

    # va(1, 2, 3):
    push __zero_reg__
    ldi r24,lo8(3)
    push r24
    push __zero_reg__
    ldi r24,lo8(2)
    push r24
    push __zero_reg__
    ldi r24,lo8(1)
    push r24
    rcall va
    pop __tmp_reg__
    pop __tmp_reg__
    pop __tmp_reg__
    pop __tmp_reg__
    pop __tmp_reg__
    pop __tmp_reg__

    # b(1, 2, 3):
    ldd r18,Y+1
    ldd r19,Y+2
    ldi r20,lo8(3)
    ldi r22,lo8(2)
    ldi r23,0
    ldi r24,lo8(1)
    ldi r25,0
    mov r30,r18
    mov r31,r19
    icall

    # vb(1, 2, 3)
    push __zero_reg__
    ldi r24,lo8(3)
    push r24
    push __zero_reg__
    ldi r24,lo8(2)
    push r24
    push __zero_reg__
    ldi r24,lo8(1)
    push r24
    ldd r24,Y+3
    ldd r25,Y+4
    mov r30,r24
    mov r31,r25
    icall
    pop __tmp_reg__
    pop __tmp_reg__
    pop __tmp_reg__
    pop __tmp_reg__
    pop __tmp_reg__
    pop __tmp_reg__

Here we see that a() , being non-vararg, gets fed via r20..r25 , while va() , being vararg, gets fed via push ing to the stack.

Concerning b() and vb() , I deliberately mixed up the definitions, ignoring the warnings I got about that. So the calls are as above, but they use the wrong calling conventions due to the mix-up. This is the reason of this being UB. While staying on x86, the code in the OP may or may not work (probably it does), but already after a switch to x64, it may start to fail and no one sees at first glance why it does. So we see once again: avoiding undefined behaviour is a strict requirement. It may work as expected, but you have no guarantees at all. Changing compiler flags may be sufficient to change the behaviour. Or porting the code to a different architecture.

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