How to share a C-singleton between multiple C-extensions

冷暖自知 提交于 2021-02-15 05:16:44

问题


I have a static library (or a bunch of c/cpp-files for that matter) which contains a singleton and is used by/linked to two different C-extensions. However, the singleton from the C-library doesn't behave like a singleton anymore:

import getter
import setter

# set singleton:
setter.set(21)
# get singleton:
print("singleton: ", getter.get()) 
#prints the old value:42

Here is a minimal example illustrating this problem using Cython for the sake of simplicity (all files are in the same folder):

C-library:

//lib.h:
int get_singleton(void);
void set_singleton(int new_val);

//lib.c:
#include "lib.h"

static int singleton=42;
int get_singleton(void){
    return singleton;
}
void set_singleton(int new_val){
    singleton=new_val;
}

The two Cython extensions:

# getter.pyx:
#cython: language_level=3

cdef extern from "lib.h":
    int get_singleton()

def get():
    return get_singleton()

# setter.pyx:
#cython: language_level=3

cdef extern from "lib.h":
    void set_singleton(int new_val);

def set(new_val):
    set_singleton(new_val)

A setup-file following this SO-post:

#setup.py
from setuptools import setup, find_packages, Extension

setup(
      name='singleton_example',
      ext_modules=[Extension('getter', sources=['getter.pyx']), 
                   Extension('setter', sources=['setter.pyx']),],
      # will be build as static libraries and automatically passed to linker for all extensions:
      libraries = [('static_lib', {'sources': ["lib.c"]}) ] 
     )

After building via python setup.py build_clib build_ext --inplace, the above python script can be run.

What is the correct way to share a C-singleton between multiple (Cython)-C-extensions?


回答1:


The problem at hand is that the variable singleton exists twice: once in the extension setter and once in the extension getter (also functions get_singleton and set_singleton exist twice, i.e. having two different addresses each), this is more or less a violation of one definition rule (ODR) even if this rule only exists in C++. Violation of ODR is not the end of the world, but in most cases the behavior becomes not portable because different linkers/compilers/OSes handle this situation differently.

For example, for shared libraries on Linux we have the symbol-interposition. However, Python uses ldopen without RTLD_GLOBAL (means implicitly with RTLD_LOCAL) for loading of C-extensions, thus preventing symbol-interposition. We could enforce the usage of RTLD_GLOBAL in Python:

import sys; import ctypes;
sys.setdlopenflags(sys.getdlopenflags() | ctypes.RTLD_GLOBAL)

prior to importing getter and setter and restore the singleton-property again. This however would not work on Windows, because dlls don't support symbol-interposition.

The portable way to ensure the "singleton-property" is to avoid a violation of ODR and in order to achieve that one should make the static library/bunch of files dynamic. This dynamic library will be loaded only once by the process, thus ensuring that we have only one singleton.

Depending on the scenario, there are some options how this dll can be used:

  1. the extensions are used only locally and not distributed, using shared objects (see this SO-post) or dll (see this SO-post).
  2. extensions are only distributed on some platforms, then it is possible to prebuild shared objects/dlls and distribute them like a third-party library, see for example this SO-post
  3. it is possible to override setuptools' build_clib command, so it will build a shared object/dll instead of a static library, which will be used when extensions are linked and copied to the installation. However, adding more extension, which would use this very dll is quite cumbersome (even if not impossible).
  4. it is possible to write a cython-wrapper of the dll, which has the advantage of using the Python's machinery for loading and posponing the resultion of the symbols until the run-time, rather than linkers/loaders of the underlying OSes, which makes it for example easier to create futher extensions depending on the dynamic library later on.

I think the last approach should be used by default. Here is a possible implementation:

  1. Create a wrapper of the static library and expose its functionality via a pxd-file:
# lib_wrapper.pxd
cdef int get_singleton()
cdef void set_singleton(int new_value)

#lib_wrapper.pyx
cdef extern from "lib.h":
    int c_get_singleton "get_singleton" ()
    void c_set_singleton "set_singleton" (int new_val)

cdef int get_singleton():
    return c_get_singleton()

cdef void set_singleton(int new_val):
    c_set_singleton(new_val)

An important part: the wrapper introduces a level of indirection (thus inducing a lot of boiler-plate code writing which should be automatized), thus when using it in further modules neither header-files nor c-files/libraries are needed.

  1. Adjust other modules, they need only to cimport the wrapper:
# getter.pyx:
#cython: language_level=3
cimport lib_wrapper
def get():
    return lib_wrapper.get_singleton()

# setter.pyx:
#cython: language_level=3
cimport lib_wrapper
def set(new_val):
    lib_wrapper.set_singleton(new_val)
  1. Setup no longer needs build_clib-step:
from setuptools import setup, find_packages, Extension

setup(
      name='singleton_example',
      ext_modules=[Extension('lib_wrapper', sources=['lib_wrapper.pyx', 'lib.c']),
                   Extension('getter', sources=['getter.pyx']), 
                   Extension('setter', sources=['setter.pyx']),],
     )

After building via python setup.py build_ext --inplace (in the source distribution, i.e. python setup.py build sdist the h-file will be missing, but there are many different solutions for this problem possible) the example will set/get the same singleton (because there is only one).



来源:https://stackoverflow.com/questions/63875206/how-to-share-a-c-singleton-between-multiple-c-extensions

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!