6 SWIG and C++

  本章描述SWIG对封装c++的支持。它主要关注c++ 98和03标准所定义的c++。对于原有c++标准的补充,请阅读SWIG和c++ 11、SWIG和c++ 14以及SWIG和c++ 17章。作为先决条件,您应该先阅读SWIG基础一章,了解SWIG是如何包装ISO C的,对c++的支持建立在ISO C包装的基础上,这些材料将有助于理解本章。

6.1 Comments on C++ Wrapping

  由于其复杂性以及c++本身很难集成的事实,更不用说其他语言了,SWIG只支持c++特性的一个子集。幸运的是,现在这是一个相当大的子集。

  在某种程度上,c++包装的问题在于没有语义上明显的(或自动的)方法将其许多高级特性映射到其他语言中。作为一个简单的例子,考虑将c++多重继承包装到没有这种支持的目标语言的问题。类似地,当目标语言中不存在重载操作符和重载函数时,重载操作符和重载函数的使用可能会出现问题。

  c++的一个更微妙的问题与一些c++程序员对编程库的思考方式有关。在SWIG的世界中,您实际上是在尝试创建用于其他语言的二进制级软件组件。为了做到这一点,一个“组件”必须包含真正的可执行指令,并且必须有某种二进制链接机制来访问它的功能。相反,c++越来越依赖泛型编程和模板来实现其大部分功能。尽管模板是一个强大的特性,但它们在很大程度上与二进制组件和库的整个概念是正交的。例如,STL向量不定义任何类型的二进制对象,SWIG可以为其创建包装器。更复杂的是,这些库经常使用大量的幕后魔法,这些看似基本操作(如指针解引用、过程调用等)的语义可以以戏剧性的、有时不明显的方式改变。虽然这种“魔力”在只使用c++的环境中可能不会带来什么问题,但它极大地复杂化了跨越语言边界的问题,并提供了许多搬起石头砸自己的脚的机会。你得小心点。

6.2 Approach

  为了包装c++, SWIG使用分层的方法来生成代码。在最底层,SWIG生成一组过程性ISO C风格的包装器。这些包装器负责基本类型转换、类型检查、错误处理和其他c++绑定的低级细节。这些包装器也足以将c++绑定到任何支持内置过程的目标语言中。在某种意义上,您可以将这一层包装看作是为c++提供了一个C库接口。在低级过程(扁平的)接口之上,SWIG生成代理类,这些代理类为底层代码提供了一个自然的面向对象(OO)接口。代理类通常是用目标语言本身编写的。例如,在Python中,一个真正的Python类用于为底层c++对象提供包装。

  需要强调的是,SWIG对c++包装采取了一种刻意保守和非侵入性的方法。SWIG不将c++类封装在一个特殊的c++适配器中,它不依赖模板,在生成包装器时也不添加额外的c++继承。大多数c++程序最不需要的就是更多的编译器魔法。因此,SWIG试图在c++应用程序的实现和生成的包装器代码之间保持非常严格和干净的分离。您可能会说,编写SWIG时遵循的原则是最简单的——它不会对c++类型系统耍花招,不会扰乱类层次结构,也不会引入新的语义。尽管这种方法可能不能提供与c++最无缝的集成,但它是安全的、简单的、可移植的和可调试的。

  本章的部分内容主要关注底层的c++过程接口,它被用作所有语言模块的基础。请记住,目标语言还通过代理类提供高级OO接口。在每种目标语言的文档中可以找到更详细的介绍。

6.3 Supported C++ features

  SWIG目前支持大多数c++特性,包括:

  • Classes
  • Constructors and destructors
  • Virtual functions
  • Public inheritance (including multiple inheritance)
  • Static functions
  • Function and method overloading
  • Operator overloading for many standard operators
  • References
  • Templates (including specialization and member templates)
  • Pointers to members
  • Namespaces
  • Default parameters
  • Smart pointers

  目前不支持以下c++特性:

  • Overloaded versions of certain operators (new, delete, etc.)

  根据经验,SWIG不应该用于原始的c++源文件,只使用头文件。

  SWIG的c++支持是一个正在进行的项目,因此其中一些限制可能会在未来的版本中取消。然而,我们不做任何承诺。另外,提交bug报告是修复问题的一个很好的方法。

