Search code examples
capigccplatform

C api compatible with 32 and 64 bit integers simultaneously


I have a C api to a shared (or static) library using int types and would like to upgrade to int64_t. Doing so, I want to make sure that my previous users can still upgrade the library and do not have to rewrite their whole codes.

I have come up with the solution on this gist and which replicates my code behavior (using callbacks): https://git.io/vMy8G

example.c

// User defines a different type
#define MYLONG int

#include "interface.h"
#include "stdio.h"

// User callback using his own type
int test(const int i, const my_long var, const int j) {
    printf("i   = %d\n",i);
    printf("var = %lld\n",(long long) var);
    printf("j   = %d\n",j);
    printf("%lld\n", 2LL*var-11);
    return var - 7;
}

int main() {
    // This is what user sees
    api_func functionPtr = &test;
    define_callback(functionPtr);

    // Simulate callback call
    call_callback();
    return 0;
}

interface.h

#pragma once

// MYLONG is defined differently internally and externally (before importing this file)
typedef MYLONG my_long;

// Callback definition
typedef int (*api_func)(const int, const my_long, const int); // surround by int to test alignment issues

void define_callback(api_func fptr);
void call_callback();

internal.c

#define MYLONG long long

#include "interface.h"

// Callback handling
api_func callback_ptr = 0;

void define_callback(api_func fptr) {
    callback_ptr = fptr;
}

void call_callback() {
    (*callback_ptr)(1000, 100000, 100);
    (*callback_ptr)(1000, 10000000000, 100); // will overflow when user uses int
}

The library would always be compiled with #define MYLONG int64_t while user will either use #define MYLONG int64_t or #define MYLONG int (this would be done automatically depending on some other settings). The latest define would ensure backwards compatibility.

Valgrind checks pass with all builds.

My question is the following:

  • Is it safe?
  • Am I relying on any non-guaranteed behavior of the compiler?
  • Why would it (or wouldn't it) work? (any paragraph about this in the norm?)
  • Do you see any better way of doing this?

Note that I would like to avoid writing 64 bit versions of all my functions if possible. Also, this has to work on Linux (gcc), Mac (gcc) and Windows (Visual Studio).


Solution

    1. Is it safe?

    No. User in example.c sets callback_ptr to :

    int test(const int i, const int var, const int j)
    

    ... and call_callback() calls that as:

    int (*api_func)(const int, const int64_t, const int)
    

    This is UB (not overflow) whenever int is not the same type as int64_t.

    1. Am I relying on any non-guaranteed behavior of the compiler?

    Yes.

    1. Why would it (or wouldn't it) work? (any paragraph about this in the norm?)

    It is undefined behavior (UB). Code is calling a function with one signature, yet the function may have an incompatible one.

    1. Do you see any better way of doing this?

    Yes.
    A. Certainly do not code with mis-matched-able function signatures.
    B. In the end, I think OP needs a new approach. I would recommend adding a function define_callback64() and then have the user call define_callback64() with int (*f)(const int, int64_t, const int) or define_callback() as before. call_callback() can then use the one that was set.


    Code comments

    Note that I would like to avoid writing 64 bit versions of all my functions if possible.

    OP wants "would like to upgrade to int64_t" yet "avoid writing 64 bit versions of all my functions". This is most curious and apparently contradictory.

    Perhaps re-write internal.c as int64_t and then call select call back functions.

    The 3 const in typedef int (*api_func)(const int, const my_long, const int) serve no purpose.

    OP seems to assume int is 32-bit given the title. Better to write code that does not assume this. int is at least 16-bit. OTOH, I do not see a great problem assuming int is not wider than int64_t, but even that is not defined by the C spec.

    OP also seem to also assume long long is int64_t given code #define MYLONG long long yet "library would always be compiled with #define MYLONG int64_t". OP needs to use types correctly and consistently in code and documentation.