4

In some library code, there's a pattern of setting up callbacks for events, where the callback may receive an argument. The callback itself may not do anything with the argument, or the argument might be 0, but it is passed. Decomposing it down to the basics, it looks like the following:

#include <stdio.h>
#include <string.h>

void callback_1(char *data) {
  printf("Length of data: %d\n", strlen(data));
}

void callback_2() {
  printf("No parameters used\n");
}

typedef void (*Callback)(char *);

int main(void) {
    Callback callback;

    callback = callback_1;
    callback("test");

    callback = callback_2;
    callback("test");

    return 0;
}

This compiles and runs on GCC 4.9.3 (32-bit) without anything unexpected.

A good callback function has a signature like callback_1, but occasionally I forget the data parameter if it's not being used. While I'm aware that C isn't always typesafe (especially with regards to void pointers), I expected a warning, since if I had provided a mismatched parameter, e.g. int data, I would receive a warning about incompatible types. If the typedef for Callback didn't accept a parameter, I would receive a compilation error if a callback function had one in the signature.

Is there a way in C to get a warning for the case where a function pointer is assigned to a function where the signature is missing an argument? What happens on the stack if the callback is missing the parameter? Are there possible repercussions of missing the parameter in the callback?

superlou
  • 679
  • 1
  • 8
  • 21
  • 2
    `void callback_2()` isn't a function taking zero arguments. `void callback_2(void)` is. – milleniumbug May 11 '16 at 20:24
  • Yep, immediately after the comments mentioned `()` vs. `(void)` it clicked. Now that I know the root cause, it's easier to find similar stackoverflow posts. This may be a duplicate of [this one](http://stackoverflow.com/questions/693788/is-it-better-to-use-c-void-arguments-void-foovoid-or-not-void-foo). Since `()` is deprecated in C99, I'm going to see if there's a flag I can set to get the warning. – superlou May 12 '16 at 17:20

2 Answers2

3

This is because you have callback2 defined as:

void callback_2()

The empty parenthesis means it takes an unspecified number of arguments. So it qualifies to be assigned to type Callback.

If you change the definition to this:

void callback_2(void)

This explicitly specifies that the function takes 0 arguments, and you'll get an "assignment from incompatible pointer type" warning.

In order to properly catch this condition, compile with -Wstrict-prototypes along with -Wall -Wextra and you'll get the following if declare or define a function with an empty argument list:

warning: function declaration isn’t a prototype
dbush
  • 205,898
  • 23
  • 218
  • 273
  • Right. `void callback_2()` *declares* that it takes an unspecified number of arguments. `void callback_2() { /* ... */ }` *defines* a function that has no parameters; calling it with one or more parameters has undefined behavior. That's why prototypes (`void callback_2(void)`) were added to the language, to allow such errors to be caught by the compiler. – Keith Thompson May 11 '16 at 20:48
  • It seems like this specific case can be warned by using -Wstrict-prototypes on GCC C99. Is that appropriate for this question? – superlou May 12 '16 at 17:43
  • @superlou Yes, that's correct. I've edited my answer with this info. – dbush May 12 '16 at 18:06
1

Your code not only has undefined behavior it also contains a deprecated feature.

6.7.5.3/14

An identifier list declares only the identifiers of the parameters of the function. An empty list in a function declarator that is part of a definition of that function specifies that the function has no parameters. The empty list in a function declarator that is not part of a definition of that function specifies that no information about the number or types of the parameters is supplied.

Notice the difference. void f(); is a declarator without a definition. void f() {} is a declarator with a definition.

6.5.2.2/2:

If the expression that denotes the called function has a type that includes a prototype, the number of arguments shall agree with the number of parameters. Each argument shall have a type such that its value may be assigned to an object with the unqualified version of the type of its corresponding parameter.

6.11.6

The use of function declarators with empty parentheses (not prototype-format parameter type declarators) is an obsolescent feature.

It's true that void f() and void f(void) are compatible types, but since f() defines a function that takes no parameters, calling it with parameters is undefined behavior.


OK enough pedantry, so what actually happens? There is no name mangling in C, so the linker only sees the name of the function. GCC and Clang both emit code that call the functions in the exactly the same way. They push the pointer to the function onto the stack, the argument (the "test" string) then they call it. Nothing fishy really happens here. Here's what I get with objdump:

00000000004006b3 <callback_2>:
  ...

I only included this to show the address of callback_2. The address in this example for callback_1 is 400686.

First some general stack stuff:

  4006c4:   55                      push   rbp
  4006c5:   48 89 e5                mov    rbp,rsp
  4006c8:   48 83 ec 10             sub    rsp,0x10

We store the address of callback_one:

  4006cc:   48 c7 45 f8 86 06 40    mov    QWORD PTR [rbp-0x8],0x400686
  4006d3:   00
  4006d4:   48 8b 45 f8             mov    rax,QWORD PTR [rbp-0x8]

Then our "test" string. Using objdump -s -j .rodata shows that the address of our string is at index 7, address 4007b0.

 4007b0 73207573 65640074 65737400           s used.test. 
                                             1234567

4007b0 + 7 is 4007b7.

  4006d8:   bf b7 07 40 00          mov    edi,0x4007b7

Call callback_one:

  4006dd:   ff d0                   call   rax

Repeat for callback_two:

  4006df:   48 c7 45 f8 b3 06 40    mov    QWORD PTR [rbp-0x8],0x4006b3
  4006e6:   00 
  4006e7:   48 8b 45 f8             mov    rax,QWORD PTR [rbp-0x8]
  4006eb:   bf b7 07 40 00          mov    edi,0x4007b7
  4006f0:   ff d0                   call   rax

So why didn't you get any warning? Well, technically you're not violating any rules of the language. And undefined behavior is not required to be diagnosable. The moral of the story is: if you think code needs a warning, don't write it. But C is a tricky language. So long as you know the language, the caveats and what exactly your compiler is doing you should be fine.

user6322488
  • 116
  • 2