Le problème en question est que la variable singleton
existe deux fois : une fois dans l'extension setter
et une fois dans l'extension getter
(également les fonctions get_singleton
et set_singleton
existent deux fois, c'est-à-dire ayant deux adresses différentes chacune), c'est plus ou moins une violation de la règle d'une définition (ODR) même si cette règle n'existe que en C++. La violation de l'ODR n'est pas la fin du monde, mais dans la plupart des cas, le comportement devient non portable car différents linkers/compilateurs/SO traitent cette situation différemment.
Par exemple, pour les bibliothèques partagées sur Linux, nous avons l'interposition de symboles. Cependant, Python utilise ldopen
sans RTLD_GLOBAL
(signifie implicitement avec RTLD_LOCAL
) pour le chargement des extensions C, empêchant ainsi l'interposition de symboles. Nous pourrions forcer l'utilisation de RTLD_GLOBAL
en Python :
import sys; import ctypes;
sys.setdlopenflags(sys.getdlopenflags() | ctypes.RTLD_GLOBAL)
avant d'importer getter
et setter
et restaurer à nouveau la propriété du singleton. Cependant, cela ne fonctionnerait pas sous Windows, car les dll ne supportent pas l'interposition de symboles.
La manière portable de garantir la "propriété du singleton" est d'éviter une violation de l'ODR et pour y parvenir, il convient de rendre la bibliothèque statique/un ensemble de fichiers dynamique. Cette bibliothèque dynamique ne sera chargée qu'une seule fois par le processus, garantissant ainsi que nous n'avons qu'un seul singleton
.
En fonction du scénario, il existe quelques options pour l'utilisation de cette dll :
- les extensions sont utilisées uniquement localement et non distribuées, en utilisant des objets partagés (voir ce message SO) ou une dll (voir ce message SO).
- les extensions ne sont distribuées que sur certaines plates-formes, il est alors possible de précompiler des objets partagés/dll et de les distribuer comme une bibliothèque tierce, voir par exemple ce message SO
- il est possible de remplacer la commande
build_clib
de setuptools, de sorte qu'elle construise un objet partagé/dll au lieu d'une bibliothèque statique, qui sera utilisé lorsque les extensions sont liées et copiées vers l'installation. Cependant, ajouter davantage d'extensions qui utiliseraient cette même dll est assez fastidieux (même si pas impossible).
- il est possible d'écrire un wrapper cython de la dll, qui a l'avantage d'utiliser le mécanisme de chargement de Python et de reporter la résolution des symboles jusqu'au moment de l'exécution, plutôt que les linkers/chargeurs des OS sous-jacents, ce qui facilite par exemple la création ultérieure d'autres extensions dépendant de la bibliothèque dynamique.
Je pense que la dernière approche devrait être utilisée par défaut. Voici une implémentation possible:
-
Créez un wrapper de la bibliothèque statique et exposez sa fonctionnalité via un fichier 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)
Une partie importante : le wrapper introduit un niveau d'indirection (induisant donc beaucoup d'écriture de code de base qui devrait être automatisée), donc lors de son utilisation dans d'autres modules, ni les fichiers d'en-tête ni les fichiers/c/librairies ne sont nécessaires.
-
Adaptez les autres modules, ils ont juste besoin d'importer le wrapper avec 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)
-
La configuration n'a plus besoin de l'étape 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']),],
)
Après la construction via python setup.py build_ext --inplace
(dans la distribution source, c'est-à-dire python setup.py build sdist
, le fichier h manquera, mais il existe de nombreuses solutions possibles pour ce problème), l'exemple set
/get
le même singleton (car il n'y en a qu'un).