Appeler exit () dans la bibliothèque C++ termine le script python qui enroule cette bibliothèque en utilisant swig

j'écris un wrapper Swig-Python pour une bibliothèque C++. Lorsque l'erreur critique se produit , la bibliothèque appelle exit(err); , qui à son tour termine le script python entier qui exécute les fonctions à partir de cette bibliothèque.

y a-t-il un moyen de contourner la fonction exit() pour revenir au script ou faire une exception?

3
demandé sur alnet 2015-12-02 16:12:51

2 réponses

vous pouvez assembler un hack massif pour cela en utilisant longjmp et on_exit , bien que je recommande fortement d'éviter cela en faveur d'une solution avec des processus multiples, que je décrirai plus tard dans la réponse.

supposons que nous ayons le fichier d'en-tête suivant (brisé par le dessin):

#ifndef TEST_H
#define TEST_H

#include <stdlib.h>

inline void fail_test(int fail) {
  if (fail) exit(fail);
}

#endif//TEST_H

nous voulons l'envelopper et convertir l'appel à exit() en une exception Python à la place. Une façon d'y parvenir serait quelque chose comme l'interface suivante, qui utilise %exception pour insérer du code C autour de l'appel à chaque fonction C de votre interface Python:

%module test

%{
#include "test.h"
#include <setjmp.h>

static __thread int infunc = 0;
static __thread jmp_buf buf;

static void exithack(int code, void *data) {
  if (!infunc) return;
  (void)data;
  longjmp(buf,code);
}
%}

%init %{
  on_exit(exithack, NULL);
%}

%exception {
  infunc = 1;
  int err = 0;
  if (!(err=setjmp(buf))) {
    $action
  }
  else {
    // Raise exception, code=err
    PyErr_Format(PyExc_Exception, "%d", err);
    infunc = 0;
    on_exit(exithack, NULL);
    SWIG_fail;
  }
  infunc = 0;
}

%include "test.h"

cela "fonctionne" quand nous le compilons:

swig3.0 -python -py3 -Wall test.i
gcc -shared test_wrap.c -o _test.so -I/usr/include/python3.4 -Wall -Wextra -lpython3.4m 

et nous pouvons le démontrer avec:

Python 3.4.2 (default, Oct  8 2014, 13:14:40)
[GCC 4.9.1] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import test
>>> test.fail_test(0)
>>> test.fail_test(123)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
Exception: 123
>>> test.fail_test(0)
>>> test.fail_test(999)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
Exception: 999
>>>

il est très laid cependant, presque certainement pas portable et le plus probable comportement non défini ainsi.


mon conseil serait de ne pas le faire et utiliser une solution avec deux processus de communication au lieu. Nous pouvons encore avoir SWIG pour nous aider à générer un module agréable et mieux encore, nous pouvons compter sur quelques constructions Python de haut niveau pour nous aider avec cela. L'exemple complet ressemble à:

%module test

%{
#include "test.h"

static void exit_handler(int code, void *fd) {
  FILE *f = fdopen((int)fd, "w");
  fprintf(stderr, "In exit handler: %d\n", code);
  fprintf(f, "(dp0\nVexited\np1\nL%dL\ns.", code);
  fclose(f);
}
%}

%typemap(in) int fd %{
   = PyObject_AsFileDescriptor($input);
%}

%inline %{
  void enter_work_loop(int fd) {
    on_exit(exit_handler, (void*)fd);
  }
%}

