2

In C++ code, I would like to be able to include two headers for two different versions of a shared C library that I load at runtime (with dlopen/dlsym on linux, GetProcAddress on windows).

For one execution, I load only one shared library (.so on linux, .dll on windows), the chosen version is determined by a parameter given to my program on the command line.

For each version of the C library, I hesitate between including one header for function declarations or the other one for function pointers types declaration (or both).

Headers for functions declarations are of this form :

#ifdef __cplusplus
extern "C" 
{
#endif

extern int func(int argInt);

#ifdef __cplusplus
}
#endif

Let's call the 2 versions of it my_header_old.h and my_header_new.h.

Headers for functions pointers types declarations are of this form :

typedef int (*func)(int argInt)

Let's call the 2 versions of it my_header_ptr_types_old.h and my_header_ptr_types_new.h.

This second form seems mandatory since I need to cast the result of dlsym/GetProcAddress which is of type void* to functions pointers types.

My first question is :

Is it mandatory to include the header for functions declarations in my case or can I use only the header for functions pointers types declarations ?

Since declarations in headers are very similars, I try to avoid conflicts with namespace :

namespace lib_old
{
#include "my_header_ptr_old.h"
}

namespace lib_new
{
#include "my_header_ptr_new.h"
}

My second question is :

Is it correct to declare functions pointers types this way in this case ?

I can do the same for the 1st form of headers but I'm not sure it's usefull, according to the first question above. Nevertheless, If I try it on windows, it compiles fine without warnings. Unfortunately, on linux I get:

my_header_new.h: warning: conflicting C language linkage declaration 'int lib_new::func(int)'

my_header_old.h: note: previous declaration 'int lib_old::func(int)'

The warning seems important according to the answers of this question. Furthemore, none of the answers purpose a workaround.

Since I didn't found any way to solve the conflict-problem without modifying the prototype of the functions in the headers my_header_new.h and my_header_old.h, I think a better way is to resolve the problem by using only the second form (my_header_ptr_old.h and my_header_ptr_new.h).

Eventually, I saw in comments that "C linkage moots the namespacing" and some "ABI conflicts" could happen "when you use both versions in the same translation unit" and I'm interested in sources on this subject.

Ulrich Eckhardt
  • 16,572
  • 3
  • 28
  • 55
kingsjester
  • 94
  • 1
  • 12
  • 5
    You can't link two versions of a C library together. C doesn't work that way. – rustyx May 02 '23 at 15:20
  • 3
    Don't include two versions of the function in one file. If a code uses the old version, then it does `#include "admin_tcef_old.h"`, if another code uses the new version, it does `#include "admin_tcef.h"`. Nether of them includes the both files. – 273K May 02 '23 at 15:26
  • 3
    Even if you manage to resolve the declarations the way you want: How do you make sure that there won't be ABI conflicts when you use both versions in the same translation unit? That seems very risky. – user17732522 May 02 '23 at 15:30
  • 4
    This cannot work (as you envision). Where the two versions of the library provide functions with the same name, you will only be able to link *one* of those functions into your program. I'm actually impressed that your Linux compiler recognized the problem here, because the C linkage moots the namespacing. – John Bollinger May 02 '23 at 15:32
  • 4
    "I'm actually impressed that your Linux compiler recognized the problem here because the C linkage moots the namespacing." That's also probably why "On windows, this code compiles fine without warnings." Doesn't mean the code *works* without problems though. – erik258 May 02 '23 at 15:34
  • 1
    Thanks a lot ! If I don't want to use two versions of the library together, rather choose at runtime which one I use, is there a way to include only one of the two headers (i read that #include inside if/else is not recommended) ? – kingsjester May 02 '23 at 15:34
  • 2
    @kingsjester Then compile the library versions as a shared libraries (e.g. `.so`) and load only one of the two dynamically at runtime (e.g. with `dlopen`). – user17732522 May 02 '23 at 15:35
  • 2
    ... but only if all the two versions provide the same API with respect to all the functions and types you want to use from them. Overall, you might find it easier to just build two different versions of your program. – John Bollinger May 02 '23 at 15:37
  • 1
    yes my program already "compile the library versions as a shared libraries (e.g. .so) and load only one of the two dynamically at runtime (e.g. with dlopen)" but I store the result of dlsym in a function pointer which type is defined according to the header.. It's why I have two headers – kingsjester May 02 '23 at 15:49
  • 1
    It sounds like you aren't actually using `func` so just remove it from both headers – Alan Birtles May 02 '23 at 15:55
  • 3
    if you're using `dlsym` then you aren't using the function declarations, do you ever actually directly call `func`? Or do you call it via a `dlsym` function pointer – Alan Birtles May 02 '23 at 19:50
  • 1
    It seems that the problem is fixed by declaring only function pointer in the headers, with typedef int (*func)(int argInt) – kingsjester May 03 '23 at 09:36
  • 1
    This question needs a **lot** of context to have any hope of a meaningful answer. How do you plan to **link** to two versions of the same library that define the same symbols? What difference does it make whether you name the one C function via one namespace or the other (such that you need the two of them)? – Davis Herring Jul 20 '23 at 23:48
  • 1
    (To be more specific, please edit the question to include the details from the comments as well as describe the differences between the relevant header files.) – Davis Herring Jul 20 '23 at 23:58
  • 1
    @DavisHerring thank you for the comment, it's done. – kingsjester Jul 22 '23 at 09:10
  • 1
    @kingsjester: We’re getting there, but what is it that differs between the header files? (That is, why can’t you use just one of them and still switch out the shared objects?) What are the function pointer **variables** (as opposed to function types) for? (It’s probably a good idea to edit _into_ the question, as if it had always included all this.) – Davis Herring Jul 22 '23 at 16:46
  • 1
    @DavisHerring done again. – kingsjester Jul 23 '23 at 12:18
  • 2
    @ Davis Herring With `dlopen` you can't use function prototypes, so the problem is moot. Instead you have to have pointers to functions with same signature as functions in library (C++ allows to do this an automatic way) and assign actual values to "load" them. That's how extended OpenGL API works. – Swift - Friday Pie Jul 23 '23 at 12:41

