45

I have a C library (with C headers) which exists in two different versions.

One of them has a function that looks like this:

int test(char * a, char * b, char * c, bool d, int e);

And the other version looks like this:

int test(char * a, char * b, char * c, bool d)

(for which e is not given as function parameter but it's hard-coded in the function itself).

The library or its headers do not define / include any way to check for the library version so I can't just use an #if or #ifdef to check for a version number.

Is there any way I can write a C program that can be compiled with both versions of this library, depending on which one is installed when the program is compiled? That way contributors that want to compile my program are free to use either version of the library and the tool would be able to be compiled with either.

So, to clarify, I'm looking for something like this (or similar):

#if HAS_ARGUMENT_COUNT(test, 5)
    test("a", "b", "c", true, 20);
#elif HAS_ARGUMENT_COUNT(test, 4)
    test("a", "b", "c", true);
#else
    #error "wrong argument count"
#endif

Is there any way to do that in C? I was unable to figure out a way.

The library would be libogc ( https://github.com/devkitPro/libogc ) which changed its definition of if_config a while ago, and I'd like to make my program work with both the old and the new version. I was unable to find any version identifier in the library. At the moment I'm using a modified version of GCC 8.3.

Solomon Ucko
  • 5,724
  • 3
  • 24
  • 45
Florian Bach
  • 919
  • 7
  • 22
  • I don't understand how that would help me? I would still need to figure out somehow if I need to call test with 4 or 5 arguments from inside the macro, right? – Florian Bach Nov 18 '20 at 07:38
  • How large is that C library? Dozen of functions, or thousands of them? A thousand of C source lines, or a million of them? What is your C compiler? **Please [edit](https://stackoverflow.com/posts/64888608/edit) your question to improve it.** – Basile Starynkevitch Nov 18 '20 at 09:45
  • 1
    OP: Would be able to share what C library is that exactly? – KamilCuk Nov 18 '20 at 09:50
  • 7
    I would expect that anyone providing different API versions of a library would also define some `VERSION` macro that can be used to detect the proper API. That's a really weird library... – Gerhardh Nov 18 '20 at 10:07
  • 2
    Added https://github.com/devkitPro/libogc/issues/102 – Basile Starynkevitch Nov 18 '20 at 10:21
  • Thanks for opening the bug report, but that's also only something that might help if they change around more functions in future versions, not particularly for this instance. – Florian Bach Nov 18 '20 at 10:24
  • 5
    And comments like the one that was just posted on the bug report were one of the reasons why I asked for a workaround on SO instead of asking for a proper solution in their bugtracker ... would love to comment on that bug but the repo owner blocked me for asking a similar question a while ago. – Florian Bach Nov 18 '20 at 10:28
  • **DON'T** don't omit the proper prototype, don't call with 5 arguments (even when using the library where the function takes 4 arguments). I repeat: **DON'T DO THIS** – pmg Nov 18 '20 at 17:12

8 Answers8

32

This should be done at the configure stage, using an Autoconf (or CMake, or whatever) test step -- basically, attempting to compile a small program which uses the five-parameter signature, and seeing if it compiles successfully -- to determine which version of the library is in use. That can be used to set a preprocessor macro which you can use in an #if block in your code.

Sneftel
  • 40,271
  • 12
  • 71
  • 104
  • 5
    Although the learning curve is quite steep. Can you reference a guide for the perplexed? – Mark Morgan Lloyd Nov 18 '20 at 21:39
  • See [the Autoconf manual](https://www.gnu.org/savannah-checkouts/gnu/autoconf/manual/autoconf-2.69/html_node/index.html), and in particular [AC_COMPILE_IFELSE](https://www.gnu.org/savannah-checkouts/gnu/autoconf/manual/autoconf-2.69/html_node/Running-the-Compiler.html#Running-the-Compiler). – Sneftel Nov 19 '20 at 10:51
  • 4
    ...which I'd hardly call a simple introduction for somebody whose never come across it before. – Mark Morgan Lloyd Nov 19 '20 at 11:58
  • This should be easy to do with a simple shell script even without going full Autoconf. Just run the check and generate a one-line header file. As long as you don't have too many such things you need to configure at compile time anyway... – ilkkachu Nov 19 '20 at 12:26
  • @MarkMorganLloyd If you know of a good tutorial, I'd be happy for a suggestion; otherwise, your google is as good as mine. I learned from an out-of-print O'Reilly book. – Sneftel Nov 19 '20 at 13:42
  • 11
    This answer would be improved by a snippet of example code. – James_pic Nov 19 '20 at 14:04
  • 2
    @Sneftel I'm afraid that I'm as perplexed as you are :-) This is one of the situations where we might know the "correct" answer but not necessarily how to exploit it from scratch. I've made some trivial edits to existing autoconf etc. files, but that's really as far as I've gone. – Mark Morgan Lloyd Nov 19 '20 at 14:19
  • Via Wp's "Autotools" article, PDF at https://www.lrde.epita.fr/~adl/autotools.html is an OKish summary (use the "Handout" version). Section 10 deals with writing custom macros, which is where OP would probably have to go if the library he wants to use isn't supported yet. – Mark Morgan Lloyd Nov 19 '20 at 14:42
25

I think there's no way to do this at the preprocesing stage (at least not without some external scripts). On the other hand, there is a way to detect a function's signature at compiling time if you're using C11: _Generic. But remember: you can't use this in a macro like #if because primary expressions aren't evaluated at the preprocessing stage, so you can't dynamically choose to call the function with signature 1 or 2 in that stage.

#define WEIRD_LIB_FUNC_TYPE(T) _Generic(&(T), \
    int (*)(char *, char *, char *, bool, int): 1, \
    int (*)(char *, char *, char *, bool): 2, \
    default: 0)

printf("test's signature: %d\n", WEIRD_LIB_FUNC_TYPE(test));
// will print 1 if 'test' expects the extra argument, or 2 otherwise

I'm sorry if this does not answer your question. If you really can't detect the version from the "stock" library header file, there are workarounds where you can #ifdef something that's only present in a specific version of that library.

This is just a horrible library design.

Update: after reading the comments, I should clarify for future readers that it isn't possible in the preprocessing stage but it is possible at compile time still. You'd just have to conditionally cast the function call based on my snippet above.

typedef int (*TYPE_A)(char *, char *, char *, bool, int);
typedef int (*TYPE_B)(char *, char *, char *, bool);

int newtest(char *a, char *b, char *c, bool d, int e) {
    void (*func)(void) = (void (*)(void))&test;
    if (_Generic(&test, TYPE_A: 1, TYPE_B: 2, default: 0) == 1) {
        return ((TYPE_A)func)(a, b, c, d, e);
    }
    return ((TYPE_B)func)(a, b, c, d);
}

This indeed works although it might be controversial to cast a function this way. The upside is, as @pizzapants184 said, the condition will be optimized away because the _Generic call will be evaluated at compile-time.

Toribio
  • 3,963
  • 3
  • 34
  • 48
  • 1
    You can decide which version to call, just not with `#if`. Just use `_Generic` to select the expression to use. – user2357112 Nov 18 '20 at 18:34
  • You can save the value returned by the `_Generic` and use it in a normal `if`. Note that you may need to cast the function to the appropriate type to avoid "calling function with wrong number of parameters" errors in the branch not taken. Any optimizing compiler will see that the `_Generic`'s value is a compile-time constant and will optimize away the not-taken branch. E.g.: https://godbolt.org/z/T16jhK – pizzapants184 Nov 19 '20 at 01:59
  • 1
    Officially, function pointers can't be cast to `void*`, but they can be cast to each other, so try `((TYPE_A)&test)` and `((TYPE_B)&test)` and skip the `void*` step. – user253751 Nov 19 '20 at 14:33
11

I don't see any way to do that with standard C, if you are compiling with gcc a very very ugly way can be using gcc aux-info in a command and passing the number of parameters with -D:

#!/bin/sh

gcc -aux-info output.info demo.c
COUNT=`grep "extern int foo" output.info | tr -dc "," | wc -m`
rm output.info
gcc -o demo demo.c -DCOUNT="$COUNT + 1"
./demo

This snippet

#include <stdio.h>

int foo(int a, int b, int c);

#ifndef COUNT
#define COUNT 0
#endif

int main(void)
{
    printf("foo has %d parameters\n", COUNT);
    return 0;
}

outputs

foo has 3 parameters
David Ranieri
  • 39,972
  • 7
  • 52
  • 94
5

Attempting to support compiling code with multiple versions of a static library serves no useful purpose. Update your code to use the latest release and stop making life more difficult than it needs to be.

WinterMute
  • 336
  • 3
  • 5
  • 1
    That's definitely true for this library specifically - I see you also commented on https://github.com/devkitPro/libogc/issues/102 – Peter Cordes Nov 18 '20 at 21:27
  • 8
    That might be true for this library specifically, but it's certainly not true for libraries in general. As a library developer myself, I hate old versions as much as you do, and wish they'd just vanish from the earth. But pragmatically, there are good reasons for professional developers to stick to older versions of libraries, particularly in the late stages of a development cycle. The devil you know... – Sneftel Nov 19 '20 at 10:53
5

In Dennis Ritchie's original C language, a function could be passed any number of arguments, regardless of the number of parameters it expected, provided that the function didn't access any parameters beyond those that were passed to it. Even on platforms whose normal calling convention wouldn't be able to accommodate this flexibility, C compilers would generally used a different calling convention that could support it unless functions were marked with qualifiers like pascal to indicate that they should use the ordinary calling convention.

Thus, something like the following would have had fully defined behavior in Ritchie's original C language:

int addTwoOrThree(count, x, y, z)
  int count, x, y, z;
{
  if (count == 3)
    return x+y+z;
  else
    return x+y;
}
int test()
{
  return count(2, 10,20) + count(3, 1,2,3);
}

Because there are some platforms where it would be impractical to support such flexibility by default, the C Standard does not require that compilers meaningfully process any calls to functions which have more or fewer arguments than expected, except that functions which have been declared with a ... parameter will "expect" any number of arguments that is at least as large as the number of actual specified parameters. It is thus rare for code to be written that would exploit the flexibility that was present in Ritchie's language. Nonetheless, many implementations will still accept code written to support that pattern if the function being called is in a separate compilation unit from the callers, and it is declared but not prototyped within the compilation units that call it.

supercat
  • 77,689
  • 9
  • 166
  • 211
3

you don't.

the tools you're working with are statically linked and don't support versioning. you can get around it using all kind of tricks and tips that have been mentioned, but at the end of the day they are ugly patch works of something you're trying to do that makes no sense in this context(toolkit/code environment).

you design your code for the version of the toolkit you have installed. its a hard requirement. i also don't understand why you would want to design your gamecube/wii code to allow building on different versions. the toolkit is constantly changing to fix bugs, assumptions etc etc. if you want your code to use an old version that potentially have bugs or do things wrong, that is on you.

i think you should realize what kind of botch work you're dealing with here if you need or want to do this with an constantly evolving toolkit..

I also think, but this is because i know you and your relationship with DevKitPro, i assume you ask this because you have an older version installed and your CI builds won't work because they use a newer version (from docker). its either this, or you have multiple versions installed on your machine for a different project you build (but won't update source for some odd reason).

DacoTaco
  • 57
  • 2
  • My CI builds are working just fine because I'm not relying on existing Docker containers. And yes, I have multiple versions of devkitPro and libogc installed on my machine, exactly because of issues like this. Because it is *impossible* to write software to work with multiple versions. I'm building lots of different software from different developers and it's not my job to keep them all updated. – Florian Bach Nov 18 '20 at 12:36
  • 2
    It's not your job to compile lots of different software from different developers either yet here we are. You've been asked multiple times to stop attempting to provide and support ancient, deprecated versions of our tools an libraries. Please don't do this. You're creating more problems than you're solving. – WinterMute Nov 18 '20 at 17:03
  • i agree with WinterMute here. It is not your job to build those old applications. even if it was,don't you think its about time to update and fix the code to work with modern and fixed code/libraries? do you think it took me a long time to update priiloader with every version of libogc? no. on contrary, i usually don't have to do anything. did it took a long time before i could build the old GeckoOs code on modern libogc? no, 30min max ( + code fixes ). this is why i said 'botch work'. just fix it. What is the worst that could happen? bad code not working anymore and requiring a refactor? – DacoTaco Nov 18 '20 at 17:51
2

If your compiler is a recent GCC, e.g. some GCC 10 in November 2020, you might write your own GCC plugin to check the signature in your header files (and emit appropriate and related C preprocessor #define-s and/or #ifdef, à la GNU autoconf). Your plugin could (for example) fill some sqlite database and you would later generate some #include-d header file.

You then would set up your build automation (e.g. your Makefile) to use that GCC plugin and the data it has computed when needed.

For a single function, such an approach is overkill.

For some large project, it could make sense, in particular if you also decide to also code some project-specific coding rules validator in your GCC plugin.

Writing a GCC plugin could take weeks of your time, and you may need to patch your plugin source code when you would switch to a future GCC 11.

See also this draft report and the European CHARIOT and DECODER projects (funding the work described in that report).

BTW, you might ask the authors of that library to add some versioning metadata. Inspiration might come from libonion or Glib or libgccjit.

BTW, as rightly commented in this issue, you should not use an unmaintained old version of some opensource library. Use the one that is worked on.

I'd like to make my program work with both the old and the new version.

Why?

making your program work with the old (unmaintained) version of libogc is adding burden to both you and them. I don't understand why you would depend upon some old unmaintained library, if you can avoid doing that.


PS. You could of course write a plugin for GCC 8. I do recommend switching to GCC 10: it did improve.

Basile Starynkevitch
  • 223,805
  • 18
  • 296
  • 547
  • "I don't understand why..." I guess you have never had to debug what happens when somebody uses one version of a header file in their code, and links against a different version of the library. (And the location of both the header and the library may have been chosen automagically by a build procedure created by someone who was neither the programmer nor the debugger!) – alephzero Nov 18 '20 at 20:53
  • @alephzero: The OP is asking for automated compile-time detection based on the header they included, not the library they link against. This doesn't solve the ABI-mismatch problem you're talking about. In fact, making your program work with both versions might mean avoiding some new function only available in the new version, which could have saved you from this by causing a linker error either at build time or at dynamic-link time, because the old library version wouldn't provide that symbol. – Peter Cordes Nov 18 '20 at 21:21
0

I'm not sure this solves your specific problem, or helps you at all, but here's a preprocessor contraption, due to Laurent Deniau, that counts the number of arguments passed to a function at compile time.

Meaning, something like args_count(a,b,c) evaluates (at compile time) to the constant literal constant 3, and something like args_count(__VA_ARGS__) (within a variadic macro) evaluates (at compile time) to the number of arguments passed to the macro.

This allows you, for instance, to call variadic functions without specifying the number of arguments, because the preprocessor does it for you.

So, if you have a variadic function

void function_backend(int N, ...){
  // do stuff
}

where you (typically) HAVE to pass the number of arguments N, you can automate that process by writing a "frontend" variadic macro

#define function_frontend(...) function_backend(args_count(__VA_ARGS__), __VA_ARGS__)

And now you call function_frontend() with as many arguments as you want:

I made you Youtube tutorial about this.

#include <stdint.h>
#include <stdarg.h>
#include <stdio.h>

#define m_args_idim__get_arg100(                                                                    \
  arg00,arg01,arg02,arg03,arg04,arg05,arg06,arg07,arg08,arg09,arg0a,arg0b,arg0c,arg0d,arg0e,arg0f,  \
  arg10,arg11,arg12,arg13,arg14,arg15,arg16,arg17,arg18,arg19,arg1a,arg1b,arg1c,arg1d,arg1e,arg1f,  \
  arg20,arg21,arg22,arg23,arg24,arg25,arg26,arg27,arg28,arg29,arg2a,arg2b,arg2c,arg2d,arg2e,arg2f,  \
  arg30,arg31,arg32,arg33,arg34,arg35,arg36,arg37,arg38,arg39,arg3a,arg3b,arg3c,arg3d,arg3e,arg3f,  \
  arg40,arg41,arg42,arg43,arg44,arg45,arg46,arg47,arg48,arg49,arg4a,arg4b,arg4c,arg4d,arg4e,arg4f,  \
  arg50,arg51,arg52,arg53,arg54,arg55,arg56,arg57,arg58,arg59,arg5a,arg5b,arg5c,arg5d,arg5e,arg5f,  \
  arg60,arg61,arg62,arg63,arg64,arg65,arg66,arg67,arg68,arg69,arg6a,arg6b,arg6c,arg6d,arg6e,arg6f,  \
  arg70,arg71,arg72,arg73,arg74,arg75,arg76,arg77,arg78,arg79,arg7a,arg7b,arg7c,arg7d,arg7e,arg7f,  \
  arg80,arg81,arg82,arg83,arg84,arg85,arg86,arg87,arg88,arg89,arg8a,arg8b,arg8c,arg8d,arg8e,arg8f,  \
  arg90,arg91,arg92,arg93,arg94,arg95,arg96,arg97,arg98,arg99,arg9a,arg9b,arg9c,arg9d,arg9e,arg9f,  \
  arga0,arga1,arga2,arga3,arga4,arga5,arga6,arga7,arga8,arga9,argaa,argab,argac,argad,argae,argaf,  \
  argb0,argb1,argb2,argb3,argb4,argb5,argb6,argb7,argb8,argb9,argba,argbb,argbc,argbd,argbe,argbf,  \
  argc0,argc1,argc2,argc3,argc4,argc5,argc6,argc7,argc8,argc9,argca,argcb,argcc,argcd,argce,argcf,  \
  argd0,argd1,argd2,argd3,argd4,argd5,argd6,argd7,argd8,argd9,argda,argdb,argdc,argdd,argde,argdf,  \
  arge0,arge1,arge2,arge3,arge4,arge5,arge6,arge7,arge8,arge9,argea,argeb,argec,arged,argee,argef,  \
  argf0,argf1,argf2,argf3,argf4,argf5,argf6,argf7,argf8,argf9,argfa,argfb,argfc,argfd,argfe,argff,  \
  arg100, ...)  arg100
#define m_args_idim(...)  m_args_idim__get_arg100(, ##__VA_ARGS__,                  \
  0xff,0xfe,0xfd,0xfc,0xfb,0xfa,0xf9,0xf8,0xf7,0xf6,0xf5,0xf4,0xf3,0xf2,0xf1,0xf0,  \
  0xef,0xee,0xed,0xec,0xeb,0xea,0xe9,0xe8,0xe7,0xe6,0xe5,0xe4,0xe3,0xe2,0xe1,0xe0,  \
  0xdf,0xde,0xdd,0xdc,0xdb,0xda,0xd9,0xd8,0xd7,0xd6,0xd5,0xd4,0xd3,0xd2,0xd1,0xd0,  \
  0xcf,0xce,0xcd,0xcc,0xcb,0xca,0xc9,0xc8,0xc7,0xc6,0xc5,0xc4,0xc3,0xc2,0xc1,0xc0,  \
  0xbf,0xbe,0xbd,0xbc,0xbb,0xba,0xb9,0xb8,0xb7,0xb6,0xb5,0xb4,0xb3,0xb2,0xb1,0xb0,  \
  0xaf,0xae,0xad,0xac,0xab,0xaa,0xa9,0xa8,0xa7,0xa6,0xa5,0xa4,0xa3,0xa2,0xa1,0xa0,  \
  0x9f,0x9e,0x9d,0x9c,0x9b,0x9a,0x99,0x98,0x97,0x96,0x95,0x94,0x93,0x92,0x91,0x90,  \
  0x8f,0x8e,0x8d,0x8c,0x8b,0x8a,0x89,0x88,0x87,0x86,0x85,0x84,0x83,0x82,0x81,0x80,  \
  0x7f,0x7e,0x7d,0x7c,0x7b,0x7a,0x79,0x78,0x77,0x76,0x75,0x74,0x73,0x72,0x71,0x70,  \
  0x6f,0x6e,0x6d,0x6c,0x6b,0x6a,0x69,0x68,0x67,0x66,0x65,0x64,0x63,0x62,0x61,0x60,  \
  0x5f,0x5e,0x5d,0x5c,0x5b,0x5a,0x59,0x58,0x57,0x56,0x55,0x54,0x53,0x52,0x51,0x50,  \
  0x4f,0x4e,0x4d,0x4c,0x4b,0x4a,0x49,0x48,0x47,0x46,0x45,0x44,0x43,0x42,0x41,0x40,  \
  0x3f,0x3e,0x3d,0x3c,0x3b,0x3a,0x39,0x38,0x37,0x36,0x35,0x34,0x33,0x32,0x31,0x30,  \
  0x2f,0x2e,0x2d,0x2c,0x2b,0x2a,0x29,0x28,0x27,0x26,0x25,0x24,0x23,0x22,0x21,0x20,  \
  0x1f,0x1e,0x1d,0x1c,0x1b,0x1a,0x19,0x18,0x17,0x16,0x15,0x14,0x13,0x12,0x11,0x10,  \
  0x0f,0x0e,0x0d,0x0c,0x0b,0x0a,0x09,0x08,0x07,0x06,0x05,0x04,0x03,0x02,0x01,0x00,  \
)

typedef struct{
  int32_t x0,x1;
}ivec2;
int32_t max0__ivec2(int32_t nelems, ...){  // The largest component 0 in a list of 2D integer vectors
  int32_t max = ~(1ll<<31) + 1;  // Assuming two's complement
  va_list args;
  va_start(args, nelems);
  for(int i=0; i<nelems; ++i){
    ivec2 a = va_arg(args, ivec2);
    max = max > a.x0 ? max : a.x0;
  }
  va_end(args);
  return max;
}
#define max0_ivec2(...)  max0__ivec2(m_args_idim(__VA_ARGS__), __VA_ARGS__)

int main(){
  int32_t max = max0_ivec2(((ivec2){0,1}), ((ivec2){2,3}, ((ivec2){4,5}), ((ivec2){6,7})));
  printf("%d\n", max);
}
étale-cohomology
  • 2,098
  • 2
  • 28
  • 33