ImportCpp 编译指示

注意: c2nim可以解析大量的 C++ 子集, 关于 importcpp 编译指示模式语言,没有必要知道这里描述的所有细节。

与 C 语言的importc 编译指示类似,importcpp 编译指示可以用来导入 C++ 方法或一般的 C++ 标识符。 生成的代码使用 C++ 的方法调用语法: obj->method(arg) 。 与 header 和 emit 编译指示相结合,可与用 C++ 编写的库 宽松 对接。

  1. # 关于如何与 C++ 引擎对接的可怕示例 ... ;-)
  2. {.link: "/usr/lib/libIrrlicht.so".}
  3. {.emit: """
  4. using namespace irr;
  5. using namespace core;
  6. using namespace scene;
  7. using namespace video;
  8. using namespace io;
  9. using namespace gui;
  10. """.}
  11. const
  12. irr = "<irrlicht/irrlicht.h>"
  13. type
  14. IrrlichtDeviceObj {.header: irr,
  15. importcpp: "IrrlichtDevice".} = object
  16. IrrlichtDevice = ptr IrrlichtDeviceObj
  17. proc createDevice(): IrrlichtDevice {.
  18. header: irr, importcpp: "createDevice(@)".}
  19. proc run(device: IrrlichtDevice): bool {.
  20. header: irr, importcpp: "#.run(@)".}

这个例子需要告知编译器生成 C++ (命令 cpp ) 才能工作。编译器生成 C++ 代码时会定义条件标识符 cpp。

命名空间

这个 宽松对接 的例子使用了 .emit 来生成 using namespace 声明。通过 namespace::identifier 标识符来引用导入的名称往往会更好:

  1. type
  2. IrrlichtDeviceObj {.header: irr,
  3. importcpp: "irr::IrrlichtDevice".} = object

Importcpp 应用于枚举

importcpp 应用于枚举类型时,数字枚举值都会标注 C++ 枚举类型,就像这样: ((TheCppEnum)(3)) 。(事实上这已是最简单的实现方式。)

Importcpp 应用于过程

请注意,用于过程的 importcpp 使用了一种有些隐晦的模式语言,以获得最大的灵活性:

  • 井号 # 会被第一个或下一个参数所取代。
  • 井号加个点 #. 表示调用应该使用 C++ 的点或箭头符号。
  • 符号 @ 被剩余参数替换,通过逗号分隔。

例如:

  1. proc cppMethod(this: CppObj, a, b, c: cint) {.importcpp: "#.CppMethod(@)".}
  2. var x: ptr CppObj
  3. cppMethod(x[], 1, 2, 3)

生成:

  1. x->CppMethod(1, 2, 3)

有一项特殊规则: 为了保持与旧版本的 importcpp 编译指示的向后兼容性,如果没有任何特殊的模式字符 ( # ‘ @ 中的任意一个 ),就会假定使用 C++ 的点或箭头符号。所以上述例子也可以写成:

  1. proc cppMethod(this: CppObj, a, b, c: cint) {.importcpp: "CppMethod".}

请注意,模式语言当然也具有 C++ 操作符重载的能力:

  1. proc vectorAddition(a, b: Vec3): Vec3 {.importcpp: "# + #".}
  2. proc dictLookup(a: Dict, k: Key): Value {.importcpp: "#[#]".}
  • 撇号 ‘ 后面跟着 0..9 范围内的整数 i ,被第 i 个参数的 类型 替换。第 0 个位置是返回值类型。这可以用来向 C++ 函数模板传递类型。

在 ‘ 和数字之间,用星号来获得该类型的基本类型。(也就是说,它从类型中“拿走星号”,如 T* 变成 T 。)两个星号可以用来获取元素类型的元素类型,等等。

例如:

  1. type Input {.importcpp: "System::Input".} = object
  2. proc getSubsystem*[T](): ptr T {.importcpp: "SystemManager::getSubsystem<'*0>()", nodecl.}
  3. let x: ptr Input = getSubsystem[Input]()

生成:

  1. x = SystemManager::getSubsystem<System::Input>()
  • @ 用来支持 cnew 操作这一特殊情况。它使调用表达式直接被内联,而不需要经过一个临时地址。这只是为了规避当前代码生成器的限制。

例如,C++中 new 运算符可以像这样“导入”:

  1. proc cnew*[T](x: T): ptr T {.importcpp: "(new '*0#@)", nodecl.}
  2. # 'Foo' 的构造函数:
  3. proc constructFoo(a, b: cint): Foo {.importcpp: "Foo(@)".}
  4. let x = cnew constructFoo(3, 4)

生成:

  1. x = new Foo(3, 4)

然而,根据使用情况 new Foo 也可以像这样包装:

  1. proc newFoo(a, b: cint): ptr Foo {.importcpp: "new Foo(@)".}
  2. let x = newFoo(3, 4)

包装构造函数

有时候 C++ 类的拷贝构造函数是私有的,所以不能生成 Class c = Class(1,2);:cpp:,而应该是 Class c(1,2); 。 要达到这个目的,需要给包装 C++ 构造函数的 Nim 过程加上 constructor 编译指示。这个编译指示也有助于生成更快的 C++ 代码,因为这样一来构造时就不会再调用拷贝构造函数:

  1. # 'Foo' 的更好的构造函数:
  2. proc constructFoo(a, b: cint): Foo {.importcpp: "Foo(@)", constructor.}

包装析构函数

由于 Nim 直接生成C++,任何析构函数都会在作用域退出时被 C++ 编译器隐式调用。这意味着,通常我们可以不包装析构函数! 但是,当需要显式调用它时,就需要包装。模式语言提供了所需一切:

  1. proc destroyFoo(this: var Foo) {.importcpp: "#.~Foo()".}

Importcpp 应用于对象

C++ 模板被映射成 importcpp 泛型对象。这意味着可以很容易地导入 C++ 模板,不需要再为对象类型设计模式语言:

  1. type
  2. StdMap[K, V] {.importcpp: "std::map", header: "<map>".} = object
  3. proc `[]=`[K, V](this: var StdMap[K, V]; key: K; val: V) {.
  4. importcpp: "#[#] = #", header: "<map>".}
  5. var x: StdMap[cint, cdouble]
  6. x[6] = 91.4

生成:

  1. std::map<int, double> x;
  2. x[6] = 91.4;
  • 如果需要更精确的控制,可以在提供的模式中使用撇号 ‘ 来表示泛型的具体类型参数。更多细节请参见过程模式中的撇号操作符的用法。

    1. type
    2. VectorIterator[T] {.importcpp: "std::vector<'0>::iterator".} = object
    3. var x: VectorIterator[cint]

    生成:

    1. std::vector<int>::iterator x;