2010-03-09

Const correctnes and callback interfaces in C

From time to time, I use and/or design callback interfaces in C. Usually, the signature of the callback will be similar to

typedef int (*callback)(int some_argument, void* user_data);


and the function that calls the callback is

int do_something_with_callback(int another_argument, callback f, void* user_data);


This is all well and good (as long as the documentation for do_something_with_callback specifies exactly how if will call f). But consider the following example


#include <stdio.h>
int add(int a, void* user_data)
{
int* b=(int*)user_data;
return a + (*b);
}

typedef int (*callback)(int some_argument, void* user_data);

int apply_arithmetic_function(int initial_value, callback f, void* user_data)
{
return f(initial_value, user_data);
}

int main()
{
int v=1000;
printf("%d\n", apply_arithmetic_function(123, add, &v));
return 0;
}

This works, but as you can see, the add function does not write though its user_data pointer. Therefore, in isolation, it could be marked const, which is ususally a good thing. But is int add(int a, const void* user_data) compatible with the callback interface?

At first glance, it would seem like it, since something that takes a const pointer can be passed a non-const pointer (and gcc 4.3.4 with -Wall -Wextra -Werror -std=c99 -pedantic permitts it). But then again: what if the calling convention is different for const and non-const pointers? In a plain function call, it will work, since the compiler sees the signature of the actual function that is being called, but though the function pointer, it does not.

Looking at the (draft) spec, it is not permitted.
6.7.5.3§15 says:

For two function types to be compatible, both shall specify compatible return types. [127]

Moreover, the parameter type lists, if both are present, shall agree in the number of parameters and in use of the ellipsis terminator; corresponding parameters shall have compatible types.

127) If both function types are ‘‘old style’’, parameter types are not compared.


So, all the parameters in the signatures have to be "compatible". What does that mean? 6.7.3§9 says:

For two qualified types to be compatible, both shall have the identically qualified version of a compatible type ...

Ergo, the function signatures are not compatible, and 6.5.2.2§9 says:

If the function [that is to be called] is defined with a type that is not compatible with the type (of the expression) pointed to by the expression that denotes the called function, the behavior is undefined.

Conclusion: be careful with the types of callback functions. Copy-paste verbatim from the declaration, and write a comment at the site of the callback implementations saying that they are supposed to conform to a specific interface and that the parameter declarations may not be modified unless the interface is also changed.

4 comments:

  1. The pitfalls of callback-functions are many, obscure and deep. Even in a language lika Java where this particular const-correctness does not apply there are many others. One particular favourite is the listener/event pattern where you loop a list of listeners, make a callback and the callback mutates the lsit of listeners.

    ReplyDelete
  2. Yes, that's what I was implying by "as long as the documentation for do_something_with_callback specifies exactly how if will call f". A horror story from Windows Mobile is the Radio Interface Layer API, which is entirely callback based (fair enough, it has a reason to be asynchronous in some places), but does not mention the threading model at all. Using it is almost impossible when you have to handle that your callback may be called from
    * your own thread,
    * a thread that is shared by all callbacks,
    * a global callback calling thread,
    * a thread talking to a lower layer that mustn't be delayed for any reason, or
    * some other random thread,
    before or after the call you made returns, repeatedly, simultaneously, or not called at all. Oh, and no mention of whether you can call any part of the API from within your callback. Designing the module that would call the RIL was an interesting challenge (since I wanted to write it according to the "spec", and not by testing and guessing like it appears everyone else does).

    Anyway, back to your point. Yes, maybe this can be combined with what we discussed before with object state being exposed as different interfaces. Maybe the callback should be passed a restricted version of the object that called it, so that it is easy to see what can be called at this point, and what cannot.

    ReplyDelete
  3. Yes, that is an interesting and clever thought. I wonder what a good practice would be for that, maybe the callback function should receive the calling object as a parameter. Otherwise the callback-function might hold on to an object reference that is not "valid" anymore since the object state has changed.

    When I was looking at the JSR-310 spec (the "will they finally get it right-spec" for date and times in Java) there is an example of builder methods:

    Year.of(2010).atMonth(MonthOfYear.FEBRUARY).atDay(26).atTime(12, 30).atOffset(ZoneOffset.of("+03:00"));

    This chain of method calls to build the state of an object is close to what we were talking about, although in this particular example I guess all methods return the same type.

    So I got to thinking: imagine a language where you could not hold references to objects and all state is hidden behind interfaces. You would have no direct access to "memory" - only functions you could call that returned a new interface and maybe had some side effects like I/O.

    I realized I had invented functional programing all over again. It seems to happen more frequently the older I get :-D

    ReplyDelete
  4. No, they actually do it correctly. Year.atMonth returns YearMonth, which has a different methods than Year (including, of course, a different atDay). They really do it properly, even though I suspect that it's different objects in use, and not a single object with many types.

    And you really should learn Objective CAML or Haskell. You will like it.

    ReplyDelete