Conflicting C linkage declaration of two functions in different C++ namespaces

482 views Asked by At

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.

3

There are 3 answers

1
dpronin On BEST ANSWER

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
0
Davis Herring On

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.

1
Paolo Crosetto On

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.