3 Answers3

1

This is still a bit vague, but perhaps that's for the best, since it lets the answer cover a broader set of use cases.

Certainly, you can't have any undefined references to functions in a library you're not going to load until runtime. That by itself doesn't mean that you can't #include the relevant header (which you might need for, say, a struct definition), but if you do you must verify whether the two library versions are sufficiently similar ("ABI-compatible", at least in relevant part) that nothing goes wrong using the compiled code from one version with the (type) declarations from the other.

If the versions are so compatible, you can probably use just one of the header files. The function-pointer variable approach is a separate convenience: it allows, once you have loaded the correct version and installed the function pointers, the rest of your code to be written as if it had the library as an ordinary dependency. While you're using a header crafted for the purpose of cross-version compatibility, note that you might very well want to put the function pointers into a namespace so that they do not conflict with the C functions of the same name in the shared library. This also provides an opportunity to use just one header, perhaps expressing the intersection of the two interfaces to avoid accidentally depending on something non-portable.

If the two versions are not ABI-compatible, but use (some of) the same symbols, things get really interesting. The proposed inclusion of both interfaces in namespaces works in almost exactly the cases where it's useless: since you can't refer to functions or variables, only the types could be helpful, and the two versions of any interfaces defined in terms of those types would conflict because they used different types (as established by the namespaces).

The safe approach in that case is to #include each version in a separate translation unit (being careful to avoid link-time optimization that might allow the inconsistent definitions to interact), basically writing your own compatibility wrapper around the two versions. That component of your program would have its own, single header (perhaps complete with function pointer variables) against which the rest of the program was written.

Davis Herring
  • 36,443
  • 4
  • 48
  • 76
1

Im' not sure I understand what you want to achieve, but if you are sure that the 2 libraries are ABI compatible and the functions have the same signature, you probably don't need an extern "C" header declaration.

For instance this simple code

#include <dlfcn.h>
#include <iostream>

int main(int argc, char** argv){

  int version = std::atoi(argv[1]);

  void* lib;
  if(version == 0)
    lib = dlopen("func0.so", RTLD_NOW);
  if(version == 1)
    lib = dlopen("func1.so", RTLD_NOW);

  auto f = reinterpret_cast<int(*)(int)>(dlsym(lib, "func"));
  std::cout<<f(0)<<"\n";
}