6.4 Command line options and compilation

  当包装c++代码时,使用' -c++'选项调用SWIG是至关重要的。这改变了许多关键特性(如内存管理)的处理方式。它还支持识别c++关键字。如果没有-c++标志,当SWIG在接口文件中遇到c++代码时,它将发出警告或大量语法错误。

  在编译和链接生成的包装器文件时,通常使用c++编译器。例如:

$ swig -c++ -tcl example.i
$ c++ -fPIC -c example_wrap.cxx 
$ c++ example_wrap.o $(OBJS) -o example.so

  不幸的是,这个过程在每个平台上都略有不同。要了解更多详细信息,请确保参阅有关每种目标语言的文档。SWIG Wiki也有更多的细节。

  兼容性注意:早期版本的SWIG在默认情况下只生成一个扁平的低级C风格API到c++类。许多目标语言都能识别-noproxy命令行选项,它将像在早期版本中一样生成这个接口。

6.5 Proxy classes

  为了提供从c++类到目标语言类的自然映射,SWIG的目标语言大多使用特殊的代理类包装c++类。这些代理类通常在目标语言本身中实现。例如,如果您正在构建一个Python模块,那么每个c++类都由一个Python代理类包装。或者,如果您正在构建一个Java模块,那么每个c++类都由一个Java代理类包装。

6.5.1 Construction of proxy classes

  代理类总是构造为使用低级访问器函数的额外包装层。为了说明这一点,假设你有这样一个c++类:

class Foo {
  public:
    Foo();
    ~Foo();
    int  bar(int x);
    int  x;
};

  使用c++作为伪代码,一个代理类看起来像这样:

class FooProxy {
  private:
    Foo    *self;
  public:
    FooProxy() {
      self = new_Foo();
    }
    ~FooProxy() {
      delete_Foo(self);
    }
    int bar(int x) {
      return Foo_bar(self, x);
    }
    int x_get() {
      return Foo_x_get(self);
    }
    void x_set(int x) {
      Foo_x_set(self, x);
    }
};

  当然,一定要记住,真正的代理类是用目标语言编写的。例如,在Python中,代理看起来大概是这样的:

class Foo:
    def __init__(self):
        self.this = new_Foo()
    def __del__(self):
        delete_Foo(self.this)
    def bar(self, x):
        return Foo_bar(self.this, x)
    def __getattr__(self, name):
        if name == 'x':
            return Foo_x_get(self.this)
        ...
    def __setattr__(self, name, value):
        if name == 'x':
            Foo_x_set(self.this, value)
        ...

  再次强调,代理类总是使用低级访问器函数,这一点很重要。只要有可能,代理就会尝试利用类似于c++的语言特性。这可能包括操作符重载、异常处理和其他特性。

6.5.2 Resource management in proxies

  代理的一个主要问题涉及包装对象的内存管理。考虑下面的c++代码:

class Foo {
public:
  Foo();
  ~Foo();
  int bar(int x);
  int x;
};

class Spam {
public:
  Foo *value;
  ...
};

  考虑一些使用这些类的脚本代码:

f = Foo()               # Creates a new Foo
s = Spam()              # Creates a new Spam
s.value = f             # Stores a reference to f inside s
g = s.value             # Returns stored reference
g = 4                   # Reassign g to some other value
del f                   # Destroy f 

  现在,考虑一下由此产生的内存管理问题。在脚本中创建对象时,对象被新创建的代理类包装。也就是说,有一个新的代理类实例和一个基础c++类的新实例。在这个例子中,f和s都是这样创建的。然而,语句s.value相当奇怪——当执行时,一个指向f的指针被存储在另一个对象中。这意味着脚本代理类和另一个c++类共享对同一对象的引用。让事情变得更有趣的是,考虑语句g = s.value。执行时,这会创建一个新的代理类g,它为存储在s.value中的c++对象提供一个包装器。通常,无法知道这个对象来自何处——它可能是由脚本创建的,但也可能是内部生成的。在这个特殊的例子中,g的赋值导致f的第二个代理类。换句话说,f的引用现在由两个代理类和一个c++类共享。

  最后,考虑一下当对象被销毁时会发生什么。在语句g=4中,变量g被重新赋值。在许多语言中,这使得g的旧值可以用于垃圾收集。因此,这会导致一个代理类被销毁。随后,语句del f销毁了另一个代理类。当然,仍然有一个对存储在另一个c++对象中的原始对象的引用。它发生了什么?这个客体仍然有效吗

  为了处理内存管理问题,代理类提供了控制所有权的API。在c++伪代码中,所有权控制可能看起来大致如下:

