2 votes

Comment partager un singleton C entre plusieurs extensions C

J'ai une bibliothèque statique (ou un ensemble de fichiers c/cpp pour ainsi dire) qui contient un singleton et est utilisée par/liée à deux extensions C différentes. Cependant, le singleton de la bibliothèque C ne se comporte plus comme un singleton :

import getter
import setter

# set singleton:
setter.set(21)
# get singleton:
print("singleton: ", getter.get()) 
#imprime l'ancienne valeur :42

Voici un exemple minimal illustrant ce problème en utilisant Cython pour simplifier les choses (tous les fichiers sont dans le même dossier) :

Bibliothèque 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;
}

Les deux extensions 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)

Un fichier de configuration suivant ce post SO :

#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']),],
      # sera compilé en tant que bibliothèques statiques et automatiquement transmis au linker pour toutes les extensions :
      libraries = [('static_lib', {'sources': ["lib.c"]}) ] 
     )

Après la compilation via python setup.py build_clib build_ext --inplace, le script python ci-dessus peut être exécuté.

Quelle est la manière correcte de partager un singleton C entre plusieurs extensions C (Cython) ?

2voto

ead Points 1051

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 :

  1. 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).
  2. 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
  3. 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).
  4. 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:

  1. 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.

  1. 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)

  2. 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).

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X