should load func0.so or func1.so depending on a runtime value on linux, and call a symbol matching the signature int func(int). In literature the "component configurator" design pattern may be describing a solution for your problem as I understood it.

Paolo Crosetto
  • 1,038
  • 7
  • 17
  • Thanks for the answer ! In my case the 2 libraries are not ABI compatible and some functions have not exactly the same signature. Furthermore, I need a way to load the type of the functions because it's seems painful to correctly write all the reinterpret_cast by myself. The if version == corresponds to the way I wrote my program. – kingsjester Jul 25 '23 at 20:21
1

I would consider the following approach with shared objects whose version might change and also ABI might change:

foo version 1 (foo.1.cpp -> foo.1.so):

#include "foo.h"

#include <iostream>

#define VERSION 1

namespace foo::v1 {
    void bar() {
        std::cout << "version: " << VERSION << std::endl;
    }
}

namespace {
    call_table_v1 const ct = {
        .bar = foo::v1::bar,
    };

    call_table_description const ctd = {
        .version = VERSION,
        .call_table = &ct,
    };
}

call_table_description get_call_table_description(void)
{
    return ctd;
}

foo version 2 (foo.2.cpp -> foo.2.so):

#include "foo.h"

#include <iostream>

#define VERSION 2

namespace foo::v2 {
    void bar(int param) {
        std::cout << "version: " << VERSION << ", param: " << param << std::endl;
    }
}

namespace {
    call_table_v2 const ct = {
        .bar = foo::v2::bar,
    };

    call_table_description const ctd = {
        .version = VERSION,
        .call_table = &ct,
    };
}

call_table_description get_call_table_description(void)
{
    return ctd;
}

foo generic header to access different versions:

#ifndef FOO_H_
#define FOO_H_

#ifdef __cplusplus
extern "C" {
#endif /* __cplusplus */

struct call_table_v1 {
  void (*bar)(void);
};

struct call_table_v2 {
  void (*bar)(int);
};

struct call_table_description {
  int version;
  void const *call_table;
};

struct call_table_description get_call_table_description(void);

#ifdef __cplusplus
}
#endif /* __cplusplus */

#endif /* FOO_H_ */

Main program to access libraries:

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>

#include <dlfcn.h>

#include "foo.h"

int main(int argc, char **argv) {

  int const version = atoi(argv[1]);

  void *lib;
  switch (version) {
  case 1:
    lib = dlopen("./foo.1.so", RTLD_NOW);
    break;
  case 2:
    lib = dlopen("./foo.2.so", RTLD_NOW);
    break;
  default:
    fprintf(stderr, "unsupported version %i of library\n", version);
    return EXIT_FAILURE;
  }

  if (!lib) {
    perror("could not open library");
    return EXIT_FAILURE;
  }

  typeof(get_call_table_description) *call_table_description_getter_f =
      dlsym(lib, "get_call_table_description");

  struct call_table_description const ctd = call_table_description_getter_f();
  assert(ctd.version == version);

  switch (ctd.version) {
  case 1: {
    struct call_table_v1 const *pct_v1 = ctd.call_table;
    pct_v1->bar();
  } break;
  case 2: {
    struct call_table_v2 const *pct_v2 = ctd.call_table;
    pct_v2->bar(42);
  } break;
  default:
    assert(0);
  }

  dlclose(lib);

  return 0;
}

Build and run to check it out:

dpronin-gentoo➜  dlopen  ᐅ  g++ -shared -fPIC foo.1.cpp -ofoo.1.so 
dpronin-gentoo➜  dlopen  ᐅ  g++ -shared -fPIC foo.2.cpp -ofoo.2.so
dpronin-gentoo➜  dlopen  ᐅ  gcc main.c -g -omain                  
dpronin-gentoo➜  dlopen  ᐅ  ./main 1
version: 1
dpronin-gentoo➜  dlopen  ᐅ  ./main 2
version: 2, param: 42
dpronin
  • 275
  • 1
  • 6
  • Thank you! Extern 'C' doesn't seem appropriate here since your files to create .so are in C++. I've awarded you the bounty since it appears to me that it's a nice answer for being able to choose the shared lib at runtime but it doesn't correspond exactly to the question. Despite this, Cheers! – kingsjester Aug 18 '23 at 05:41