class FooProxy {
  public:
    Foo    *self;
    int     thisown;

    FooProxy() {
      self = new_Foo();
      thisown = 1;       // Newly created object
    }
    ~FooProxy() {
      if (thisown) delete_Foo(self);
    }
    ...
    // Ownership control API
    void disown() {
      thisown = 0;
    }
    void acquire() {
      thisown = 1;
    }
};

class FooPtrProxy: public FooProxy {
public:
  FooPtrProxy(Foo *s) {
    self = s;
    thisown = 0;
  }
};

class SpamProxy {
  ...
  FooProxy *value_get() {
    return FooPtrProxy(Spam_value_get(self));
  }
  void value_set(FooProxy *v) {
    Spam_value_set(self, v->self);
    v->disown();
  }
  ...
};

  看看这段代码,有几个中心特性:

  • 每个代理类都保留一个额外的标志来表示所有权。只有设置了所有权标志,c++对象才会被销毁。
  • 在目标语言中创建新对象时,设置所有权标志。
  • 当一个对内部c++对象的引用被返回时,它被一个代理类包装,但是代理类没有所有权。
  • 在某些情况下,所有权进行了调整。例如,当一个值被赋给类的成员时,所有权就会丢失。
  • 手工所有权控制由特殊的disown()和acquire()方法提供。

  鉴于c++内存管理的棘手本质,代理类不可能自动处理所有可能的内存管理问题。但是,代理确实提供了一种手动控制机制,可以(如果必要的话)使用这种机制来解决一些比较棘手的内存管理问题。

6.5.3 Language specific details

  关于代理类的语言特定细节包含在描述每种目标语言的章节中。这一章只是很笼统地介绍了这个问题。

6.6 Simple C++ wrapping

  下面的代码显示了一个简单c++类的SWIG接口文件。

%module list
%{
#include "list.h"
%}

// Very simple C++ example for linked list

class List {
public:
  List();
  ~List();
  int  search(char *value);
  void insert(char *);
  void remove(char *);
  char *get(int n);
  int  length;
static void print(List *l);
};

  为了为该类生成包装器,SWIG首先将该类减少为一组低级c风格的访问器函数,然后由代理类使用这些函数。

6.6.1 Constructors and destructors

  c++的构造函数和析构函数被翻译成如下的访问函数:

List * new_List(void) {
  return new List;
}
void delete_List(List *l) {
  delete l;
}

6.6.2 Default constructors, copy constructors and implicit destructors

  遵循c++的隐式构造函数和析构函数规则,即使在类接口中没有显式声明,SWIG也会自动假定存在一个隐式构造函数和析构函数。

  一般来说:

  •   如果c++类没有声明任何显式构造函数,SWIG将自动为该类生成一个包装器。
  •   如果一个c++类没有声明显式的复制构造函数,如果使用了%copyctor, SWIG将自动为它生成一个包装器。
  •   如果c++类没有声明显式析构函数,SWIG将自动为该类生成一个包装器。

  而在c++中,一些规则改变了之前的行为:

  •   如果类已经定义了带参数的构造函数,则不会创建默认构造函数。
  •   默认构造函数不会为具有纯虚方法的类或从抽象类继承的类生成,但不会为所有纯方法提供定义。
  •   除非所有基类都支持默认构造函数,否则不会创建默认构造函数。
  •   如果类在私有或受保护的部分中定义了默认构造函数和隐式析构函数,则不会创建它们。
  •   如果任何基类定义了非公共的默认构造函数或析构函数,则不会创建默认构造函数和隐式析构函数。

  SWIG不应该为不合法的类生成默认构造函数、复制构造函数或默认析构包装器。然而,在某些情况下,手动禁用隐式构造函数/析构函数生成可能是必要的(如果在SWIG中看不到完整的类声明,并且违反了上述规则之一)或需要的(为了减少最终接口的大小)。

  要手动禁用这些功能,可以使用% nodefaulttor和% nodefaulttor特性标志指令。请注意,这些指令只影响隐式生成,如果在类接口中显式声明默认/复制构造函数或析构函数,则它们不起作用。

  例如:

