SWIG学习记录(二)SWIG实用性基础

1 SWIG实用基础

1.1 值传递结构体

有时,C函数接受按值传递的结构参数。例如,考虑以下函数:

double dot_product(Vector a, Vector b);

为了解决这个问题,SWIG将函数转换为使用指针,方法是创建一个等价于以下内容的包装器:

double wrap_dot_product(Vector *a, Vector *b) {
  Vector x = *a;
  Vector y = *b;
  return dot_product(x, y);
}

在目标语言中,dot_product()函数现在接受指向vector的指针,而不是vector。

1.2 返回值

按值返回结构体或类数据类型的C函数更难处理。考虑以下函数:

Vector cross_product(Vector v1, Vector v2);

函数想返回Vector,但是SWIG实际只支持指针。因此SWIG创建一个包装器:

Vector *wrap_cross_product(Vector *v1, Vector *v2) {
  Vector x = *v1;
  Vector y = *v2;
  Vector *result;
  result = (Vector *) malloc(sizeof(Vector));
  *(result) = cross(x, y);
  return result;
}

SWIG分配一个新对象并返回对该对象的引用。需要注意指针内存的释放问题。
还应该注意的是,在c++中按值处理传递/返回有一些特殊情况。例如,如果Vector没有定义默认构造函数,上述代码片段就无法正常工作。

1.3 链接到结构体变量

当遇到涉及结构的全局变量或类成员时,SWIG将它们作为指针处理。
c++类必须提供正确定义的复制构造函数,以便赋值能够正确地工作。

1.4 链接到char*

当出现char *类型的全局变量时,SWIG使用malloc()或new为新值分配内存。
如果不想分配新值内存,可以考虑使用%immutable指令使变量只读。或者,您也可以编写一个简短的辅助函数来完全按照您希望的那样设置值。例如:

%inline %{
  void set_foo(char *value) {
    strncpy(foo, value, 50);
  }
%}

使用char *变量的一个常见错误是链接到这样声明的变量:

char *VERSION = "1.0";

在这种情况下,变量是可读的,但是任何更改值的尝试都会导致分段或一般保护故障。这是因为SWIG试图使用free或delete释放旧值,而当前分配给变量的字符串文字值没有使用malloc()或new分配。要修复此行为,您可以将变量标记为只读,编写typemap,或编写一个特殊的set函数,如所示。另一种方法是将变量声明为数组:

char VERSION[64] = "1.0";

1.5 数组

数组被SWIG完全支持,但它们总是作为指针处理,而不是将它们映射到目标语言中的特殊数组对象或列表。因此下列声明都被处理为指针。

int foobar(int a[40]);
void grok(char *argv[]);
void transpose(double a[20][20]);

多维数组被转换为指向低一维数组的指针。例如:

int [10];         // Maps to int *
int [10][20];     // Maps to int (*)[20]
int [10][20][30]; // Maps to int (*)[20][30]

SWIG支持数组变量,在默认情况下是只读的。例如:

int   a[100][200];

在本例中,读取变量’a’将返回一个int( * ) [200] 类型的指针,该指针指向数组的第一个元素&[0][0]。试图修改’a’会导致错误。这是因为SWIG不知道如何将数据从目标语言复制到数组中。为了解决这个限制,你可以像这样编写一些简单的辅助函数:

%inline %{
void a_set(int i, int j, int val) {
  a[i][j] = val;
}
int a_get(int i, int j) {
  return a[i][j];
}
%}

要动态创建不同大小和形状的数组,在接口中编写一些辅助函数可能会很有用。例如:

// Some array helpers
%inline %{
  /* Create any sort of [size] array */
  int *int_array(int size) {
    return (int *) malloc(size*sizeof(int));
  }
  /* Create a two-dimension array [size][10] */
  int (*int_array_10(int size))[10] {
    return (int (*)[10]) malloc(size*10*sizeof(int));
  }
%}

字符数组是SWIG作为特殊情况处理的。在这种情况下,目标语言中的字符串可以存储在数组中。例如,如果你有这样一个声明,

char pathname[256];

SWIG生成用于获取和设置值的函数,其等价于以下代码:

