如何在多个 C 扩展之间共享一个 C 单例

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

我有一个静态库(或一堆 c/cpp-files ),它包含一个单例并用于 by/linked 两个不同的 C 扩展。但是,C 库中的单例不再表现得像单例:

import getter
import setter

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

为了简单起见,下面是一个使用 Cython 说明此问题的最小示例(所有文件都在同一文件夹中):

C 库:

//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;
}

两个 Cython 扩展:

# 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)

之后的安装文件:

#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"]}) ] 
     )

通过python setup.py build_clib build_ext --inplace构建后,上面的python脚本可以运行.

在多个 (Cython)-C 扩展之间共享 C 单例的正确方法是什么?

手头的问题是变量 singleton 存在两次:一次在扩展 setter 中,一次在扩展 getter 中(也函数 get_singletonset_singleton 存在两次,即每个有两个不同的地址),这或多或少违反了一个定义规则(ODR),即使该规则仅存在于 C++ 中。违反 ODR 并不是世界末日,但在大多数情况下,行为变得不可移植,因为不同的 linkers/compilers/OSes 处理这种情况的方式不同。

例如,对于 Linux 上的共享库,我们有 symbol-interposition。但是,Python 使用 ldopen 而不使用 RTLD_GLOBAL(意味着隐式使用 RTLD_LOCAL)来加载 C-extensions,从而防止 symbol-interposition。我们可以在 Python:

中强制使用 RTLD_GLOBAL
import sys; import ctypes;
sys.setdlopenflags(sys.getdlopenflags() | ctypes.RTLD_GLOBAL)

在导入 gettersetter 之前再次恢复 singleton-property。然而,这不适用于 Windows,因为 dll 不支持 symbol-interposition.

确保“singleton-property”的可移植方法是避免违反 ODR,为了实现这一点,应该使文件的静态 library/bunch 动态化。这个动态库只会被进程加载一次,从而保证我们只有一个singleton.

根据具体情况,有一些如何使用此 dll 的选项:

  1. 这些扩展仅在本地使用,而不是分布式使用,使用共享对象(请参阅此 ) or dll (see this )。
  2. 扩展仅在某些平台上分发,然后可以预构建共享 objects/dlls 并像 third-party 库一样分发它们,例如
  3. 可以覆盖 setuptools 的 build_clib 命令,因此它将构建共享 object/dll 而不是静态库,当扩展被链接并复制到安装时将使用静态库。但是,添加更多将使用此 dll 的扩展非常麻烦(即使并非不可能)。
  4. 可以写一个cython-wrapper的dll,优点是可以利用Python的机器进行加载,直到run-time ,而不是底层操作系统的 linkers/loaders,这使得以后更容易根据动态库创建进一步的扩展。

我认为应该默认使用最后一种方法。这是一个可能的实现:

  1. 创建静态库的包装器并通过 pxd-文件公开其功能:
# 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)

一个重要的部分:包装器引入了一个间接级别(因此导致大量 boiler-plate 代码编写应该自动化),因此在进一步的模块中使用它时既 header-files 也不 c-files/libraries 需要。

  1. 调整其他模块,他们只需要cimport包装器:
# 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. 安装程序不再需要 build_clib 步:
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']),],
     )

通过 python setup.py build_ext --inplace 构建后(在源代码分发中,即 python setup.py build sdist h-file 将丢失,但对于这个问题可能有许多不同的解决方案)示例将 set/get同一个单例(因为只有一个)。