%nodefaultctor Foo;  // Disable the default constructor for class Foo.
class Foo {          // No default constructor is generated, unless one is declared
...
};
class Bar {          // A default constructor is generated, if possible
...
};

  指令% nodefaulttor也可以“全局”应用,如:

%nodefaultctor; // Disable creation of default constructors
class Foo {     // No default constructor is generated, unless one is declared
...
};
class Bar {   
public:
  Bar();        // The default constructor is generated, since one is declared
};
%clearnodefaultctor; // Enable the creation of default constructors again

  如果需要,可以使用相应的% nodefaultdtor指令禁用默认或隐式析构函数的生成。但是要注意,这可能导致目标语言中的内存泄漏。因此,建议只在众所周知的情况下使用这个指令。例如:

%nodefaultdtor Foo;   // Disable the implicit/default destructor for class Foo.
class Foo {           // No destructor is generated, unless one is declared
...
};

  兼容性注意:生成默认构造函数/隐式析构函数是SWIG 1.3.7中的默认行为。这可能会破坏某些旧模块,但是使用%nodefault或-nodefault命令行选项可以很容易地恢复旧的行为。此外,为了让SWIG正确地生成(或不生成)默认构造函数,它必须能够从私有部分和受保护部分收集信息(具体来说,它需要知道是否定义了私有或受保护的构造函数/析构函数)。在较旧的SWIG版本中,由于解析器的限制,通常会简单地删除或注释掉类的私有和受保护部分。但是,这种删除现在可能会导致SWIG错误地为在这些部分中定义构造函数的类生成构造函数。考虑在接口中恢复这些部分,或者使用%nodefault来修复这个问题。

  注意:上面描述的%nodefault指令/-nodefault选项禁用了默认构造函数和隐式析构函数,可能会导致内存泄漏,因此强烈建议不要使用它们。

6.6.3 When constructor wrappers aren't created

  如果一个类定义了构造函数,SWIG通常会尝试为它生成一个包装器。但是,如果SWIG认为构造函数包装器将导致非法包装器代码,那么它将不会生成构造函数包装器。有两种情况会出现这种情况。

  首先,SWIG不会为受保护构造函数或私有构造函数生成包装器。例如:

class Foo {
protected:
  Foo();         // Not wrapped.
public:
  ...
};

  其次,如果类看起来是抽象的,SWIG将不会为该类生成包装器——也就是说,它具有未定义的纯虚方法。下面是一些例子:

class Bar {
public:
  Bar();               // Not wrapped.  Bar is abstract.
  virtual void spam(void) = 0;
};

class Grok : public Bar {
public:
  Grok();              // Not wrapped. No implementation of abstract spam().
};

  一些用户惊讶(或困惑)地发现他们的接口中缺少构造函数包装。几乎在所有情况下,当类被确定为抽象类时,都会导致这种情况。要查看情况是否如此,运行SWIG并打开所有的警告:

% swig -Wall -python module.i

  

 

6.18 Templates

  模板类型名称可以出现在接口文件中需要类型的任何地方。例如:

void foo(vector<int> *a, int n);
void bar(list<int, 100> *x);

  对于非类型参数的使用有一些限制。它支持简单的文字,也支持一些常量表达式。但是,SWIG目前不支持在常量表达式中使用'<'和'>'(但支持'<='和'>=')。例如:

void bar(list<int, 100> *x);                // OK
void bar(list<int, 2*50> *x);               // OK
void bar(list<int, (2>1 ? 100 : 50)> *x)    // Not supported

  类型系统足够聪明,可以找出您可能尝试使用typedef进行的聪明游戏。例如,考虑以下代码:

typedef int Integer;
void foo(vector<int> *x, vector<Integer> *y);

  在本例中,vector<Integer>与vector<int>的类型完全相同。foo()的包装器将接受任意一种变体。