char *pathname_get() {
  return pathname;
}
void pathname_set(char *value) {
  strncpy(pathname, value, 256);
}

在目标语言中,可以像设置普通变量一样设置该值。

1.6 创建只读变量

使用 %immutable 指令来创建只读变量。如下:

// File : interface.i

int a;       // Can read/write
%immutable;
int b, c, d;   // Read only variables
%mutable;
double x, y;  // read/write

%immutable 指令启用只读模式,直到使用%mutable指令显式禁用它为止。除了像这样关闭或打开只读模式外,还可以将单个声明标记为只读。例如:

%immutable x;                   // Make x read-only

%mutable和%immutable指令实际上是%feature指令,定义如下:

#define %immutable   %feature("immutable")
#define %mutable     %feature("immutable", "")

如果你想让所有被包装的变量都是只读的(除了一个或两个),采用这种方法可能更容易:

%immutable;                     // Make all variables read-only
%feature("immutable", "0") x;   // except, make x read/write
...
double x;
double y;
double z;
...

当声明为const时,也会创建只读变量。

1.7 重命名和忽略声明

1.7.1 特定标识符的简单重命名

通常,在将C声明包装到目标语言中时,会使用该声明的名称。但是,这可能会与脚本语言中的关键字或现有函数产生冲突。要解决名称冲突,可以使用%rename指令,如下所示:

// interface.i

%rename(my_print) print;
extern void print(const char *);

%rename(foo) a_really_long_and_annoying_name;
extern int a_really_long_and_annoying_name;

在本例中,函数print()在目标语言中实际上被称为“my_print()”。
%rename指令位置是任意的,只要它出现在要重命名的声明之前。一种常见的技术是编写这样包装头文件的代码:

// interface.i

%rename(my_print) print;
%rename(foo) a_really_long_and_annoying_name;

%include "header.h"

rename对未来出现的所有名称应用重命名操作。重命名适用于函数、变量、类和结构名、成员函数和成员数据。例如,如果你有24个c++类,它们都有一个名为“print”的成员函数(这是Python中的一个关键字),你可以通过以下命令将它们全部重命名为“output”:

%rename(output) print; // Rename all `print' functions to `output'
1.7.2 重命名和歧义解决

如果重载解析中出现了歧义,或者某个模块不允许重载,那么可以使用一些策略来处理这个问题。首先,您可以告诉SWIG忽略其中一个方法。使用 %ignore指令。

%ignore foo(long);
void foo(int);
void foo(long);       // Ignored.

另一种方法是重命名其中一个方法。这可以使用%rename来完成。例如:

%rename("foo_short") foo(short);
%rename(foo_long) foo(long);

void foo(int);
void foo(short);      // Accessed as foo_short()
void foo(long);       // Accessed as foo_long()

注意,新名称周围的引号是可选的,但是,如果新名称是C/ c++关键字,它们将是必要的,以避免解析错误。%ignore和%rename指令在匹配声明方面都非常强大。当以简单的形式使用时,它们可应用于全局函数和方法。

/* Forward renaming declarations */
%rename(foo_i) foo(int); 
%rename(foo_d) foo(double);
...
void foo(int);           // Becomes 'foo_i'
void foo(char *c);       // Stays 'foo' (not renamed)

class Spam {
public:
  void foo(int);      // Becomes 'foo_i'
  void foo(double);   // Becomes 'foo_d'
  ...
};

