ImportCpp 编译指示
注意: c2nim可以解析大量的 C++ 子集, 关于 importcpp 编译指示模式语言,没有必要知道这里描述的所有细节。
与 C 语言的importc 编译指示类似,importcpp 编译指示可以用来导入 C++ 方法或一般的 C++ 标识符。 生成的代码使用 C++ 的方法调用语法: obj->method(arg) 。 与 header 和 emit 编译指示相结合,可与用 C++ 编写的库 宽松 对接。
# 关于如何与 C++ 引擎对接的可怕示例 ... ;-)
{.link: "/usr/lib/libIrrlicht.so".}
{.emit: """
using namespace irr;
using namespace core;
using namespace scene;
using namespace video;
using namespace io;
using namespace gui;
""".}
const
irr = "<irrlicht/irrlicht.h>"
type
IrrlichtDeviceObj {.header: irr,
importcpp: "IrrlichtDevice".} = object
IrrlichtDevice = ptr IrrlichtDeviceObj
proc createDevice(): IrrlichtDevice {.
header: irr, importcpp: "createDevice(@)".}
proc run(device: IrrlichtDevice): bool {.
header: irr, importcpp: "#.run(@)".}
这个例子需要告知编译器生成 C++ (命令 cpp ) 才能工作。编译器生成 C++ 代码时会定义条件标识符 cpp。
命名空间
这个 宽松对接 的例子使用了 .emit 来生成 using namespace 声明。通过 namespace::identifier 标识符来引用导入的名称往往会更好:
type
IrrlichtDeviceObj {.header: irr,
importcpp: "irr::IrrlichtDevice".} = object
Importcpp 应用于枚举
importcpp 应用于枚举类型时,数字枚举值都会标注 C++ 枚举类型,就像这样: ((TheCppEnum)(3)) 。(事实上这已是最简单的实现方式。)
Importcpp 应用于过程
请注意,用于过程的 importcpp 使用了一种有些隐晦的模式语言,以获得最大的灵活性:
- 井号 # 会被第一个或下一个参数所取代。
- 井号加个点 #. 表示调用应该使用 C++ 的点或箭头符号。
- 符号 @ 被剩余参数替换,通过逗号分隔。
例如:
proc cppMethod(this: CppObj, a, b, c: cint) {.importcpp: "#.CppMethod(@)".}
var x: ptr CppObj
cppMethod(x[], 1, 2, 3)
生成:
x->CppMethod(1, 2, 3)
有一项特殊规则: 为了保持与旧版本的 importcpp 编译指示的向后兼容性,如果没有任何特殊的模式字符 ( # ‘ @ 中的任意一个 ),就会假定使用 C++ 的点或箭头符号。所以上述例子也可以写成:
proc cppMethod(this: CppObj, a, b, c: cint) {.importcpp: "CppMethod".}
请注意,模式语言当然也具有 C++ 操作符重载的能力:
proc vectorAddition(a, b: Vec3): Vec3 {.importcpp: "# + #".}
proc dictLookup(a: Dict, k: Key): Value {.importcpp: "#[#]".}
- 撇号 ‘ 后面跟着 0..9 范围内的整数 i ,被第 i 个参数的 类型 替换。第 0 个位置是返回值类型。这可以用来向 C++ 函数模板传递类型。
在 ‘ 和数字之间,用星号来获得该类型的基本类型。(也就是说,它从类型中“拿走星号”,如 T* 变成 T 。)两个星号可以用来获取元素类型的元素类型,等等。
例如:
type Input {.importcpp: "System::Input".} = object
proc getSubsystem*[T](): ptr T {.importcpp: "SystemManager::getSubsystem<'*0>()", nodecl.}
let x: ptr Input = getSubsystem[Input]()
生成:
x = SystemManager::getSubsystem<System::Input>()
例如,C++中 new 运算符可以像这样“导入”:
proc cnew*[T](x: T): ptr T {.importcpp: "(new '*0#@)", nodecl.}
# 'Foo' 的构造函数:
proc constructFoo(a, b: cint): Foo {.importcpp: "Foo(@)".}
let x = cnew constructFoo(3, 4)
生成:
x = new Foo(3, 4)
然而,根据使用情况 new Foo 也可以像这样包装:
proc newFoo(a, b: cint): ptr Foo {.importcpp: "new Foo(@)".}
let x = newFoo(3, 4)
包装构造函数
有时候 C++ 类的拷贝构造函数是私有的,所以不能生成 Class c = Class(1,2);:cpp:,而应该是 Class c(1,2); 。 要达到这个目的,需要给包装 C++ 构造函数的 Nim 过程加上 constructor 编译指示。这个编译指示也有助于生成更快的 C++ 代码,因为这样一来构造时就不会再调用拷贝构造函数:
# 'Foo' 的更好的构造函数:
proc constructFoo(a, b: cint): Foo {.importcpp: "Foo(@)", constructor.}
包装析构函数
由于 Nim 直接生成C++,任何析构函数都会在作用域退出时被 C++ 编译器隐式调用。这意味着,通常我们可以不包装析构函数! 但是,当需要显式调用它时,就需要包装。模式语言提供了所需一切:
proc destroyFoo(this: var Foo) {.importcpp: "#.~Foo()".}
Importcpp 应用于对象
C++ 模板被映射成 importcpp 泛型对象。这意味着可以很容易地导入 C++ 模板,不需要再为对象类型设计模式语言:
type
StdMap[K, V] {.importcpp: "std::map", header: "<map>".} = object
proc `[]=`[K, V](this: var StdMap[K, V]; key: K; val: V) {.
importcpp: "#[#] = #", header: "<map>".}
var x: StdMap[cint, cdouble]
x[6] = 91.4
生成:
std::map<int, double> x;
x[6] = 91.4;
如果需要更精确的控制,可以在提供的模式中使用撇号 ‘ 来表示泛型的具体类型参数。更多细节请参见过程模式中的撇号操作符的用法。
type
VectorIterator[T] {.importcpp: "std::vector<'0>::iterator".} = object
var x: VectorIterator[cint]
生成:
std::vector<int>::iterator x;