I'm currently just trying to understand how SWIG works, and I encountered something: Using Swig 4.3, C++ 14 and generating a wrapper for Python:
This is my interface file:
//project.i
%module project
%{
#include "object.h"
%}
%include "object.h"
//%include "std_vector.i"
//%template(vec_int) std::vector<int>;
And This is the file that contains the code. I can't share the original code, because it is confidential, so I have trimmed away everything (hopefully not neccesary), and renamed the other objects.
//object.h
#include "MyClass.h"
#include <vector>
typedef int ID;
struct Object {
using MyClass = Settings::MyClass;
std::vector<MyClass> calculateMyClass() const;
std::vector<ID> getIDs() const;
}
And here is the code of the class that the vector is made of.
//MyClass.h
namespace Settings {
double member1;
double member2;
double member3;
unsigned int member4;
bool member5;
}
In my wrapper .cxx file, arguments and returnvalues of the calculateMyClass() method of the form are wrapped by the SwigValueWrapper:
[...]
SwigValueWrapper< std::vector< myClass > > result;
However, the wrapper for the getIDs method has the following result:
std::vector< ID > result;
It has not been wrapped.
It's getting even more weird: When I include
%include "std_vector.i"
In the interface file (so now Swig knows what a vector is, and how to treat it), then the wrapper code changes to
SwigValueWrapper< std::vector< int,std::allocator< int > > > result;
And when I instantiate a template via
%template(vec_int) std::vector<int>;
In the interface file, then the SwigValueWrapper is removed again, and replaced by:
std::vector< ID,std::allocator< ID > > result;
The Swig Documentation writes (6.11):
If Vector is defined as a class in the interface, but it does not support a default constructor and an assignment operator, SWIG changes the wrapper code by encapsulating the arguments inside a special C++ template wrapper class, through a process called the "Fulton Transform".
Why is a std::vector of an integer not a defined class in the interface, but a std::vector of a class I have defined myself is?
What exactly happens when I include "std_vector.i"? - I guess that means swig now knows how to resolve and wrap occurences of vector, should it meet them in the interface file. Does that now make std::vector a defined class in the interface, that is lacking a constructor?
And finally, does the instantiation of the template via
%template(vec_int) std::vector<int>;
add so mutch to the std::vector class that swig no longer needs to wrap it?
First, I assume that in MyClass.h
the member
's are supposed to be defined inside MyClass
class and that MyClass.h
is %include
'd into project.i
, directly or indirectly (e.g. in some way through -includeall
option of SWIG executable), since it would make your code compile and give results you described. With that out of the way:
The documentation isn't very clear about what happens in all these cases, so we have to look at the source. Whether, for a particular type, SwigValueWrapper
is used for arguments and return values during emission of wrapper code is decided by SwigType_alttype
function, returning 0 if wrapper shouldn't be used or the wrapped type otherwise:
SwigType *SwigType_alttype(const SwigType *t, int local_tmap) {
Node *n;
SwigType *w = 0;
int use_wrapper = 0;
SwigType *td = 0;
if (!cparse_cplusplus)
return 0;
if (value_wrapper_mode == 0) {
/* old partial use of SwigValueTypes, it can fail for opaque types */
if (local_tmap)
return 0;
if (SwigType_isclass(t)) { // 1
SwigType *ftd = SwigType_typedef_resolve_all(t);
td = SwigType_strip_qualifiers(ftd);
Delete(ftd);
n = Swig_symbol_clookup(td, 0);
if (n) { // 4
if (GetFlag(n, "feature:valuewrapper")) {
use_wrapper = 1;
} else {
if (Checkattr(n, "nodeType", "class")
&& (!Getattr(n, "allocate:default_constructor")
|| (Getattr(n, "allocate:noassign")))) {
use_wrapper = !GetFlag(n, "feature:novaluewrapper") || GetFlag(n, "feature:nodefault");
}
}
} else { // 5
if (SwigType_issimple(td) && SwigType_istemplate(td)) {
use_wrapper = 1;
}
}
}
} else {
/* safe use of SwigValueTypes, it can fail with some typemaps */
SwigType *ftd = SwigType_typedef_resolve_all(t);
td = SwigType_strip_qualifiers(ftd);
Delete(ftd);
if (SwigType_type(td) == T_USER) {
use_wrapper = 1;
n = Swig_symbol_clookup(td, 0);
if (n) {
if ((Checkattr(n, "nodeType", "class")
&& !Getattr(n, "allocate:noassign")
&& (Getattr(n, "allocate:default_constructor")))
|| (GetFlag(n, "feature:novaluewrapper"))) {
use_wrapper = GetFlag(n, "feature:valuewrapper");
}
}
}
}
if (use_wrapper) {
w = NewStringf("SwigValueWrapper<(%s)>", td);
}
Delete(td);
return w;
}
cparse_cplusplus
indicates C++ mode, value_wrapper_mode
is 0 unless an undocumented -newvwm
flag is used to specify a more aggressive usage of wrapper (i.e. "safe use of SwigValueTypes"), local_tmap
parameter is set to 1 when used for typemap local variables and otherwise (like our case) is 0
Now, the crucial function to explain the strange behaviour you see is SwigType_isclass
. If it returns 0 at line 1
, no further checks are done, use_wrapper
remains 0 and thus no wrapper is used. The intended logic is probably just to handle built-in types like int
and pointers, which obviously don't require wrapper. But it produces counterintuitive (and, likely, unintended) results if applied to undefined types due to peculiar way SwigType_isclass
is implemented:
int SwigType_isclass(const SwigType *t) {
SwigType *qty, *qtys;
int isclass = 0;
qty = SwigType_typedef_resolve_all(t);
qtys = SwigType_strip_qualifiers(qty);
if (SwigType_issimple(qtys)) {
String *td = SwigType_typedef_resolve(qtys); // 2
if (td) {
Delete(td);
}
if (resolved_scope) { // 3
isclass = 1;
}
/* Hmmm. Not a class. If a template, it might be uninstantiated */
if (!isclass) { // 6
String *tp = SwigType_istemplate_templateprefix(qtys);
if (tp && Strcmp(tp, t) != 0) {
isclass = SwigType_isclass(tp);
}
Delete(tp);
}
}
Delete(qty);
Delete(qtys);
return isclass;
}
SwigType_issimple
considers the type 'simple' when there are no top-level (meaning 'not within template parameters') operators (like 'pointer to' or cv qualifiers) in type's textual representation used by SwigType
. All types of interest to us here are simple, even before stripping the qualifiers (they don't have any)
The key check here at line 3
involves resolved_scope
, which is a global variable modified by the preceding SwigType_typedef_resolve
call at line 2
. This function is mainly intended to resolve (once1) a type alias in the given type, but here it is used only because of its effect on resolved_scope
. In particular, as you can see, its argument qtys
had all its aliases already resolved by SwigType_typedef_resolve_all
(which invokes SwigType_typedef_resolve
repeatedly until all aliases are resolved), so td
is always 0 (see below) and if (td)
check is redundant.
In SwigType_typedef_resolve
, resolved_scope
is first set to 0 and then, if the whole type or alias is found (making return value, respectively, 0 or alias definition), to a corresponding scope, but otherwise the individual parts comprising a type are attempted to be resolved:
2
) for some part, resolved_scope
is set to a scope of that alias and returned type is formed by replacing the part according to definition of the alias (no more parts are resolved);resolved_scope
still gets modified through the whole process as the parts get resolved. Since the process involves (sometimes indirect) recursion into SwigType_typedef_resolve
(which, as stated above, clears resolved_scope
at its start), the part for which it gets called last determines what the final resolved_scope
is. Return value is 0.[1] To be precise, there is one place in SwigType_typedef_resolve
where a substitution for a qualified type alias can happen for a second time, and I don't understand the comment given there as a justification. Besides, this section contains a bug (which I tested and observed), because both typedef_resolve
invocations call the underlying _typedef_resolve
with look_parent
parameter equal to 1, which allows lookup in parent scopes. This shouldn't happen during qualified lookup. That is, with the following code, not only A::Ident
immediately resolves to int
(rather than to ID
at first), but also A::ID
erroneously gets resolved to int
:
using ID = int;
namespace A {
using Ident = ID;
};
Now let's consider what happens in line 1
(and after) in SwigType_alttype
for your example in different cases:
%include "std_vector.i"
std::vector<Settings::MyClass>
can't be found as a whole, and the last part SwigType_typedef_resolve
tries to resolve is Settings::MyClass
template argument (here):
// ...
if (!type && SwigType_istemplate(base)) {
newtype = 1;
type = template_parameters_resolve(base); // 7
}
// ...
template_parameters_resolve
calls SwigType_typedef_resolve
for all template arguments stopping if an alias was found, which, again, can't be the case for us
Settings::MyClass
is found, resolved_scope
at line 3
is set, so SwigType_isclass
for std::vector<Settings::MyClass>
returns 1. Now, back at SwigType_alttype
, at line 4
the symbol corresponding to std::vector<Settings::MyClass>
still can't be found by Swig_symbol_clookup
(which performs actual symbol lookup), and in this case (line 5
) there's a heuristic (for some reason) to use wrapper if a type contains (anywhere) template arguments, which is exactly what happens here (SwigType_istemplate
performs its check purely textually, without any lookups), so the wrapper is used. Also note that SwigType_issimple(td)
check is redundant because if it was 0, check at line 1
would return 0 as well.
std::vector<ID>
gets resolved into std::vector<int>
before any further checks in SwigType_isclass
(and if it returns 1, again in SwigType_alttype
). Now the last part SwigType_typedef_resolve
tries to resolve is int
which, unlike Settings::MyClass
, fails, so resolved_scope
is 0 at line 3
. So, back in SwigType_isclass
we get to point 6
where an attempt is made to, if a type is (textually) supposed to be a template specialization, determine whether a template prefix (std::vector
) is a class by calling SwigType_isclass
for it, which understandably returns 0, so back in SwigType_alttype
at line 1
0 is returned and thus no wrapper is used.
%include "std_vector.i"
without %template
directiveAdding %include "std_vector.i"
makes SWIG aware of std::vector
. In particular, by the time SwigType_alttype
is called the type t
already has default template arguments expanded, so t
arrives as std::vector<(T,std::allocator<(T)>)>
instead of std::vector<(T)>
(note that <(
and )>
are due to aforementioned textual representation of SwigType
). T
is replaced by SwigType_typedef_resolve_all
according to aliases before any checks of interest to us, so hereafter I use it to denote Settings::MyClass
and int
(rather than their aliases which are intact upon arrival of t
in SwigType_alttype
). Now, in call at line 2
the last part resolved by SwigType_typedef_resolve
is T
because, again, at line 7
the template arguments get resolved recursively and the last is std::allocator<(T)>
, for which, analogously, the line 7
leads to its argument T
being resolved last.
So, if T
is Settings::MyClass
, analogously to case without %include "std_vector.i"
, we have nonzero resolved_scope
corresponding to Settings::MyClass
at line 3
, and the rest (up to SwigType_alttype
return) is the same (in particular, at line 4
the symbol for vector specialization still isn't found because %template
directive wasn't used). Thus, again, wrapper is used.
If T
is int
, analogously to case without %include "std_vector.i"
, we have zero resolved_scope
at line 3
, but this time when at 6
we try to call SwigType_isclass
for std::vector
, it returns 1, because, unlike specializations, the template itself is now defined (and, inside, SwigType_typedef_resolve
sets resolved_scope
to std
scope), so back at SwigType_alttype
1 is returned at line 1
and the rest is the same as for Settings::MyClass
(because vector specialization is again not defined), so wrapper is used.
%include "std_vector.i"
and %template
directiveAt line 1
, SwigType_isclass
returns 1 for both vector specializations, but because of the same reasons as in the case above without %template
directives since they, as it turns out, don't define a class in type tables (typetab) used by SwigType_typedef_resolve
(even though comment above line 6
seems to imply that the author assumed that they would). The directives, however, define symbols in symbol tables (symtab) used by Swig_symbol_clookup
, so this time at line 4
both vector specializations are found. Only now we get to the checks regarding type properties, which are performed using feature flags (corresponding to SWIG feature directives of the same name, e.g. %feature("valuewrapper")
) and attributes determining whether it is default constructible and assignable (which are set during semantic pass stage implemented by Allocate
class e.g. here, hence the allocate:
prefix) of a found parse tree node n
. Since both std::vector<Settings::MyClass>
and std::vector<int>
are default constructible and assignable (which SWIG is able to determine due to properly implemented std_vector.i
) and aren't marked with special features, for both of them no wrapper is used.
For practical purposes, this is what you want to do, and it aligns with what the documentation recommends.