如果您只想将重命名应用于某个作用域,可以使用c++作用域解析操作符(:😃。例如:

%rename(foo_i) ::foo(int);      // Only rename foo(int) in the global scope.
                                // (will not rename class members)

%rename(foo_i) Spam::foo(int);  // Only rename foo(int) in class Spam

当重命名操作符像Spam::foo(int)中那样应用于一个类时,它将应用于该类和所有派生类。这可以用于在整个类层次结构中应用一致的重命名。例如:

%rename(foo_i) Spam::foo(int);
%rename(foo_d) Spam::foo(double);

class Spam {
public:
  virtual void foo(int);      // Renamed to foo_i
  virtual void foo(double);   // Renamed to foo_d
  ...
};

class Bar : public Spam {
public:
  virtual void foo(int);      // Renamed to foo_i
  virtual void foo(double);   // Renamed to foo_d
  ...
};

class Grok : public Bar {
public:
  virtual void foo(int);      // Renamed to foo_i
  virtual void foo(double);   // Renamed to foo_d
  ...
};

可以使用特殊形式的%rename对(所有类的)类成员进行重命名:

%rename(foo_i) *::foo(int);   // Only rename foo(int) if it appears in a class.

'*'是一个匹配任何类名的通配符。
虽然本节讨论主要集中在%rename上,但所有相同的规则也适用于%ignore。例如:

%ignore foo(double);          // Ignore all foo(double)
%ignore Spam::foo;            // Ignore foo in class Spam
%ignore Spam::foo(double);    // Ignore foo(double) in class Spam
%ignore *::foo(double);       // Ignore foo(double) in all classes

当应用于基类时,%ignore将强制派生类中的所有定义消失。例如,%ignore Spam::foo(double)将在Spam和所有派生自Spam的类中消除foo(double)。

1.7.3 在使用%rename和%ignore指令需注意
  • 由于%rename声明用于提前声明重命名,所以可以将它放在接口文件的开头。这使得无需修改头文件就可以应用一致的名称解析成为可能。
  • 作用域限定符(::)也可以用于简单的名称。
%rename(bar) ::foo;       // Rename foo to bar in global scope only
%rename(bar) Spam::foo;   // Rename foo to bar in class Spam only
%rename(bar) *::foo;      // Rename foo in classes only
  • 名称匹配尝试查找所定义的最特定的匹配。像Spam::foo这样的限定名总是比非限定名foo具有更高的优先级。Spam::foo的优先级高于*::foo, *::foo的优先级高于foo。在同一作用域级别中,参数化名称的优先级高于未参数化名称。但是,带有作用域限定符的未参数化名称在全局作用域中的优先级高于参数化名称(例如,Spam::foo的重命名优先于foo(int)的重命名)。
  • %rename指令定义的顺序并不重要,只要它们出现在要重命名的声明之前。如果提供了完全相同的名称、作用域和参数,重复的%rename指令将更改前面%rename指令的设置。
  • 名称匹配规则严格遵循成员限定符规则。例如,如果你有一个类和成员,其成员是const限定的,像这样:
class Spam {
public:
  ...
  void bar() const;
  ...
};

声明没有const限定符,则不起作用,如:

%rename(name) Spam::bar();  //不能匹配
%rename(name) Spam::bar() const; // 可以匹配

必须指定所有限定符才能正确匹配

%rename(name) Jam::bar();          // will not match
%rename(name) Jam::bar() &;        // will not match
%rename(name) Jam::bar() const;    // will not match
%rename(name) Jam::bar() const &;  // ok, will match
  • 目前没有执行匹配函数参数的解析。这意味着函数形参类型必须精确匹配。例如,命名空间限定符和类型定义将不起作用。typedefs的以下用法说明了这一点:
typedef int Integer;

%rename(foo_i) foo(int);

class Spam {
public:
  void foo(Integer);  // Stays 'foo' (not renamed)
};
class Ham {
public:
  void foo(int);      // Renamed to foo_i
};

在包装具有默认参数的方法时,名称匹配规则也使用默认参数来进行更精细的控制。

%rename(newbar) Spam::bar(int i=-1, double d=0.0);

如果不带默认参数,形如:

%rename(newbar) Spam::bar(int i, double d);

这样只有完全匹配的两个参数的接口才会匹配

void Spam::newbar(int i, double d);
void Spam::bar(int i);
void Spam::bar();

事实上,可以在等效的重载方法上使用%rename来重命名所有等效的重载方法:

%rename(bar_2args)   Spam::bar(int i, double d);
%rename(bar_1arg)    Spam::bar(int i);
%rename(bar_default) Spam::bar();
1.7.4 高级重命名支持

可能需要对目标语言中的所有名称应用一些转换,以便更好地遵循其命名约定,比如为所有包装的函数添加特定的前缀。对每个函数单独进行重命名是不切实际的,因此SWIG支持对所有声明应用重命名规则,如果要重命名的标识符的名称没有指定:

%rename("myprefix_%s") ""; // print -> myprefix_print

这还表明%rename的参数不必是文字字符串,但可以是类似printf()的格式字符串。在最简单的形式中,“%s”被替换为原始声明的名称,如上所示。
SWIG提供了对通常格式字符串语法的扩展,以允许对参数应用(swg定义的)函数。例如,要将所有C函数do_something_long()包装成更像java的doSomethingLong(),你可以使用“lowercamelcase”扩展格式说明符,如下所示:

%rename("%(lowercamelcase)s") ""; // foo_bar -> fooBar; FooBar -> fooBar

有些函数可以参数化,例如“strip”函数从其参数中去掉所提供的前缀。前缀被指定为格式字符串的一部分,在函数名之后的冒号之后,如下:

%rename("%(strip:[wx])s") ""; // wxHello -> Hello; FooBar -> FooBar

下表总结了所有当前定义的函数,并给出了应用每个函数的示例。注意,其中一些函数有两个名称,一个较短,一个描述性更强,但这两个函数在其他方面是等价的:
在这里插入图片描述

1.7.5 限制全局重命名规则

%rename指令可以重命名单个声明,也可以一次对所有声明应用重命名规则。可以使用后续匹配参数限制未命名%rename的作用域。它们可以应用于SWIG与其输入中出现的声明相关联的任何属性。例如:

%rename("foo", match$name="bar") "";
//或
%rename("foo") bar;

match也可以应用于声明类型,例如match="class"将匹配限制为只匹配类声明(在c++中),而match="enumitem"将匹配限制为枚举元素。例如,SWIG还为这种匹配表达式提供了方便的宏:

%rename("%(title)s", %$isenumitem) "";

将所有枚举元素的名称大写,但不改变其他声明的大小写。类似地,%$isclass、% $isfunction、% $isconstructor、% $isunion、% $ istemplate和 % $ isvariable都可以使用。

1.7.6 忽略所有内容,有选择地包装符号

可以忽略头文件中的所有内容,然后有选择地包装一些选定的方法或类。例如,考虑一个头文件,myheader.h,它包含许多类,并且在这个头文件中只需要一个名为Star的类,可以采用以下方法:

%ignore ""; // Ignore everything

// Unignore chosen class 'Star'
%rename("%s") Star;

// As the ignore everything will include the constructor, destructor, methods etc
// in the class, these have to be explicitly unignored too:
%rename("%s") Star::Star;
%rename("%s") Star::~Star;
%rename("%s") Star::shine; // named method

%include "myheader.h"

另一种方法可能更合适,因为它不需要命名所选类中的所有方法,那就是从忽略类开始。这不会向类的任何成员添加显式忽略,所以当所选的类不被忽略时,它的所有方法都将被包装。

%rename($ignore, %$isclass) ""; // Only ignore all classes
%rename("%s") Star; // Unignore 'Star'
%include "myheader.h"

1.8 默认/可选参数

SWIG在C和c++代码中都支持默认参数。例如:

int plot(double x, double y, int color=WHITE);

在这种情况下,SWIG生成包装器代码,其中默认参数在目标语言中是可选的。

1.9 指向函数和回调的指针

C库可能包含期望接收指向函数的指针的函数——可能用作回调函数。当回调函数是用C而不是用目标语言定义时,SWIG提供了对函数指针的完全支持。例如,考虑这样一个函数:

int binary_op(int a, int b, int (*op)(int, int));

当您第一次将此类内容封装到扩展模块中时,您可能会发现该函数无法使用。这个错误的原因是SWIG不知道如何将脚本语言函数映射到C回调。但是,如果将现有的C函数作为常量安装,则可以将它们用作参数。一种方法是像这样使用%constant指令:

/* Function with a callback */
int binary_op(int a, int b, int (*op)(int, int));

/* Some callback functions */
%constant int add(int, int);
%constant int sub(int, int);
%constant int mul(int, int);

不幸的是,通过将回调函数声明为常量,它们就不再可以作为函数访问了。
如果你想让一个函数同时是回调函数和函数,你可以像这样使用%callback和%nocallback指令:

/* Function with a callback */
int binary_op(int a, int b, int (*op)(int, int));

/* Some callback functions */
%callback("%s_cb");
int add(int, int);
int sub(int, int);
int mul(int, int);
%nocallback;

%callback的参数是一个printf样式的格式字符串,它指定了回调常量的命名约定(%s将被函数名替换)。回调模式一直有效,直到使用%nocallback显式禁用它。当你这样做时,界面现在工作如下:

>>> binary_op(3, 4, add_cb)
7
>>> binary_op(3, 4, mul_cb)
12
>>> add(3, 4)
7
>>> mul(3, 4)
12

注意,当函数用作回调时,会使用特殊的名称,如add_cb。要正常调用函数,只需使用原始函数名,如add()。

2 结构体和联合体

如果SWIG遇到结构或联合的定义,它将创建一组访问器函数。尽管SWIG不需要结构定义来构建接口,但提供定义使访问结构成员成为可能。SWIG生成的访问器函数只接受一个指向对象的指针,并允许访问单个成员。
除此之外,SWIG会默认创建构造函数和析构函数,如果接口中没有定义。

2.1 Typedef和结构体

SWIG支持以下结构,这在C程序中很常见:

typedef struct {
  double x, y, z;
} Vector;

遇到这种情况时,SWIG假设对象的名称是“Vector”,并像以前一样创建访问器函数。唯一的区别是使用typedef允许SWIG在其生成的代码上删除struct关键字。

2.2 字符串和结构体

涉及字符串的结构需要谨慎处理。SWIG假设char *类型的所有成员都已使用malloc()动态分配,并且它们是以null结尾的ASCII字符串。当这样的成员被修改时,以前的内容将被释放,新的内容将被分配。例如:

%module mymodule
...
struct Foo {
  char *name;
  ...
}

这将产生以下访问器函数:

char *Foo_name_get(Foo *obj) {
  return Foo->name;
}

char *Foo_name_set(Foo *obj, char *c) {
  if (obj->name)
    free(obj->name);
  obj->name = (char *) malloc(strlen(c)+1);
  strcpy(obj->name, c);
  return obj->name;
}
2.3 数组成员

数组可以作为结构的成员出现,但它们将是只读的。SWIG将编写一个访问器函数来返回指向数组第一个元素的指针,但不会编写一个函数来更改数组本身的内容。

2.4 结构体数据成员

当一个结构体成员被包装时,它被作为指针处理,并生成相应的包装指针的访问器代码。
需要注意的是,只有当SWIG知道数据成员是结构或类时,才会发生向指针的转换。

2.5 C构造函数和析构函数

如果不希望SWIG为接口生成默认构造函数,可以使用%nodefaultctor指令或-nodefaultctor命令行选项。例如:

swig -nodefaultctor example.i 

%module foo
...
%nodefaultctor;        // Don't create default constructors
... declarations ...
%clearnodefaultctor;   // Re-enable default constructors

由于忽略隐式或默认析构函数大多数时候会产生内存泄漏,因此SWIG总是尝试生成它们。但是,如果需要,您可以使用%nodefaultdtor有选择地禁用默认/隐式析构函数的生成。

%nodefaultdtor Foo; // No default/implicit destructor for Foo
2.6 在C结构体中添加成员函数

SWIG提供了一个特殊的%extend指令,可以将方法附加到C结构体中,以构建面向对象的接口。假设你有一个C头文件,声明如下:

/* file : vector.h */
...
typedef struct Vector {
  double x, y, z;
} Vector;

你可以通过像这样编写一个SWIG接口来让Vector看起来像一个类:

// file : vector.i
%module mymodule
%{
#include "vector.h"
%}

%include "vector.h"          // Just grab original C header file
%extend Vector {             // Attach these functions to struct Vector
  Vector(double x, double y, double z) {
    Vector *v;
    v = (Vector *) malloc(sizeof(Vector));
    v->x = x;
    v->y = y;
    v->z = z;
    return v;
  }
  ~Vector() {
    free($self);
  }
  double magnitude() {
    return sqrt($self->x*$self->x+$self->y*$self->y+$self->z*$self->z);
  }
  void print() {
    printf("Vector [%g, %g, %g]\n", $self->x, $self->y, $self->z);
  }
};

注意$self特殊变量的用法。它的用法与c++ 'this’指针相同,应该在需要访问结构实例时使用。
与普通的c++构造函数实现有一个细微的区别,新构造的对象必须返回一个对象类型指针。

%extend指令也可以在Vector结构的定义中使用。例如:

// file : vector.i
%module mymodule
%{
#include "vector.h"
%}

typedef struct Vector {
  double x, y, z;
  %extend {
    Vector(double x, double y, double z) { ... }
    ~Vector() { ... }
    ...
  }
} Vector;

用于%extend的名称应该是该结构的名称,而不是该结构的任何类型定义的名称。例如:

typedef struct Integer {
  int value;
} Int;
%extend Integer { ...  } /* Correct name */
%extend Int { ...  } /* Incorrect name */

这个规则有一个例外,那就是当结构匿名命名时,例如:

typedef struct {
  double value;
} Double;
%extend Double { ...  } /* Okay */
2.7 嵌套结构体

当SWIG遇到嵌套结构体时,它会执行一个结构体拆分操作,将声明转换成等价的内容。

typedef struct Object {
  int objtype;
  union {
    int ivalue;
    double dvalue;
    char *strvalue;
    void *ptrvalue;
  } intRep;
} Object;

转换为

typedef union {
  int ivalue;
  double dvalue;
  char *strvalue;
  void *ptrvalue;
} Object_intRep;

typedef struct Object {
  int objType;
  Object_intRep intRep;
} Object;

3 代码插入

3.1 SWIG输出

当SWIG创建它的输出C/ c++文件时,它被分成五个部分,分别对应于运行时代码、头文件、包装器函数和模块初始化代码(按此顺序)。

  • Begin section. 用户将代码放在C/ c++包装器文件开头的占位符。通常用于定义预处理器宏。
  • Runtime code. 此代码是SWIG的内部代码,用于包含类型检查和模块其余部分使用的其他支持函数。
  • Header section. 这是用户定义的支持代码,包含在%{…%}指令。通常这包含头文件和其他助手函数。
  • Wrapper code. 这些是由SWIG自动生成的包装器。
  • Module initialization. SWIG生成的函数,用于在加载时初始化模块。
3.2 代码插入块

%insert指令允许将代码块插入到生成的代码的给定部分。它有两种用法:

%insert("section") "filename"
%insert("section") %{ ... %}

第一个将把给定文件名中的文件内容转储到指定的section中。第二个函数将代码插入大括号之间的指定section。例如,下面的代码将代码添加到runtime section:

%insert("runtime") %{
  ... code in runtime section ...
%}

%{…%}指令是一个快捷方式,与%header %{…%}作用相同。
如下用法与%insert(“section”) %{ … %}等价。

%begin %{
  ... code in begin section ...
%}

%runtime %{
  ... code in runtime section ...
%}

%header %{
  ... code in header section ...
%}

%wrapper %{
  ... code in wrapper section ...
%}

%init %{
  ... code in init section ...
%}

%begin部分实际上是空的,因为默认情况下它只包含SWIG标题。提供这个部分是为了让用户在生成任何其他代码之前在包装器文件的顶部插入代码。

代码插入块中的所有内容都被逐字复制到输出文件中,并且不会被SWIG解析。大多数SWIG输入文件至少有一个这样的块来包含头文件并支持C代码。

代码块的一个常见用途是编写“帮助”函数。这些函数是专门用于构建接口的,但通常对普通的C程序是不可见的。例如:

%{
/* Create a new vector */
static Vector *new_Vector() {
  return (Vector *) malloc(sizeof(Vector));
}
%}
// Now wrap it 
Vector *new_Vector();
3.3 内联代码块

%inline指令将后面的所有代码逐字插入到接口文件的头部分。然后,SWIG预处理器和解析器对代码进行解析。

3.4 初始化块

当代码包含在%init部分时,它会被直接复制到模块初始化函数中。例如,如果你需要在模块加载时执行一些额外的初始化,你可以这样写:

%init %{
  init_variables();
%}

请注意,一些语言后端(如c#或Java)没有任何初始化函数,因此你应该定义一个全局对象来执行必要的初始化。

%init %{
  static struct MyInit { MyInit() { init_variables(); } } myInit;
%}
  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值