%pythoncode %{
import os
import pickle

serialize=pickle.dump
deserialize=pickle.load

def do_work(wrapped, args_pipe, results_pipe):
  wrapped.enter_work_loop(results_pipe)

  while True:
    try:
      args = deserialize(args_pipe)
      f = getattr(wrapped, args['name'])
      result = f(*args['args'], **args['kwargs'])
      serialize({'value':result},results_pipe)
      results_pipe.flush()
    except Exception as e:
      serialize({'exception': e},results_pipe)
      results_pipe.flush()

class ProxyModule():
  def __init__(self, wrapped):
    self.wrapped = wrapped
    self.prefix = "_worker_"

  def __dir__(self):
    return [x.strip(self.prefix) for x in dir(self.wrapped) if x.startswith(self.prefix)]

  def __getattr__(self, name):
    def proxy_call(*args, **kwargs):
      serialize({
        'name': '%s%s' % (self.prefix, name),
        'args': args,
        'kwargs': kwargs
      }, self.args[1])
      self.args[1].flush()
      result = deserialize(self.results[0])
      if 'exception' in result: raise result['exception']
      if 'exited' in result: raise Exception('Library exited with code: %d' % result['exited'])
      return result['value']
    return proxy_call

  def init_library(self):
    def pipes():
      r,w=os.pipe()
      return os.fdopen(r,'rb',0), os.fdopen(w,'wb',0)

    self.args = pipes()
    self.results = pipes()

    self.worker = os.fork()

    if 0==self.worker:
      do_work(self.wrapped, self.args[0], self.results[1])
%}

// rename all our wrapped functions to be _worker_FUNCNAME to hide them - we'll call them from within the other process
%rename("_worker_%s") "";
%include "test.h"

%pythoncode %{
import sys
sys.modules[__name__] = ProxyModule(sys.modules[__name__])
%}

qui utilise les idées suivantes:

  1. Pickle sérialiser des données avant de les écrire sur des pipes à un processus ouvrier.
  2. os.fork pour lancer le processus de travail, avec os.fdopen créant un objet plus agréable à utiliser en Python
  3. SWIG's advanced rename pour cacher les fonctions réelles que nous avons enveloppé des utilisateurs du module, mais les envelopper tout de même
  4. une astuce pour remplacer le module par un objet Python qui implémente __getattr__ pour retourner les fonctions de proxy pour le travailleur le processus de
  5. __dir__ pour garder TAB travailler à l'intérieur d'ipython
  6. on_exit pour intercepter la sortie (mais pas la dévier) et rapporter le code via un ascii pré-écrit objet décapé

vous pouvez faire l'appel à library_init transparent et automatique si vous le souhaitez. Vous devez également gérer le cas où le travailleur n'a pas été démarré ou a déjà sorti mieux (il va juste bloquer dans mon exemple). Et vous devrez également vous assurer que le travailleur est nettoyé correctement à la sortie, mais il vous permet maintenant de courir:

Python 3.4.2 (default, Oct  8 2014, 13:14:40)
[GCC 4.9.1] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import test
>>> test.init_library()
>>> test.fail_test(2)
In exit handler: 2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/mnt/lislan/ajw/code/scratch/swig/pyatexit/test.py", line 117, in proxy_call
    if 'exited' in result: raise Exception('Library exited with code: %d' % result['exited'])
Exception: Library exited with code: 2
>>>

et être encore (quelque peu) portable, mais certainement bien défini.

3
répondu Flexo 2017-05-23 12:15:03

sans plus d'information il est difficile d'offrir une solution, cependant:

avez-vous écrit la bibliothèque? Si oui, pouvez-vous le retravailler pour lancer un logic_error au lieu d'appeler exit ?

si la bibliothèque appelle exit , cela implique un échec totalement catastrophique. L'état interne de la bibliothèque pourrait bien être incohérente (vous devez supposer que c'est!) - êtes-vous sûr de vouloir poursuivre le processus après cela? Si vous ne pas écrire de la bibliothèque vous n'êtes pas en mesure de raison à ce sujet. Si vous l'avez fait, voir ci-dessus.

peut-être pourriez-vous écrire un processus d'enveloppement autour de la bibliothèque, et marshall appelle à travers la limite du processus? Cela sera plus lent dans l'exécution et plus douloureux pour écrire et maintenir, mais cela permettra au processus parent (python) de détecter la terminaison de l'enfant (l'enrubanneur de bibliothèque).

0
répondu Richard Hodges 2015-12-02 13:53:39