2

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?

2
  • Just never use variable argument functions or macros: they are 100% superfluous features, which are also both slow and dangerous. Commented Feb 2, 2015 at 14:14
  • @Lundin IBTD. It is a little bit trick feature and has some pitfalls, but I find it quite useful. However, you have to know what you do when you use them. Commented Feb 3, 2015 at 8:32

1 Answer 1

6

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 pushing 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.

Sign up to request clarification or add additional context in comments.

6 Comments

This is indeed not standard C, but listed as a "common extension" to the C standard in J.5.7: "A pointer to a function may be cast to a pointer to an object or to void, allowing a function to be inspected or modified"
@Lubdin This is right, and it may also be used for inspection and stuff, but for calling it we must use a function pointer with the proper type. Failing to do so leaves us with code which puts the arguments to the wrong places (ot the stack instead of registers or vice versa) --> UB.
@Gopi, yes, it will. See my updated question where I show what differences a given platform may make between the two cases.
@glglgl thank you very much for your deep explanation, but I still don't understand that syntax, see my edit. Also, what's the meaning of UB?
@AR89 UB = undefined behaviour, a very usual expression when it comes to programming in C. My point is that what internally happens may be different, thus it is important to use the correct function signature.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.