1
votes

I am wrapping the following C++ code:

// content of cpp.h
class A
{
public:
  virtual int fnA() = 0;

  virtual ~A() { }
};

class B
{
public:
  int fnB(A &a)
  {
    return a.fnA();
  }
};

by SWIG using the SWIG wrapper:

// content of swigmodule.i
%module(directors="1") swigmodule

%feature("director");

%feature("director:except") {
    if ($error != NULL) {
      fprintf(stderr, "throw\n");
        throw Swig::DirectorMethodException();
    }
}

%exception {
    try { $action }
    catch (Swig::DirectorException &e) { fprintf(stderr, "catch\n"); SWIG_fail; }
}

%{
#include "cpp.h"
%}

%include "cpp.h"

where the exception handling is copied from the SWIG manual. Using this I generated the SWIG wrapper with: "swig -c++ -python swigmodule.i; g++ -shared -fPIC -I/usr/include/python2.7 swigmodule_wrap.cxx -o _swigmodule.so"

The problem now comes when incorrectly overloading the "int fnA()" function with a "void fnA()" in Python.

# content of useit.py
from swigmodule import A, B

class myA(A):

    def fnA(self):
        print("myA::fnA")

b = B();
a = myA();

print("%d"% b.fnB(a) )

The generated SWIG wrapper correctly flags this at runtime as an error; fnA(self) returns None which is not an int. However, the output at the console is:

$ python useit.py
myA::fnA
catch
Traceback (most recent call last):
  File "useit.py", line 12, in <module>
    print("%d"% b.fnB(a) )
  File "/home/schuttek/tmp/swigmodule.py", line 109, in fnB
    def fnB(self, *args): return _swigmodule.B_fnB(self, *args)
TypeError: SWIG director type mismatch in output value of type 'int'

which is misleading as it suggests that the error is in B::fnB where the actual error is in overloading A::fnA.

How do I get SWIG to provide a meaningfull diagnostic to where the error occured? In my real code (this is a simplified version) I had to use GDB to trap on the constructor of the Swig::DirectorException class. This is unwanted as the actual error was in the Python domain (where an incorrect overlaod was performed) and I want to shield future Python users from GDB and its usage, as well as from a SWIG internal such as a DirectorException.

2
I'm not sure this really is an error in A, it's during the application of fnB that the type mismatch is detected and rightly so. You probably could do something clever with metaclasses to spot it earlier if you really wanted though, but I'm not sure I buy it as "a big thing". - Flexo
What I hoped for is something along the lines that the text in the TypeError has something like "during matching A::fnA" added to its current diagnostic. - Klamer Schutte
Do you want to change it globally? I can tell you which typemap controls that %typemap(directorout), using the macro %dirout_fail to actually do it but changing globally is tricky because there are a lot of typemaps for use with directors already, so it's not just a case of tweaking one to apply everywhere. Apply for all int or any other known return type should be simple enough though. - Flexo
I think I want the change globally. An example typemap will be greatly appreciated! - Klamer Schutte

2 Answers

1
votes

You can get some distance towards improving the error like you want using %typemap(directorout), for example:

// content of swigmodule.i
%module(directors="1") swigmodule

%feature("director");

%feature("director:except") {
    if ($error != NULL) {
      fprintf(stderr, "throw\n");
        throw Swig::DirectorMethodException();
    }
}

%exception {
    // $parentclassname
    try { $action }
    catch (Swig::DirectorException &e) { fprintf(stderr, "catch\n"); SWIG_fail; }
}

%{
#include "cpp.h"
%}

%typemap(directorout,noblock=1,fragment=SWIG_AsVal_frag(int)) int (int swig_val) {
  int swig_res = SWIG_AsVal(int)($input, &swig_val);
  if (!SWIG_IsOK(swig_res)) {
    //%dirout_fail(swig_res, "$type"); // This line expands into the default call
    Swig::DirectorTypeMismatchException::raise(SWIG_ErrorType(SWIG_ArgError(swig_res)), "in output of $symname value of type '""$type""'");
  }
  $result = %static_cast(swig_val,$type);
}


%include "cpp.h"

Which replaces the directorout typemap for int. It seems that directorout doesn't get some special variables from %exception applied though, so the best we get from SWIG itself is $symname, which isn't fully qualified, but does let you slip fnA into the error message.

You can use __PRETTY_FUNCTION__ to get something a little more explicit about what caused it, but that will now include "SwigDirector_" in the type - probably not a big deal:

%typemap(directorout,noblock=1,fragment=SWIG_AsVal_frag(int)) int (int swig_val) {
  int swig_res = SWIG_AsVal(int)($input, &swig_val);
  if (!SWIG_IsOK(swig_res)) {
    //%dirout_fail(swig_res, "$type");
    const std::string msg = std::string("in output of ") + __PRETTY_FUNCTION__ + " ($symname) value of type '""$type""'";
    Swig::DirectorTypeMismatchException::raise(SWIG_ErrorType(SWIG_ArgError(swig_res)), msg.c_str());
  }
  $result = %static_cast(swig_val,$type);
}

Where that gets tricky though is if you want to do the same for all possible return types - directorout is generated by a series of macros inside the SWIG library, so there's one of return primitives by value (including const/reference/etc. variants), one for strings, one for non-primitive types so it's quite a lot of work to do it everywhere. (You probably need to patch the core SWIG library to do that everywhere, properly).

0
votes

A way to fix this for all return types is by hacking the SWIG generated wrapper file. I have done this by:

cp swigmodule_wrap.cxx x
cp myRaise.h swigmodule_wrap.cxx
sed 's/Swig::DirectorTypeMismatchException::raise(/myRaise(__PRETTY_FUNCTION__,swig_get_self(),/' < x >> swigmodule_wrap.cxx

i.e. replacing all places where the DirectorTypeMismatchException is raised by a call to a separate function, and supplying the __PRETTY_FUNCTION__ and swig_get_self() extra information to generate the needed extra information.

The separate function is supplied (as a macro) in the myRaise.h file:

#include <string>
#include <Python.h>

// ReplaceString taken from http://stackoverflow.com/questions/3418231/replace-part-of-a-string-with-another-string
inline std::string ReplaceString(std::string subject, const std::string& search,
                          const std::string& replace) {
    size_t pos = 0;
    while ((pos = subject.find(search, pos)) != std::string::npos) {
         subject.replace(pos, search.length(), replace);
         pos += replace.length();
    }
    return subject;
}

#define myRaise(fn, obj, err, msg) \
  { \
     std::string fns(fn); \
     std::string objs(PyString_AsString(PyObject_Repr(obj)));       \
     std::string msgs(std::string("when calling ")+ReplaceString(fns, std::string("SwigDirector_"), std::string(""))+std::string(" in ")+objs+std::string(" ")+std::string(msg)); \
     Swig::DirectorTypeMismatchException::raise(err, msgs.c_str()); \
  }

and the resulting diagnostic is:

$ python useit.py 
myA::fnA
catch
Traceback (most recent call last):
  File "useit.py", line 12, in <module>
    print("%d"% b.fnB(a) )
  File "/home/schuttek/tmp/swigmodule.py", line 109, in fnB
    def fnB(self, *args): return _swigmodule.B_fnB(self, *args)
TypeError: SWIG director type mismatch when calling virtual int A::fnA() in <__main__.myA; proxy of <Swig Object of type 'A *' at 0x7f3628d6b540> > in output value of type 'int'

where it can be seen that the __main__.myA class does not provide the int return value as needed for the virtual int A::fnA() function.