6.18.1 The %template directive

  关于模板包装,有几点很重要。首先,一个纯c++模板没有定义任何类型的可运行对象代码,SWIG通常可以为其创建包装器的。因此,为了包装模板,您需要向SWIG提供关于特定模板实例化的信息(例如,vector<int>, array<double>,等等)。其次,像vector<int>这样的实例化名称在大多数目标语言中通常不是有效的标识符名称。因此,您需要给模板实例化一个更合适的名称,比如int-vector。

  为了说明这一点,考虑以下类模板定义:

template<class T> class List {
private:
  T *data;
  int nitems;
  int maxitems;
public:
  List(int max) {
    data = new T [max];
    nitems = 0;
    maxitems = max;
  }
  ~List() {
    delete [] data;
  };
  void append(T obj) {
    if (nitems < maxitems) {
      data[nitems++] = obj;
    }
  }
  int length() {
    return nitems;
  }
  T get(int n) {
    return data[n];
  }
};

  这个类模板本身是无用的——SWIG会忽略它,因为除非提供T的定义,否则它不知道如何生成任何代码。%template指令需要实例化模板以便在目标语言中使用。该指令需要一个标识符名,以便在目标语言中使用,再加上用于实例化的模板。下面的例子实例化List<int>作为一个名为intList的类:

%template(intList) List<int>;

  实例化像c++编译器所做的那样扩展模板代码,然后在给定的标识符名称下使其可用。本质上,它与包装以下概念代码相同,类模板定义将T扩展为int(注意,这不是完全有效的语法):

%rename(intList) List<int>;       // Rename to a suitable identifier
class List<int> {
private:
  int *data;
  int nitems;
  int maxitems;
public:
  List(int max);
  ~List();
  void append(int obj);
  int length();
  int get(int n);
};

  %template()的参数是目标语言中实例化的名称。您选择的名称不应该与接口文件中的任何其他声明冲突,只有一个例外——模板名称可以与typedef声明的名称相匹配。例如:

%template(intList) List<int>;
...
typedef List<int> intList;    // OK

  %template指令必须总是出现在待扩展模板的定义之后,如下就可以:

template<class T> class List { ... };
%template(intList) List<int>;

  但是如果%template在模板定义之前使用,例如:

%template(intList) List<int>;
template<class T> class List { ... };

  SWIG将生成一个错误:

example.i:3: Error: Template 'List' undefined.

  因为类型系统知道如何处理typedef,所以通常没有必要为等价的类型名实例化模板的不同版本。例如,考虑以下代码:

%template(intList) List<int>;
typedef int Integer;
...
void foo(List<Integer> *x);

  在本例中,List<Integer>与List<int>的类型完全相同。List<Integer>的任何使用都被映射回之前创建的List<int>的实例化。因此,没有必要实例化Integer类型的新类(这样做是多余的,只会导致代码膨胀)。

  提供给%template用于实例化的模板必须是实际的模板,而不是模板的类型定义。

typedef List<int> ListOfInt;

%template(intList) List<int>; // ok
%template(intList) ListOfInt; // illegal - Syntax error

6.18.2 Function templates

  SWIG还可以使用与上述类模板相似的技术为函数模板生成包装器。例如:

// Function template
template<class T> T max(T a, T b) { return a > b ? a : b; }

// Make some different versions of this function
%template(maxint) max<int>;
%template(maxdouble) max<double>;

  在本例中,maxint和maxdouble成为函数特定实例的唯一名称。

  SWIG甚至支持重载模板函数。和通常一样,%template指令被用来包装模板函数。例如:

template<class T> void foo(T x) { };
template<class T> void foo(T x, T y) { };

%template(foo) foo<int>;

  这将生成两个重载的包装器方法,第一个将接受一个整数作为参数,第二个将接受两个整数参数。

6.18.3 Default template arguments

  提供给%template的实参数量应该与原始模板定义中的一致。支持模板默认参数。例如:

template vector<typename T, int max=100> class vector {
...
};

%template(intvec) vector<int>;           // OK
%template(vec1000) vector<int, 1000>;     // OK

  %template指令不能在同一个作用域中多次包装同一个模板实例。这会产生一个错误。例如:

%template(intList) List<int>;
%template(Listint) List<int>;    // Error.   Template already wrapped.

  导致此错误的原因是模板展开会导致两个具有相同名称的相同类。这会产生符号表冲突。此外,为了减少代码膨胀的可能性,只包装一次特定实例可能更有效。

6.18.4 Template base classes

  当使用%template实例化模板时,有关该类的信息将由SWIG保存并在程序的其他地方使用。例如,如果你这样写代码,

...
%template(intList) List<int>;
...
class UltraList : public List<int> {
  ...
};

  那么SWIG知道List<int>已经包装为一个名为intList的类,并安排正确地处理继承。另一方面,如果对List<int>一无所知,您将得到类似于这样的警告消息:

example.h:42: Warning 401. Nothing known about class 'List< int >'. Ignored. 
example.h:42: Warning 401. Maybe you forgot to instantiate 'List< int >' using %template. 

  如果类模板继承自另一个类模板,则需要确保基类在派生类之前实例化。例如:

template<class T> class Foo {
...
};

template<class T> class Bar : public Foo<T> {
...
};

// Instantiate base classes first 
%template(intFoo) Foo<int>;
%template(doubleFoo) Foo<double>;

// Now instantiate derived classes
%template(intBar) Bar<int>;
%template(doubleBar) Bar<double>;

  顺序很重要,因为SWIG使用实例化名称在结果包装器代码中正确地设置继承层次结构(基类需要在派生类之前包装)。不要担心——如果顺序错误,SWIG应该生成一条警告消息。

  偶尔,您可能需要告诉SWIG关于由模板定义但不应该包装的基类。由于SWIG不能为此目的自动实例化模板,因此必须手动进行。为此,只需使用空模板实例化,即没有名称的%template。例如:

// Instantiate traits<double, double>, but don't wrap it.
%template() traits<double, double>;

  如果您必须为许多不同类型实例化许多不同的类,您可能会考虑编写一个SWIG宏。例如:

%define TEMPLATE_WRAP(prefix, T...) 
%template(prefix ## Foo) Foo<T >;
%template(prefix ## Bar) Bar<T >;
...
%enddef

TEMPLATE_WRAP(int, int)
TEMPLATE_WRAP(double, double)
TEMPLATE_WRAP(String, char *)
TEMPLATE_WRAP(PairStringInt, std::pair<string, int>)
...

  请注意,对于类型t,使用了一个可变参数宏。如果没有使用这个宏,那么上一个示例中的模板类型中的逗号就不可能出现。

6.18.5 Template specialization

  SWIG模板机制确实支持专门化。例如,如果你这样定义一个类,

template<> class List<int> {
private:
  int *data;
  int nitems;
  int maxitems;
public:
  List(int max);
  ~List();
  void append(int obj);
  int length();
  int get(int n);
};

  那么当用户展开List<int>时,SWIG将使用此代码。在实践中,这可能对底层包装器代码的影响很小,因为专门化通常用于提供稍加修改的方法体(SWIG会忽略这些方法体)。但是,可以将特殊的SWIG指令(如%typemap、%extend等)附加到专门化,以便为特定类型提供定制。

  部分模板专门化由SWIG部分支持。例如,这段代码定义了当模板参数是指针时应用的模板。

template<class T> class List<T*> {
private:
  T *data;
  int nitems;
  int maxitems;
public:
  List(int max);
  ~List();
  void append(T obj);
  int length();
  T get(int n);
};

  SWIG同时支持模板显式专门化和部分专门化。考虑:

template<class T1, class T2> class Foo { };                     // (1) primary template
template<>                   class Foo<double *, int *> { };    // (2) explicit specialization
template<class T1, class T2> class Foo<T1, T2 *> { };           // (3) partial specialization

  SWIG能够正确匹配显式实例化:

Foo<double *, int *>     // explicit specialization matching (2)

  SWIG实现了模板参数演绎,因此下面的部分专门化示例就像在c++编译器中一样工作:

Foo<int *, int *>        // partial specialization matching (3)
Foo<int *, const int *>  // partial specialization matching (3)
Foo<int *, int **>       // partial specialization matching (3)

  

 

posted @ 2021-11-29 23:29  神龙逗勇士  阅读(140)  评论(0编辑  收藏  举报