概述
PHP-CPP是一个用来开发PHP扩展的C++库,它提供了一个文档完善且易于使用的类集合,这些类可以用于为PHP构建本地扩展。有完整的文档。
注意:仅适用于PHP7。这个库已经更新为适用于 PHP 7.0 及以上版本。如果你想为旧版本的 PHP 创建扩展,请使用 PHP-CPP-LEGACY 库。PHP-CPP 和 PHP-CPP-LEGACY 库有(几乎)相同的 API,所以你可以很容易地将 PHP 5.*的扩展移植到 PHP 7,反之亦然。
安装
- 注意:目前,PHP-CPP仅适用于Linux或OSX系统
- 本文实验环境为OSX
下载源码
1 | git clone -b v2.1.0 https://github.com/CopernicaMarketingSoftware/PHP-CPP.git |
打开Makefile文件(Makefile是一个保存编译器设置和指令的文件),大多数情况下,该文件中的默认配置已经足够好,但是你或许需要针对自己的环境做一些轻微的改动,比如改变安装目录或者选择自己的编译器。
开始构建PHP-CPP库
1 | make |
常见错误
- 如果你使用OSX来编译构建,可能会遇到
链接和unresolved symbol错误,如果你正面临此问题,那么需要对Makefile文件做一些改动,在这个 Makefile的某个地方有一个选项LINKER_FLAGS。修改为-shared -undefined dynamic_lookup。 - ld: unknown option: -soname clang: error: linker command failed with exit code 1
安装PHP-CPP库到系统中
1 | sudo make install |
PHP如何载入扩展
你可能知道在类unix的系统中,本地的PHP扩展名被编译成.so文件,在Windows环境中,编译成.dll文件,而全局的php.ini文件保存了系统中所有可用的扩展的列表,这意味着如果你正在创建自己的扩展,你也要创建这样的.so或.dll文件,并且你必须更新PHP配置文件,以便你自己的扩展被PHP加载。
get_module启动函数
在解释如何创建自己的扩展之前,我们先解释一下 PHP 如何加载一个扩展。当 PHP 启动时,它从配置目录中加载 *.ini 配置文件,对于这些配置文件中的每一行 extension=name.so,它都会打开相应的库,并调用其中的get_module()函数。因此,每个扩展库(你的扩展也是)都必须定义并实现这个get_module()函数。这个函数在库加载后就被 PHP 调用(因此在处理 pageviews 之前就被调用了),它应该返回一个指向一个结构的内存地址,这个结构保存了所有扩展库提供的函数、类、变量和常量的信息。
get_module()返回的结构是在Zend引擎的头文件中定义的,但它是一个相当复杂的结构,而且没有很好的文档。幸运的是,PHP-CPP库让你的生活变得更简单,并提供了一个扩展类,可以用来代替。
1 |
|
在上面的例子中,你看到了get_module()函数的一个非常直接的实现。每个使用 PHP-CPP 库的 PHP 扩展都或多或少地实现了这个函数,它是每个扩展的起点。有一些元素需要特别注意,首先,你看到的唯一的头文件是 phpcpp.h 头文件。如果你使用PHP-CPP库来构建你自己的扩展,你不需要包含Zend引擎的那些复杂的、非结构化的、大部分没有文档的头文件——你需要的只是PHP-CPP库的这个单一的phpcpp.h头文件。如果你坚持的话,你当然也可以包含核心 PHP 引擎的头文件——但你不必这样做。PHP-CPP 负责处理 PHP 引擎的内部,并提供给你一个简单易用的 API。
接下来你会注意到,我们将get_module()函数放在了一个 extern "C"的代码块中。正如库的名字所透露的那样,PHP-CPP 是一个 C++ 库。然而,PHP 希望你的库,尤其是 get_module() 函数是用 C 而不是 C++ 实现的。这就是为什么我们把 get_module() 函数包装在一个 extern "C" 块中。这将指示 C++ 编译器 get_module() 是一个常规的 C 函数,并且它不应该对它进行任何 C++ 名称的篡改。
PHP-CPP 库定义了一个 PHPCPP_EXPORT 宏,它应该放在 get_module() 函数的前面。这个宏确保get_module()函数是公开导出的,因此可以被PHP调用。这个宏根据编译器和操作系统的不同有不同的实现。
顺便说一下,这也是 PHP-CPP 提供的唯一一个宏。PHP-CPP打算成为一个普通的C++库,不使用魔术或预处理器的技巧。你所看到的就是你所得到的。如果某些东西看起来像函数,你可以肯定它实际上就是一个函数,而当某些东西看起来像一个变量,你可以肯定它也是一个变量。
我们继续往下看。在get_module()函数里面,Php::Extension对象被实例化,并被返回。至关重要的是,你必须为这个Php::Extension类创建一个静态实例,因为这个对象必须在PHP进程的整个生命周期内存在,而不仅仅是在调用get_module()的期间。构造函数有两个参数:扩展名和版本号。
get_module() 函数的最后一步是返回扩展对象。这看起来很奇怪,因为get_module()函数应该返回一个指向void的指针,而不是一个完整的Php::Extension对象。为什么编译器没有报告这个问题呢?那是因为Php::Extension类有一个cast-to-void-pointer-operator。因此,虽然看起来你返回的是完整的扩展对象,但实际上你只是返回了一个指向一个数据结构的内存地址,这个数据结构被 PHP 核心引擎所理解,并且保存了你的扩展的所有细节。
请注意,上面的例子还没有导出任何本地函数或本地类到PHP中——它只是创建了扩展。
编写第一个扩展
当你创建你自己的 PHP-CPP 扩展时,你也必须编译和部署它。一个普通的PHP脚本只需要复制到web服务器上就可以部署,但是部署一个扩展需要花费更多的精力:你需要一个Makefile,一个扩展专用的php.ini文件,当然还有实现扩展的*.cpp文件。
为了帮助你完成这些步骤,我们创建了一个几乎是空的扩展,包含了所有需要的文件。它包含了一个示例Makefile,一个示例配置文件,以及第一个main.cpp文件,其中的get_module()调用已经被实现。这为你开发扩展提供了一个良好的开端。
该扩展代码在PHP-CPP源码的Example目录下,本文后面的所有扩展源码都在这里可以找到。
1 | ├── CallPhpFunctions |
查看例子:Extension
1 | ├── 30-phpcpp.ini # 扩展声明文件 |
修改Makefile文件中下面两行内容。
1 | LIBRARY_DIR = $(shell php-config --extension-dir) |
- 第一行用于获取扩展的目录
- 第二行为你的PHP配置目录,用于存放你的扩展声明
extension=name.so
通过以下步骤安装扩展
1 | >>make |
测试扩展
1 | >>php -m | grep extension |
输出和错误
你可以使用常规的C++流来进行IO,使用常规的<<操作符和特殊的函数,如std::endl。但是使用std::cout和std::cerr流并不是一个好主意。
当 PHP 作为 webserver 模块运行时,stdout 被重定向到 webserver 进程最初启动的终端。在生产服务器上,这样的终端是不活动的,所以任何发送到stdout的输出都会丢失。因此,在webserver模块中运行的扩展中使用std::cout是不行的。但是即使 PHP 以 CLI 脚本的形式运行(并且 std::cout 也能工作),也不应该直接向 stdout 写入。写入stdout会绕过所有PHP用户空间脚本可能设置的输出处理程序。
PHP-CPP 库提供了一个 Php::out 流用来替代标准输出。这个Php::out变量是众所周知的std::ostream类的一个实例,并且尊重PHP中所有的输出缓冲设置。它的作用与PHP脚本中的echo()函数基本相同。
Php::out是一个普通的 std::ostream 对象。其结果是它使用了一个需要刷新的内部缓冲区。当你在输出中添加std::endl或明确添加std::flush时,刷新会自动发生。
1 | /** |
当你想触发一个PHP错误(相当于PHP trigger_error()的C++函数),你可以使用Php::error、Php::notice、Php::warning和Php::deprecated流中的一个。这些也是std::ostream类的实例。
1 | /** |
在上面的例子中,你可以看到我们使用了 std::flush 而不是 std::endl。原因是std::endl内部做了两件事:它附加了一个换行符,以及它刷新了缓冲区。对于错误、通知和警告,我们不需要换行,但我们仍然需要刷新缓冲区来实际生成输出。
Php::error流有一个非常奇特的地方:当你刷新它时,PHP脚本以一个致命的错误结束,而你的C++代码立即退出!!在引擎下面,PHP引擎做了一个longjump,到了Zend引擎深处的一个地方。在这个例子中,Php::out << "regular output"; 语句从未被执行。
这一切都很不寻常,而且(根据我们的说法)与软件工程的一般规则相冲突。一个输出生成函数的行为不应该像抛出一个异常。看起来像正常代码的代码,也应该表现得像正常代码一样,而不应该做意想不到的事情,比如跳出当前的调用栈。因此,我们建议不要使用Php::error,或者在使用它时要格外小心。
注册原生函数
无返回值:FunctionVoid
在get_module中声明扩展信息
1 | // create extension |
编写一个直接打印字符串的函数
1 | void my_function_void() |
有返回值:FunctionReturnValue
在get_module中声明扩展信息
1 | // create extension |
通过Php::Value来标示返回值类型
1 | /** |
Php::Value是存储在Zend引擎中的值的基类。value类的一个实例代表了在PHP环境用户空间中存在的一个变量,例如作为全局变量、函数中的局部变量、对象或数组的成员。可以是标量类型也可以是更复杂的数组或对象类型。
在内核中,Zend引擎使用zval对象来实现。这些zval对象持有引用计数和引用配置。PHP-CPP的Value类负责处理这些工作,所以你需要做的就是使用这个类的对象。
函数参数
查看例子:functionwithparameters
如何获取未定义参数
添加一个获取未定义参数的函数
1 | // add function, with undefined parameters, to extension |
可以通过Php::Parameters来获取函数参数
1 | void my_with_undefined_parameters_function(Php::Parameters ¶ms) |
上面这个例子,尽管在定义函数时没有定义参数,但是也可以通过Php::Parameters来获取,非常神奇。
如何写一个加法运算函数
添加一个有参数的函数
1 | // add function, with defined numeric parameters, to extension |
编写函数定义
1 | /** |
这个函数的含义是,接收两个整型数字,并返回求和结果。
如何传递引用
1 | // add function, with defined parameter by reference, to extension |
修改传递进来的参数值
1 | /** |
如何接收数组
1 | // add function, with defined array parameter, to extension |
如何接收对象
1 | // add function, with defined object parameter, to extension |
Php::Type支持情况
1 | /** |
小结
- 使用
Php::ByVal定义接收参数(值传递) - 使用
Php::ByRef定义接收引用 Php::Parameters是一个数组,用来获取参数
调用函数
首先让我们弄清楚一件事。 运行编译后的机器码比运行PHP代码快得多。 因此,一旦最终调用了C++函数或C++方法,通常就将参数转换为本地变量,然后开始运行自己的快速算法。从那时起,您就不想调用其他PHP函数。
但是,如果您要调用PHP函数(无论是Zend内置的函数,在扩展中定义的函数,还是来自PHP用户空间的函数),也是可以做到的。
查看例子:callphpfunction
添加一个含有两个参数的函数,第一个参数是回调函数,第二个参数是数字
1 | // add function to extension |
函数实现
1 | /** |
Lambda(匿名)函数
C++和PHP都支持lambda函数或匿名函数(在C++世界里,”lambda “这个词用得最多,PHPer讲的是 “匿名函数”)。使用 PHP-CPP 可以将这些函数从一种语言传递到另一种语言。可以从C++代码中调用一个匿名的PHP函数,也可以从PHP脚本中调用一个C++ lambda函数。
让我们从一个非常简单的PHP例子开始。在PHP中,你可以创建匿名函数,并将它们赋值给一个变量(或者直接将它们传递给一个函数)。
1 |
|
上面的代码对于大多数PHP程序员来说应该是很熟悉的,当然’other_function’也可以在PHP用户空间中实现,但是我们要用C++来演示如何用PHP-CPP来实现。’other_function’当然可以在PHP用户空间中实现,但是为了演示如何用PHP-CPP来实现,我们将用C++来构建它。就像你在前面的例子中看到的所有其他函数一样,这样的C++函数函数接收一个Php::Parameters对象作为参数,它是一个由Php::Value对象组成的std::vector。
1 |
|
就是这么简单。但是反过来说也是可以的。想象一下,我们在PHP用户空间代码中有一个接受回调函数的函数,下面的函数是PHP array_map()函数的简单版本。
1 |
|
想象一下,我们想从你的C++代码中调用这个PHP函数,使用一个C++ lambda函数作为回调。这是有可能的,而且很简单。
1 |
|
在这个例子中,我们将一个C++ lambda函数分配给一个Php::Function对象。Php::Function类是由Php::Value类派生出来的。Php::Value和Php::Function的唯一区别是Php::Function的构造函数接受一个函数。尽管有这个区别,这两个类是完全相同的。事实上,我们更希望能够让C++函数直接赋值给Php::Value对象,而跳过Php::Function构造函数,但这是不可能的,因为存在调用歧义。
Php::Function类可以像普通的Php::Value对象一样使用:你可以把它赋值给其他Php::Value对象,也可以在调用用户空间PHP函数时把它作为参数使用。在上面的例子中,我们正是这样做的:我们用我们自己的 “乘以二 “C++函数调用用户空间的my_iterate()函数。
C++ 函数签名
你可以向Php::Function构造函数传递不同类型的C++函数,只要它们与以下两个函数签名兼容:
1 | Php::Value function(); |
在内核,Php::Function类使用一个C++的std::function对象来存储函数,所以凡是可以存储在这样一个std::function对象中的东西,都可以分配给Php::Function类。
类和对象
C++和PHP都是面向对象的编程语言,你可以在其中创建类和对象。PHP-CPP 库为你提供了将这两种语言结合起来的工具,并使本地 C++ 类可以从 PHP 中访问。
遗憾的是(但如果你考虑一下,也是符合逻辑的),并不是每一个可以想到的C++类都可以直接导出到PHP中。这需要更多的工作(虽然不是那么多)。首先,你必须确保你的类是从Php::Base派生出来的,其次,当你把你的类添加到扩展对象中时,你还必须指定所有你想从PHP中访问的方法。
- 必须公开继承自
Php::Base - 指定访问控制
查看例子:cppclassinphp
1 | // we are going to define a class |
在扩展对象中,
- 通过
Php::Class定义类; - 使用
method方法来指定需要php代码访问的方法,和普通函数一样,也可以定义参数; - 使用
property来指定类成员,并设置访问权限
静态方法也支持。静态方法是指一个不能访问this指针的方法。因此,在C++中,这种静态方法和普通函数是一样的,普通函数也不能访问this指针。静态C++方法与普通C++函数的唯一区别是在编译时:编译器允许静态方法访问私有数据。然而,静态方法的签名与普通函数的签名完全相同。
PHP-CPP允许你注册静态方法。但是由于静态方法的签名与普通函数的签名完全相同,所以你注册的方法甚至不一定是同一个类的方法。普通函数和其他类的静态方法的签名完全一样,也可以注册! 从软件架构的角度来看,最好只使用同一类的静态方法,但C++允许你做的更多。
1 |
|
在PHP代码中使用扩展的功能
1 |
|
访问修饰符
在PHP中(在C++中也是),你可以将方法标记为public、private或protected。为了使你的本地类也能实现这一点,你应该在向Php::Class对象添加方法时传递一个额外的flags参数。
1 | // description of the class so that PHP knows which methods are accessible |
默认情况下,每一个方法 (还有每一个属性,但我们稍后会处理) 都是公开的。如果你想把一个方法标记为受保护的或私有的,你可以传递一个额外的 Php::Protected 或 Php::Private 标志。如果你也想把你的方法标记为抽象的或最终的,那么可以用Php::Abstract或Php::Final来对flag参数进行位或。PHP-CPP对value()方法做了这样的处理,这样在派生类中就不可能覆盖这个方法了。
请记住,C++ 类中的导出方法必须始终是公共的(即使在 PHP 中标记为私有或保护)。这是有道理的,因为毕竟你的方法会被 PHP-CPP 库调用,如果你把它们变成私有的,它们就会被库所忽略。
Abstract and final
在上一节中,我们展示了如何使用Php::Final和Php::Abstract标志来创建一个final或抽象方法。如果你想让你的整个类成为抽象的或最终的,你可以通过把这个标志传递给Php::Class构造函数来实现。
1 | // description of the class so that PHP knows which methods are accessible |
就像我们之前解释的那样,当你想注册一个抽象方法时,你应该在调用Php::Class::method()时传递一个Php::Abstract标志。然而,可能看起来很奇怪,这个方法也需要你传入一个真正的C++方法的地址。抽象方法通常没有实现,那么你需要提供一个方法的指针干什么呢?幸运的是,也有一种不同的方法来注册抽象方法。
1 | // register the decrement, and specify its parameters |
要注册抽象方法,你可以简单地使用Counter::method()方法的另一种形式,它不接受指向C++方法的指针。
构造与析构
在 C++ 中的构造函数和析构函数与 PHP 中的 construct() 和 destruct() 方法之间有一个很小但非常重要的区别。
C++ 中的构造函数是在一个正在初始化的对象上调用的,但这个对象还没有处于初始化状态。你可以通过调用构造函数中的一个虚拟方法来体验这种情况。即使这个虚拟方法在派生类中被重写,这也将始终执行类本身的方法,而不是重写的实现。原因是在调用C++构造函数的过程中,对象还没有完全初始化,对象还不知道自己在类层次结构中的位置。因此对虚拟方法的调用不能传递给派生对象。
然而在 PHP 中,__construct() 方法有不同的行为。当它被调用时,对象已经被初始化了,因此对派生类中实现的抽象方法的调用是完全合法的。下面的 PHP 脚本是完全有效的,但是在 C++ 中不可能做类似的事情。
1 | // base class in PHP, in which the an abstract method is called |
这个脚本输出的是’doSomething()’。原因是__construct()根本就不是一个构造函数,而是一个很普通的方法,只是恰好是第一个被调用的方法,而且是在对象被构造后自动调用的。
这个区别对于作为一个C++程序员的你来说是很重要的,因为你千万不要把你的C++构造函数和PHP的__construct()方法混淆。在C++构造函数中,对象正在被构造,而且还不是所有的数据都可用。虚拟方法不能被调用,对象也还不存在于 PHP 用户空间中。
在构造函数完成后,PHP引擎接管控制并创建PHP对象,然后PHP-CPP库将该PHP对象链接到你的C++对象。只有在PHP对象和C++对象都完全构造完成之后,才会调用construct()方法(就像普通方法一样)。因此,在你的类中同时拥有 C++ 构造函数和 construct() 方法是很常见的。C++ 构造函数用来初始化成员变量,而 __construct() 方法用来激活对象。
1 |
|
上面的代码显示 __construct() 被注册为一个普通的方法。我们之前使用的例子(有Counter类的例子)现在被扩展了,这样就可以通过向 “构造函数”传递一个值来给它一个计数器的初始值。
1 | $counter = new Counter(10); |
因为__construct()方法被看作是一个普通的方法,所以你也可以指定它的参数,以及该方法是公共的、私有的还是保护的。__construct()也可以从PHP用户空间直接调用,所以派生方法可以显式调用parent::__construct()。
私有构造函数
就像其他方法一样,construct()方法也可以被标记为私有或保护。如果你这样做,你将使你的类无法从PHP脚本中创建实例。重要的是要意识到,在这种情况下,C++ 构造函数和 C++ 解构函数仍然会被调用,因为会失败的是construct()调用,而不是实际的对象构造。
是的,如果你把__construct()方法设为私有,并且在 PHP 脚本中执行了new Counter()调用,PHP-CPP 库将首先实例化你的类的一个新实例,然后报告一个错误,因为__construct()方法是私有的,然后立即析构对象(并调用 C++ 析构函数)。
1 | // add a private __construct method to the class, so that objects can |
克隆对象
如果你的类有一个复制构造函数,它就会自动成为可克隆的类。如果你不希望你的类可以被 PHP 脚本克隆,你可以做两件事:
- 你可以从你的类中删除复制构造函数;
- 你可以注册一个私有的
__clone()方法,就像我们之前注册一个私有的__construct()方法一样。
删除复制构造函数
1 | /** |
把克隆方法注册为私有
1 | // alternative way to make an object unclonable |
构造对象
Php::Value类可以作为一个常规的PHP $variable使用,因此你也可以用它来存储对象实例。但是如何创建全新的对象呢?为此,我们有 Php::Object 类,它是一个简单的重写的 Php::Value 类,带有可供选择的构造函数,还有一些额外的检查,以防止你使用 Php::Object 对象来存储对象以外的值。
1 | // new variable holding the string "Counter" |
Php::Object 的构造函数接收一个类的名称,以及一个可选的参数列表,这些参数将被传递给 __construct() 函数。你可以使用内置的 PHP 类和其他扩展的名称(如 DateTime),你的扩展的类(如 Counter),甚至是 PHP 用户空间的类。
如果你想在不调用 __construct() 函数的情况下构造一个你自己的 C++ 类的实例,也可以使用 Php::Object 类。例如,当 __construct() 方法是私有的,或者当你想绕过对你自己的 __construct() 方法的调用时,这就很有用。
1 |
|
在上面的代码中,我们将 Counter 类的 __construct() 函数设为私有。这使得不可能创建这个类的实例(无论是从 PHP 用户脚本中,还是通过调用 Php::Object("Counter")),因为用这些方法构造对象最终会导致一个被禁止的 __construct() 调用。
Php::Object 确实有一种替代的语法,它可以接受一个指向 C++ 类的指针(在堆上分配,使用运算符 new!),并将这个指针变成一个 PHP 变量,而无需调用 __construct() 方法。请注意,你还必须指定类名,因为 C++ 类不保存任何关于它们自己的信息(比如它们的名字),而在 PHP 中,这样的信息是处理反射和 get_class() 等函数所需要的。
继承
PHP和C++都是支持类继承的面向对象编程语言。有一些区别。C++支持多继承,而PHP类只能有一个基类。为了弥补没有多重继承的不足,PHP支持接口和traits。
PHP-CPP库还允许你定义PHP接口,并创建PHP类和PHP接口的层次结构。
定义接口
如果你想让你的扩展定义一个接口,这样接口就可以从 PHP 用户空间脚本中实现,你可以用类似于定义类的方式来实现。唯一不同的是,你不使用Php::Class<YourClass>,而是使用Php::Interface实例。
1 | // description of the interface so that PHP knows which methods |
派生和实现
PHP-CPP 库试图使 PHP 和 C++ 的工作尽可能的透明。C++函数可以从PHP用户空间脚本中调用,C++类可以从PHP中访问。然而,归根结底PHP和C++还是不同的语言,由于C++没有PHP那样的反射功能,所以你必须显式地告诉PHP引擎该类实现了哪些基类和接口。
Php::Class<YourClass>对象有一个方法 extends()和一个方法 implements(),可以用来指定基类和实现的接口。你需要传入一个你之前配置的类或接口。我们来看一个例子。
1 | /** |
请注意,在 get_module() 函数中定义的 PHP 类的层次结构不一定要和 C++ 类的层次结构一致。你的 C++ 类 DerivedClass 根本不需要以 “MyClass”为基础,尽管在 PHP 脚本中它看起来像这样。为了代码的可维护性,当然最好让 PHP 的签名与 C++ 的实现多少有些相似。
魔术方法
每个PHP类都有 “魔术方法”。你可能已经在写PHP代码时知道这些方法:这些方法以两个下划线开头,名字像__set(),__isset(),__call()等等。
PHP-CPP库也支持这些魔术方法。使用一些C++编译器的技巧,C++编译器会检测你的类中是否存在方法,如果存在,它们会被编译到你的扩展中,并从PHP访问时被调用。
编译时检测
虽然你可能已经预料到这些魔术方法是Php::Base类中的虚函数,可以被重写,但其实不然。这些方法在编译时被C++编译器检测到(而且是非常正常的方法),只是碰巧有一个特定的名字。
由于编译时的检测,方法的签名有一定的灵活性。许多魔术方法的返回值都是分配给Php::Value对象的,这意味着只要你确保你的魔术方法返回的类型是可以分配给Php::Value的,你就可以在你的类中使用它。因此,你的 __toString() 方法可以返回一个 char*、一个 std::string、Php::Value (甚至是一个整数!),因为所有这些类型都可以分配给 Php::Value。
用PHP-CPP实现的魔术方法的好处是,它们不会在PHP用户空间中变得可见。换句话说,当你在你的 C++ 类中定义了 __set() 或 __unset() 这样的函数时,这些函数不能被 PHP 脚本显式地调用,但是当一个属性被访问时,它们会被调用。
构造函数
一般情况下,魔术方法不需要注册就可以使用。当你在你的类中添加了一个像__toString()或__get()这样的魔术方法时,当一个对象被转换为字符串或一个属性被访问时,它将被自动调用。不需要在get_module()启动函数中显式启用魔术方法。
这个规则的唯一例外是__construct()方法。这个方法必须要明确注册。这其中的原因有很多。首先,__construct()方法没有固定的签名,通过显式添加到扩展中,你还可以指定它接受什么参数,以及__construct()方法应该是公共的、私有的还是保护的(如果你想创建不能从PHP实例化的类)。
另一个必须显式注册 __construct() 方法的原因是,与其他魔术方法不同,__construct 方法必须在 PHP 中可见。在派生类的构造函数里面,经常需要对parent::__construct()进行调用。通过在get_module()函数中注册__construct()方法,你可以使该函数在PHP中可见。
克隆和析构
__clone()方法与__construct()方法非常相似。它也是在构造对象后直接调用的方法。区别在于__clone()是在一个对象被复制构造(克隆)后调用的,而__construct()是在普通构造函数之后调用的。
__destruct()方法会在对象被销毁之前被调用(也就是在C++的destructor运行之前)。
__clone() 和 __destruct() 方法是常规的魔术方法(与 __construct()不同),因此你不需要注册它们就可以使它们生效。如果你把这两个方法中的一个添加到你的类中,你将不必对get_module()启动函数做任何修改。如果有的话,PHP-CPP 库会自动调用它们。
在正常情况下,你可能不需要这些方法,也可以使用C++复制构造函数和C++析构函数。唯一不同的是,魔术方法是在处于完全初始化状态的对象上调用的,而C++复制构造函数和C++析构函数则是针对正在初始化的对象,或者是针对正在销毁的对象。
伪属性
通过__get()、__set()、__unset()和__isset()等方法,你可以定义伪属性。例如,它允许您创建只读属性,或在设置时检查其有效性的属性。
这些魔术方法与PHP脚本中的对应方法的工作原理完全一样,所以你可以轻松地将使用这些属性的PHP代码移植到C++中。
1 |
|
上面的例子展示了如何创建一个User类,该类似乎有一个名称和电子邮件属性,但不允许你分配一个没有‘@’字符的电子邮件地址,也不允许你删除属性。
1 | // initialize user and set its name and email address |
魔术方法 call(), callStatic() and __invoke()
C++方法需要在你的扩展get_module()启动函数中明确注册,才能从PHP用户空间访问。然而,当你重写 __call() 方法时,你可以接受所有的调用(甚至是对不存在的方法的调用)。当有人从用户空间对一些看起来像方法的东西进行调用时,它将被传递给这个__call()方法。在脚本中,你可以这样使用$object->something(),$object->whatever()或者$object->anything()(方法的名称是什么并不重要),所有这些调用都会传递给C++类中的__call()方法。
__callStatic()方法类似于__call()方法,但适用于静态方法。对YourClass::someMethod()的静态调用可以自动传递给你的C++类的__callStatic()方法。
除了__call()和__callStatic函数,PHP-CPP库还支持__invoke()方法。这是一个当对象实例被当作函数使用时被调用的方法。这可以与C++类中的运算符()重载相比。通过实现__invoke()方法,PHP用户空间的脚本可以创建一个对象,然后将其作为函数使用。
1 |
|
在php中使用
1 | // initialize an object |
输出如下
1 | regular |
转换为字符串
在PHP中,你可以在一个类中添加一个__toString()方法。当一个对象被转换为字符串时,或者当一个对象在字符串上下文中被使用时,这个方法会被自动调用。PHP-CPP 也支持这个 __toString() 方法。
1 |
|
除了这里描述的魔术方法,你可能已经在编写PHP脚本时知道了,PHP-CPP库还引入了一些额外的魔术方法。这些方法包括额外的转换方法,以及比较对象的方法。
魔术接口
在PHP内核中,自带了一些特殊的”魔术”PHP接口,脚本编写者可以通过这些接口来实现为一个类添加特殊功能。这些接口的名称是’Countable’、’ArrayAccess’和’Serializable’。这些接口带来的功能,也可以用PHP-CPP来实现。
你可能会好奇为什么PHP有时会使用魔术方法(例如__set和__unset),有时会使用接口来改变一个类的行为。这种选择似乎并不统一。对我们来说,不清楚为什么有些特殊功能是用魔术方法来实现的,而有些特殊功能是通过实现接口来激活的。在我们看来,Serializable接口也可以用神奇的__serialize()和__unserialize()方法来实现,或者__invoke()方法也可以是一个”Invokable”接口。PHP 不是一种标准化的语言,有些东西看起来就是这样,因为有人觉得这样或那样的方式来实现它。
尽管如此,PHP-CPP库还是试图尽可能地接近PHP。这就是为什么在你的C++类中,你也可以使用特殊的接口(因为C++没有像PHP那样的接口),所以用纯虚函数的类来代替。
SPL的支持
一个标准的PHP安装程序会附带标准PHP库(SPL)。这是一个建立在Zend引擎之上的扩展,它使用Zend引擎的特性来创建类和接口,如Countable、Iterator和ArrayAccess。
PHP-CPP 库也有这些名称的接口,它们的行为方式与 SPL 接口大致相同。但在内核中,PHP-CPP库不依赖于SPL。如果你实现了像Php::ArrayAccess或Php::Countable这样的C++接口,这和在PHP中写一个实现SPL接口的类是不同的。
PHP-CPP和SPL都是直接建立在Zend核心之上的,并且提供了相同的功能,但它们并不相互依赖。因此,如果没有加载SPL扩展,可以安全地使用PHP-CPP。
Countable接口
通过实现Php::Countable接口,你可以创建能传递给PHP count()函数的对象。
1 |
|
我们之前使用的Counter类已经被修改,展示了如何制作实现Php::Countable接口的类。这很简单,你只需要添加Php::Countable类作为基类。这个Php::Countable类有一个纯虚函数count(),必须要实现。
而这就是你要做的一切。不需要在get_module()函数里面注册专门的count()函数,添加Php::Countable作为基类即可。
1 | // create a counter |
输出的结果是,正如预期的那样,数值为3。
ArrayAccess接口
一个PHP对象可以通过实现Php::ArrayAccess接口变成一个变量,它的行为就像一个数组。当你这样做的时候,可以使用数组访问操作符($object["property"])访问对象。
在下面的例子中,我们使用Php::Countable和Php::ArrayAccess接口来创建一个可以用来存储字符串的关联数组类(记住:这只是一个例子,PHP已经支持关联数组了,所以这个例子有多大用处还值得商榷)。
1 |
|
Php::ArrayAccess有四个纯虚函数必须要实现。这些方法是用来检索和覆盖一个元素的方法,用来检查是否存在某个键的元素,以及用来删除一个元素的方法。在这个例子中,这些方法都已经实现了转发到一个常规的C++ std::map对象。
在get_module()函数里面,Map被注册并添加到扩展中。但与其他许多例子不同的是,没有一个类方法被导出到PHP中。它只实现了Php::Countable接口和Php::ArrayAccess接口,所以它完全可以用来存储和检索属性,但是从一个PHP脚本来看,它没有任何可调用的方法。下面的脚本展示了如何使用它。
1 | // create a map |
输出不言而喻。该Map有三个成员,”1234”(字符串变量)、”xyz “和 “0”。
Traversable接口
类也可以像普通数组一样,在foreach循环中使用。如果你想启用这个功能,你的类应该从Php::Traverable基类中扩展出来并实现getIterator()方法。
1 | // fill a map |
PHP-CPP 库实现迭代器的方式与 SPL 略有不同,如果你一直在使用 PHP,你就会习惯这种方式。在PHP中,为了使一个类可以遍历(在foreach循环中使用),你必须实现Iterator接口或IteratorAggregate接口。这是一个奇特的架构。仔细想想,迭代器不是容器对象本身,那个容器对象才是可迭代的!。在我们上面的例子中,$map变量不是实际的迭代器,而是被迭代的容器。真正的迭代器是一个隐藏的对象,不会暴露在你的PHP脚本中,它控制着foreach循环。然而,SPL也会将该map称为迭代器。
因此,在 PHP-CPP 中,我们决定不遵循 SPL API,而是创建了一种全新的方式来实现可遍历类。要使一个类可遍历,必须从Php::Traversable基类中扩展出来,这就迫使你实现getIterator()方法。这个方法应该返回一个Php::Iterator实例。
Php::Iterator对象有五个方法是运行foreach循环所需要的。请注意,你的 Iterator 类不需要是一个可以从 PHP 中访问的类,也不需要从 Php::Base 派生。它是一个内部类,被foreach循环使用,但它并不(必须)存在于PHP用户空间。
1 |
|
上面的例子进一步扩展了Map类。现在它实现了Php::Countable、Php::ArrayAccess和Php::Traversable。这意味着现在也可以在foreach循环中使用Map对象来迭代属性。
为了达到这个目的,我们必须将Php::Traversable类作为基类添加到Map类中,并实现getIterator()方法。这个方法返回一个新的MapIterator类,它是在堆上分配的。不用担心内存管理:PHP-CPP 库会在 foreach 循环结束的那一刻销毁迭代器。
MapIterator类是由Php::Iterator类派生出来的,实现了运行foreach循环所需的五个方法(current()、key()、next()、rewind()和valid())。请注意,基本的Php::Iterator类期望将迭代过的对象传递给构造函数。这一点是必须的,这样迭代器对象才能确保只要迭代器存在,这个迭代对象就会一直在范围内。
我们内部的MapIterator实现只是一个C++迭代器类的小包装。当然,在需要的时候,你可以创建更复杂的迭代器。
Serializable接口
通过实现 Php::Serializable 接口,你可以为一个类安装自定义的序列化和非序列化处理程序。PHP内置的serialize()函数是一个可以将数组或对象(甚至是充满数组和对象的嵌套数据结构变成简单字符串的函数。unserialize()方法正好相反,它将这样的字符串变回原始数据结构。
一个类的默认序列化实现将一个对象的所有公开可见的属性,并将它们连接成一个字符串。但由于你的类有一个本地实现,而且可能没有公共属性,你可能想安装一个自定义的序列化处理程序。在这个处理程序中,你就可以存储本地对象成员。
1 |
|
上面的例子将之前看到的Counter例子,变成了一个可序列化的对象。Php::Serializable有两个纯虚函数,应该添加到你的类中。调用serialize()方法将对象变成一个字符串,对一个未初始化的对象调用unserialize()方法将其从一个序列化的字符串中恢复出来。请注意,如果一个对象正在使用unserialize()恢复,那么 __construct()方法将不会被调用!
1 | // create an empty counter and increment it a few times |
输出结果是2
特性
当我们开发 PHP-CPP 库时,我们不得不问自己一个问题,那就是我们应该遵循 PHP 惯例还是遵循 C++ 惯例来实现库中的许多功能。
在 PHP 脚本中,你可以使用魔术方法和魔术接口来为类添加特殊的行为。在C++类中,你也可以实现同样的功能,不过是通过使用操作符重载、隐式构造函数和转换操作符等技术。例如PHP的__invoke()方法,与C++中的operator()多少有些相同。我们问自己的问题是,我们是否应该自动将 PHP 的 __invoke 方法传递给 C++ 的 operator() 调用,还是在 C++ 中也使用同样的 __invoke() 方法名?
我们决定遵循PHP的惯例,在C++中也使用魔术方法和魔术接口(尽管我们必须承认,以两个下划线开头的方法并不能使代码看起来非常漂亮),但是通过使用魔术方法,对于初学C++的程序员来说,从PHP到C++的转换保持了更简单的状态。而且最重要的是,并不是所有的魔术方法和接口都能用C++的核心特性来实现(比如运算符重载),所以我们不得不使用一些魔术方法或接口。这就是为什么我们决定,既然我们必须在C++中使用一些魔术方法,那么我们也可以完全遵循PHP,在C++中也支持所有的PHP魔术方法。
除了PHP用户空间中的魔术方法和接口外,Zend引擎还有一些额外的功能是PHP用户空间脚本无法接触到的。这些功能只有扩展程序员才能使用。PHP-CPP库也支持这些特殊功能。这意味着,如果使用PHP-CPP来编写函数和类,可以实现编写纯PHP代码无法实现的事情。
额外的转换函数
在内部,Zend引擎有特殊的转换例程来将对象转换为整数、布尔值和浮点值。由于这样或那样的原因,一个PHP脚本只能实现__toString()方法,而其他所有的转换操作都远离它。PHP-CPP 库解决了这一限制,并允许实现其他的转换函数。
PHP-CPP 库的设计目标之一是尽可能地接近 PHP。出于这个原因,转换函数被赋予了与 __toString() 方法相匹配的名称:__toInteger(), __toFloat() 和 __toBool()。
1 |
|
当一个对象被转换为标量类型时,或者在标量上下文中使用时,会自动调用转换方法。下面的例子说明了这一点。
1 | // initialize an object |
对象比较
如果你在PHP中用<, ==, !=, >等比较运算符比较两个对象,Zend引擎会运行一个对象比较函数。PHP-CPP库会拦截这个方法,并将比较方法传递给你的类的__compare方法。换句话说,如果你想安装一个自定义的比较操作符,你可以通过实现__compare()来实现。
1 |
|
当你在PHP脚本中尝试比较对象时,比较函数会被自动调用。当两个对象相同时,它应该返回0,当’this’对象较小时,返回小于0的值,当’this’对象较大时,返回大于0的值。
1 | // initialize a couple of objects |
类成员属性
当你在PHP中定义一个类时,你可以为它添加属性(成员变量)。然而,当你在一个本地C++类中添加成员变量时,你最好使用常规的本地成员变量,而不是PHP变量。原生变量的性能比PHP变量好得多,如果你也能用int's和std::string对象来存储整数或字符串,那么在Php::Value对象中存储这些变量就太疯狂了。
普通成员变量
很难想象,世界上有人愿意创建一个原生类,上面有常规的弱类型的PHP公共属性。然而,如果你坚持,你可以使用PHP-CPP库来实现。让我们以PHP中的一个类为例,看看它在C++中会是什么样子。
1 | class Example |
上面的例子创建了一个具有一个公共属性的类。这个属性可以被Example类访问,并且因为它是公共的,也可以被其他所有人访问,如示例中所示。如果你喜欢这样的类,你可以用PHP-CPP写一些类似的东西。
1 |
|
该示例代码显示了如何在get_module()函数中初始化属性。
你也可以定义私有或受保护的属性,而不是公共属性,但即使是这样也可能不是你想要的,因为在原生C++变量中存储数据要快得多。
静态属性和类常量
静态属性和类常量可以用类似于属性的方式来定义。唯一不同的是,你必须传递Php::Static或Php::Const标志,而不是Php::Public、Php::Private或Php::Protected访问修饰符。
1 |
|
该类常量可以通过使用Example::MY_CONSTANT从PHP脚本中访问,静态属性可以使用Example::$my_property访问。
除了使用property()方法,你还可以使用constant()方法,或者使用Php::Constant类创建类常量。
Smart properties
通过get()和set()魔术方法,你可以制作更高级的属性,这些属性可以直接映射到C++变量上,并且当一个属性被覆盖时,你可以执行额外的检查,从而使一个对象始终处于有效状态。
除此之外,通过 PHP-CPP 库,你还可以为属性分配 getter 和 setter 方法。每当一个属性被访问时,你的getter或setter方法就会被自动调用。
1 |
|
下面的PHP脚本使用了这一点。它创建了一个示例对象,将值属性设置为500(这是不允许的,高于100的值会被四舍五入到100),然后它读出双倍值。
1 | // create object |
异常
PHP和C++都支持异常,通过PHP-CPP库,这两种语言之间的异常处理是完全透明的。在 C++ 中抛出的异常会自动传递给 PHP 脚本,而 PHP 脚本抛出的异常可以被 C++ 代码捕获,就像一个普通的 C++ 异常一样。
让我们从一个简单的抛出异常的C++函数开始。
1 |
|
你又一次看到了一个非常简单的扩展。在这个扩展中,我们创建了一个 “myDiv “函数,用来除以两个数字。但是除以零当然是不允许的,所以当试图除以零时,会产生一个异常。下面的 PHP 脚本就使用了这个功能。
1 | try |
这个例子显示了从C++代码中抛出异常并在PHP脚本中捕获异常是多么的简单。PHP-CPP 库会在内部捕获你的 C++ 异常并将其转换为 PHP 异常,但这一切都发生在引擎盖下。对于你这个扩展程序员来说,就好像你根本没有在两种不同的语言中工作,你可以简单地抛出一个Php::Exception对象,就好像它是一个普通的PHP异常一样。
在C++中捕获异常
反过来,如果你的扩展调用了一个PHP函数,而这个PHP函数恰好抛出了一个异常,你可以像捕获一个普通的C++异常一样捕获它。
1 |
|
这段代码需要解释一下。正如我们之前提到的,Php::Value 对象可以像使用普通的 PHP $variable 一样使用,因此你可以在其中存储整数、字符串、对象、数组等等。但这也意味着你可以用它来存储函数(因为 PHP 变量也可以用来存储函数)!而这正是我们要做的。
本例扩展中的 callMe() 函数只接收一个参数:一个它将立即调用的回调函数,回调函数的返回值也由 callMe() 函数返回。如果这个回调函数以某种方式抛出一个异常,它将被callMe()函数捕获,并返回一个替代的字符串(“Exception caught!”)。
1 | // call "callMe" for the first time, and supply a function that returns "first call" |
这个 PHP 脚本使用了我们的扩展,并连续两次调用 callMe() 函数。首先用一个普通函数返回一个字符串,然后用一个抛出异常的函数(扩展会捕捉到这个异常)。输出结果正如你所期望的那样。
变量
PHP中的变量是弱类型的。因此,一个变量可以容纳任何可能的类型:整数、字符串、浮点数,甚至一个对象或数组。而C++则是一种强类型语言。在C++中,一个整数变量总是有一个数值,而一个字符串变量总是持有一个字符串值。
当你把本地代码和PHP代码混合在一起时,你需要把弱类型的PHP变量转换成本地变量,反之则是:把本地变量转换成弱类型的PHP变量。PHP-CPP库提供了Php::Value类,使这个任务变得非常简单。
Zval’s
如果你曾经花时间用纯C语言编写过PHP扩展,或者你曾经读过一些关于PHP内部的东西,你一定听说过zval的。zval是一个存储PHP变量的C结构。在内核中,这个zval保留了一个refcount、一个多种类型的联合体和一些其他成员。每次访问这样的zval,对它进行复制,或者对它进行写入,你都必须打破头正确更新refcount,或将zval分割成不同的zval,显式调用复制构造函数,分配或释放内存(使用特殊的内存分配例程),或者选择不这样做,让zval单独存在。
更糟糕的是,在Zend引擎中,有数百个不同的未被记录的宏和函数可以操作这些zval变量。有专门的宏针对zval,有通过指针指向zval的宏,有通过指针的指针指向zval的宏,甚至有通过指针的指针的指针指向zval的宏。
每一个PHP模块、每一个PHP扩展和每一个内置的PHP函数都在忙于处理这些zval结构。没有人花时间把这样的zval包在一个简单的C++类中,为你完成所有这些管理,这是一个很大的惊喜。C++就是这样一门不错的语言,它的构造函数、析构函数、转换运算符和运算符重载,可以封装这些复杂的zval处理。
PHP-CPP引入了Php::Value对象,它的接口非常简单,可以消除所有zval处理的问题。在内部,Php::Value对象是zval变量的一个包装器,但它完全隐藏了zval处理的复杂性。
标量类型
Php::Value对象可以用来存储标量类型。可以是整数、浮点数、字符串、布尔值和空值等变量。
1 | Php::Value value1 = 1234; |
Php::Value类有转换操作符,可以将对象转换成几乎所有可以想到的本地类型。当你可以访问一个Php::Value对象,但想把它存储在一个(访问速度快得多的)本地变量中时,你可以简单地赋值它。
1 | void myFunction(const Php::Value &value) |
如果 Php::Value 对象持有一个对象,并且你把它用成一个字符串,那么对象的 __toString() 方法就会被调用,这和你在 PHP 脚本中把变量用成字符串的情况完全一样。
许多不同的操作符也被重载,因此你可以在算术操作中直接使用Php::Value对象,将其与其他变量进行比较,或者将其发送到一个输出流。
1 | void myFunction(Php::Value &value) |
Php::Value对象对大多数类型都有隐式构造函数。这意味着每一个接受Php::Value作为参数的函数也可以用原生类型来调用,在应该返回Php::Value的函数中,你可以简单地指定一个标量返回值(它将被编译器自动转换为Php::Value对象)。
1 | Php::Value myFunction(const Php::Value &value) |
正如你在例子中看到的,你几乎可以用Php::Value对象做任何事情。在内部,它完成了所有的zval操作,有时会变得很复杂,但对于你这个扩展程序员来说,没什么好担心的。
字符串
字符串可以轻松地存储在Php::Value对象中。将一个字符串赋给Php::Value,或者将一个Php::Value转换为一个字符串是如此的简单,以至于几乎没有任何解释的必要。通常情况下,赋值运算符和转换运算符即可。然而,当性能是一个问题时,你可以考虑直接访问Php::Value对象的内部缓冲区。
当一个Php::Value被转换为std::string时,整个字符串的内容会从Php::Value对象复制到std::string对象中。如果你不想做这样一个完整的拷贝,你可以把值投给一个const char *来代替。这使你可以直接访问Php::Value对象内部的缓冲区。字符串的大小可以用size()方法来检索。但你必须意识到,一旦Php::Value脱离了作用域,缓冲区的指针就不再保证有效了。
1 | /** |
也可以直接写到内核的Php::Value缓冲区。当你把一个字符串分配给Php::Value对象时,整个字符串缓冲区也会被复制。不管你赋值的字符串是std::string还是char*都会有一个拷贝。对于少量字节来说,这几乎不是问题,如果你换一种方式,会让你的代码可读性大大降低。但如果你要复制很多字节,你最好能直接访问缓冲区。
1 | /** |
第一个例子函数比较容易读懂。read()系统调用用于向本地缓冲区填充字节。然后将这个本地缓冲区转换为Php::Value对象并返回。
第二个示例函数更有效率,因为现在系统调用read()会立即将字节读到Php::Value对象的缓冲区中,而不是读到一个临时缓冲区中。作为一个程序员,你必须根据你的需求在这些算法中选择一种:简单的代码或更高效的代码。
数组
PHP支持两种数组类型:常规数组(以数字为索引)和关联数组(以字符串为索引)。Php::Value对象也支持数组。通过使用数组访问操作符(方括号)给Php::Value对象赋值,你会自动把它变成一个数组。
1 | // create a regular array |
从数组中读取数据也同样简单。你也可以使用数组访问运算符(方括号)来实现。
1 | Php::Value array; |
还有一个特殊的Php::Array类。这是一个扩展的Php::Value类,在构造时,立即以空数组开始(不像Php::Value对象默认构造为NULL值)。
1 | // create empty array |
对象
就像Php::Array类是一个扩展的Php::Value,初始化为一个空数组一样,也有一个Php::Object类在构造时成为一个对象。默认情况下,这是一个stdClass的实例(PHP最简单的类)。
1 | // create empty object of type stdClass |
当你用PHP-CPP库创建了自己的类,你可以使用相同的Php::Object类来制作它的实例。因为PHP和C++是不同的语言,所以从函数中返回的对象实例(Php::Value或Php::Object实例)和在C++代码中内核使用的变量(普通的C++指针)是有区别的。PHP-CPP允许你轻松转换这两种类型。
1 |
|
迭代
Php::Value类实现了begin()和end()方法,就像许多C++ STL容器一样。因此,你可以像遍历一个std::map类一样遍历一个Php::Value。
1 | /** |
迭代值是一个std::pair<Php::Value::Php::Value>。你可以访问它的属性’first’来获取当前的键,而属性’second’来获取当前的值。这和你在std::map上迭代的方式是一样的。
你可以遍历所有持有对象或数组的Php::Value对象。当你在一个数组上迭代时,迭代器只是简单地迭代数组中的所有记录。
对于对象来说,有一些东西需要考虑。如果你迭代的对象实现了Iterator或IteratorAggregate接口,C++迭代器就会使用这些内置的接口并调用它的方法来遍历对象。对于常规对象(那些没有实现Iterator或IteratorAggregate的对象),迭代器只是简单地迭代对象的所有公共属性。
一个迭代器可以在两个方向上使用:操作符++以及操作符--都可以使用。但要注意使用--操作符。如果Php::Value对象持有一个实现了Iterator或IteratorAggregate的对象,反向迭代就无法进行,因为内部迭代器只有一个next()方法,PHP-CPP库没有办法指示内部迭代器向后移动。
同时要注意++后缀操作符的返回值。通常情况下,后缀增量操作会返回操作前的原始值。当你在实现了 Iterator 或 IteratorAggregate 的对象上进行迭代时,情况就不同了,因为 PHP-CPP 库不可能复制一个 PHP 迭代器。因此,++后缀操作符(只有在Iterator或IteratorAggregate对象上使用时)会返回一个全新的迭代器,该迭代器回到对象的前部位置。但请记住,在C++和PHP(以及许多其他编程语言)中,使用++前缀操作符要明智得多,因为这不需要对原始对象进行复制,所以无论如何你都不应该使用++后缀操作符。
函数
当一个Php::Value对象持有一个可调用的对象时,你可以使用()操作符来调用这个函数或方法。
1 | // create a string with a function name |
全局变量
要读取或更新全局PHP变量,你可以使用Php::GLOBALS变量。这个变量的工作原理和PHP脚本中的$GLOBALS变量差不多。
1 | // set a global PHP variable |
除了$GLOBALS变量之外,PHP还允许你使用$_GET、$_POST、$_COOKIE、$_FILES、$_SERVER、$_REQUEST和$_ENV变量来访问变量。在你的C++扩展中,你可以用全局变量Php::GET, Php::POST, Php::COOKIE, Php::FILES, Php::SERVER, Php::REQUEST和Php::ENV做类似的事情。这些都是全局的、只读的、具有重载操作符[]方法的对象。因此,你可以像访问关联数组一样访问它们。
1 | // retrieve the value of a request variable |
小心C++全局变量
与PHP脚本不同的是,PHP脚本只能处理单个请求会话,而扩展则是用来处理多个请求会话。这意味着当你在扩展中使用全局C++(!)变量时,这些变量不会在会话之间被设置回初始值。然而,Php::GLOBALS变量总是在每个新的会话开始时重新初始化。
全局常量和类级常量
常量
在 PHP 脚本中可以定义常量(包括全局常量和类级常量)。这也可以通过 PHP-CPP 来实现。如果你想把常量暴露在用户空间的PHP代码中,你可以通过在get_module()调用中添加常量到Php::Extension对象中来实现。
1 | // add integer constants |
在php中使用常量
1 | echo(MY_CONSTANT_1."\n"); |
PHP也支持类级常量的概念。在内部,在Zend引擎中,类级常量被实现为常规的类成员,但是常量属性没有 “public “或 “private “标志,而是用 “constant “标志来标记。PHP-CPP也暴露了这一点。你可以用Php::Const标志来注册类属性。
除此之外,一个Php::Class实例也有一个 “constant”方法,你可以将Php::Constant的实例添加到类中。从语义上看,这三种创建类级常量的方法都是相同的。
1 | /** |
运行时常量
如果你想在运行时从你的C++代码中找出一个用户空间常量的值,或者当你想找出一个常量是否被定义时,你可以简单地使用Php::constant()或Php::defined()函数。要在运行时定义常量,请使用Php::define()。
1 | /** |
从php.ini读取配置
从php.ini文件中读取设置就像从普通PHP脚本中获取设置一样简单。在PHP脚本中,你可以使用内置的ini_get()函数从php.ini文件中读取设置,而在你的C++扩展中,你可以使用Php::ini_get()函数。
1 | /** |
Php::ini_get()函数返回一个可以分配给字符串、整数和浮点数的对象。在上面的例子中,我们使用这个函数将设置直接分配给一个整数和一个std::string。
你只能从php.ini中获取预定义的变量。因此不可能用随机字符串调用Php::ini_get(). 如果你想使用你自己的变量,你必须先在get_module()函数中注册它们,然后才能调用Php::ini_get()来获取当前值。
1 |
|
扩展回调函数
get_module()函数在你的扩展启动时被调用。它返回一个内存地址,在那里Zend引擎可以找到关于你的扩展的所有相关信息。
在这个get_module()的调用之后,你的扩展就会被加载,并将被用来处理多个请求会话。这是标准PHP脚本和本地扩展之间的一个重要区别,因为标准PHP脚本只处理单个请求。但扩展服务于多个请求后。
如果你使用全局的C++变量,这种区别就显得尤为重要。这样的全局变量会在扩展加载时被初始化(而不是在每个请求开始时)。你对全局变量所做的更改会保留它们的值,因此后续的请求会看到更新后的值。
顺便说一下,这只发生在本地变量上。存储在Php::GLOBALS对象中的全局PHP变量,会在每次请求开始时重新初始化。你不必担心你对全局PHP变量所做的修改:在下一个请求开始时,Php::GLOBALS对象是全新的,你在上一个请求中所做的修改不再可见。
回到全局的C++变量。如果你想在一个新的请求开始时重置一个全局变量,你可以注册一个特殊的回调函数,这个函数在每个请求前被调用。
1 |
|
Php::Extension类有一个方法onRequest(),在上面的例子中用来注册一个回调函数。这个回调会在每个请求之前被调用。正如你所看到的,它是允许使用C++ lambda函数的。
onRequest()并不是Php::Extension对象中唯一可以注册回调的方法。事实上,有四种不同的on*()方法可以使用。
- void onStartup(const std::function<void()> &callback);
- void onRequest(const std::function<void()> &callback);
- void onIdle(const std::function<void()> &callback);
- void onShutdown(const std::function<void()> &callback);
当Zend引擎已经加载了你的扩展,并且其中的所有函数和类都被注册时,启动回调被调用。如果你想在函数被调用之前初始化扩展中的其他变量,你可以使用onStartup()函数并注册一个回调来运行这个初始化代码。
Zend引擎初始化后,就可以处理请求了。在上面的例子中,我们使用了onRequest()方法来注册一个回调,这个回调会在每个请求前被调用。除此之外,你还可以安装一个回调,当Zend引擎进入空闲状态时,这个回调会在每次请求后被调用。
等待下一个请求。这可以通过Php::Extension对象中的onIdle()方法来实现。
第四个可以注册的回调是在PHP关闭前被调用的回调。如果有什么需要清理的地方,可以安装这样的回调,并从中运行清理代码。
预先fork的Web引擎 (如Apache)
如果你在一个pre-forked的web服务器(比如Apache)上运行PHP,你的扩展会在各种工作进程被fork之前被加载和初始化。这样做的后果是,get_module()函数和你可选的onStartup()回调函数被父进程调用,而所有其他回调和实际的页面处理被子进程调用。因此,对getpid()的调用(或其他用于检索当前进程信息的函数)将在onStartup回调中返回其他东西,就像在其他扩展函数中一样。
你可能要因此而小心。最好不要在启动函数中做一些在进程fork成不同子进程时可能无法工作的事情(比如打开文件描述符)。还有一点需要注意的是,启动函数只在Apache启动(或重载,见后文)时被父进程调用,而关闭函数则被每个平滑退出的子进程调用。因此,onShutdown不仅在Apache进程停止时被调用,而且在其中一个工作进程因为不再需要而退出时,或者因为它被一个新的工作进程取代而被调用。
get_module()函数在你的扩展被初始化时被调用。但不仅如此。当apache被重载时(例如通过给命令行指令 “apachectl reload”),你的get_module()会被第二次调用,你在Extension::onStartup()中注册的回调也会被再次调用。这通常不是问题,因为在第一次调用get_module()后,静态扩展对象处于锁定状态,在第二次调用get_module()时,你试图添加到扩展对象中的函数和类会被直接忽略。
注意多线程
如果你的扩展运行在多线程的PHP安装模式上,你需要格外小心。大多数PHP安装模式(Apache、CLI脚本等)一次只服务一个请求,按顺序进行。然而,有一些PHP安装模式使用了多线程,并且可以并行处理多个请求。如果你的扩展在这样的环境中运行,你应该知道你的全局(和静态)变量也可以被多个线程同时访问。使用std::mutex或std::atomic等技术来防止数据竞态条件和冲突是你自己的责任。
如果你的扩展是为多线程环境编译的,PHP-CPP头文件定义了宏ZTS。你可以使用这个宏来检查是否需要创建特殊的代码来处理线程。
1 |
|
另一个重要的事情是,PHP内部也做了这种锁定。如果你从你的C++代码中调用一个PHP函数(比如Php::Value("myFunction")()),或者当你访问Php::GLOBALS数组中的一个PHP变量(或者其他超级全局变量之一)时,PHP必须锁定一些东西以确保没有其他线程同时访问相同的信息。这些操作可能很昂贵。
因此,用 PHP-CPP 编写本地扩展的良好经验:
- 不要使用全局变量
- 只调用其他本地函数,不要回调到PHP中。
这些规则并不像它们看起来那样具有局限性。全局变量的使用并不被认为是优秀的软件设计,所以你可能根本就没有使用它们,你之所以要写一个本地扩展,首先是因为你想摆脱PHP。从你的扩展代码中调用(慢的)PHP函数,无论如何都应该被阻止。
命名空间
尽管在 PHP 脚本中,命名空间有非常丰富的实现方式,有特殊的关键字,如 use 和 namespace 以及特殊的常量,如 __NAMESPACE__,但它们内部非常简单。
命名空间无非就是一个类或函数的前缀。如果你想让你的类或函数出现在一个特定的命名空间中,你只需要在类或函数名中添加一个前缀。下面的代码在 “myNamespace”命名空间中创建了一个函数 “myFunction”。
1 | // add the myFunction function to the extension, |
如果你愿意,你可以使用Php::Namespace实用类来实现。这个类的签名和Php::Extension类完全一样,你也可以用它来注册类和函数。
1 | // create a namespace |
Php::Namespace类只是一个容器,它会自动为你添加的所有类和函数添加一个前缀。正如你在例子中看到的那样,嵌套命名空间也是可能的。
在这个例子中,我们使用std::move()函数将嵌套的命名空间移动到父命名空间中,并将第一个命名空间移动到扩展中。移动比添加更有效率,尽管常规的extension.add(myNamespace)也是有效的。
动态加载扩展
从PHP转到C++的用户经常会问,如果用C++代码代替PHP代码,是否会增加系统管理的难度。我们必须在这里说实话:使用PHP比使用C++更容易。例如,要激活一个PHP脚本,你不需要root权限,你可以简单地复制脚本到Web服务器。部署原生C++扩展需要更多的工作:你需要先停止Web服务器,编译扩展,安装,然后重新启动Web服务器。
除此之外,当一个扩展被部署后,它将立即对所有托管在Web服务器上的网站进行激活。一个已部署的PHP脚本只改变了单个网站的行为,但一个已部署的C++扩展会影响所有网站。其实不可能只为特定的网站激活一个扩展,也不可能只为一个网站测试一个新版本的扩展,因为扩展是由所有PHP进程共享的。如果你真的想在不同的网站上使用不同的扩展,你需要多个服务器,都有自己的配置。
或者你可以使用动态加载。
PHP有一个内置的dl()函数,你可以用它来加载扩展。这允许你从PHP脚本中调用dl("myextension.so")函数来加载一个扩展,这样一个扩展只适用于一个特定的站点。出于安全考虑,这个内置的dl()函数有一些限制(否则会允许用户运行任意的本地代码),但如果只有你一个人负责一个系统,或者当一个服务器不是由多个组织共享时,你可以使用PHP-CPP创建一个类似于dl()的函数,但没有这个限制。
为什么dl()会受到限制?
由于安全问题,dl()函数受到限制。当你使用dl()时,只能加载存储在系统范围扩展目录中的扩展,而不能用于加载用户放在其他位置的扩展。因此,调用 dl(“/home/user/myextension.so”)会失败,因为”/home/user “不是官方的扩展目录。为什么会有这种限制?
要理解这一点,首先必须认识到,在正常的PHP安装中,PHP脚本是由没有root权限的用户编辑的。在共享主机环境下,不同的用户都在同一个系统上运行自己的网站。在这样的设置中,如果一个用户可以编写一个可以访问他人数据的脚本或程序,那是绝对不行的。然而,如果使用一个不受限制的dl()函数,恰恰可以做到这一点。一个不受限制的 dl() 调用将允许 PHP 程序员编写一个本地扩展,将其存储在他们的主目录或 /tmp 目录中,并由 webserver 进程加载。然后,他们可以执行任意代码,并可能在其他人的网站内安装记录器或其他恶意代码。通过只允许从系统范围内的扩展目录中加载扩展,PHP 保证了每一个动态加载的扩展必须至少由系统管理员安装。
然而,当你写你自己的扩展时(无论是直接在Zend API之上,还是通过使用PHP-CPP库)你已经可以写和执行任意代码了。这里不需要安检。从你的C/C++代码中,你可以做任何你想做的事情。如果你能根据网站的要求动态加载一个扩展,那不是很酷吗?一个网站需要稳定,并加载你的扩展测试良好的1.0版本,而第二个网站更多的是实验性的,并加载2.0版本。你可能在同一台机器上运行两个版本的扩展。
Thin loader extension
想象一下,你正在编写你自己的扩展 “MyExtension”,它有许多不同的类和功能,而且你计划一直为它推出新的版本。你不希望以 “大爆炸 “的方式部署新版本,而是希望慢慢地推出新版本,一次一个客户或一个网站。你会怎么做?
你首先要开发一个瘦加载器扩展:ExtensionLoader扩展。这个扩展只有一个函数:enable_my_extension(),它接收你的实际扩展的版本号,并动态地加载你的扩展的那个版本。这是一个简单的扩展(它只有一个功能),你可以在全球范围内安装,而且你可能永远不会有更新。
1 | /** |
上面的代码保存了ExtensionLoader扩展的全部源代码。你可以在你的系统上安装这个扩展,把它复制到全局php扩展目录下并更新php.ini文件。
当你安装了这个thin loader扩展之后,你就可以用类和函数写满你的实际大扩展,并将这个扩展编译成*.so文件:第一个版本你编译成MyExtension.so.1,以后的版本编译成MyExtension.so.2、MyExtension.so.3等。对于每一个新的版本,你都会引入一个新的版本号,并将这些共享对象复制到/path/to目录下(与上面显示的”ExtensionLoader”扩展中的路径相同)。虽然这不是官方的PHP扩展目录,但是这些扩展可以通过enable_my_extension()函数加载。
你不需要将扩展程序复制到PHP扩展目录中,也不需要更新php.ini配置。要激活一个扩展,你只需要调用引入的enable_my_extension()函数。
1 | // enable version 2 of the extension (this will load MyExtension.so.2) |
上面我们展示的ExtensionLoader还是很安全的。不能运行任意代码,也不能打开任意*.so文件。最糟糕的事情是有人用错误的版本号打开了一个扩展。
但如果你真的信任你系统上的用户,你可以很容易地调整thin loader扩展来允许其他类型的参数。在最开放的情况下,你甚至可以写一个函数,让用户从字面上打开所有可能的共享对象文件。
1 | /** |
上面的代码将允许PHP脚本动态加载PHP扩展,无论它们存储在系统的哪个位置。
1 | // load the C++ extension stored in the same directory as this file |
dl_unrestricted()函数是一个很厉害的函数,但这里要注意:如果你是共享主机平台的管理员,你绝对不要安装它!
Persistent extensions
动态加载的扩展会在请求结束时自动卸载。如果后续的请求也动态加载相同的扩展,那么它将以一个全新的环境开始。如果你想写一个使用静态数据或静态资源的扩展(比如一个持久的数据库连接,或者一个处理任务的工作线程),这不一定是你想要的行为。你要保持数据库连接的活跃性,或者线程的运行,也是在扩展被卸载之后。
为了克服这个问题,Php::dl()函数附带了第二个布尔参数,你可以用它来指定你是想持久地加载扩展,还是只为那个特定的请求加载。
请注意,如果你把这个参数设置为true,唯一持久化的就是扩展中的数据。在后续的请求中,你仍然需要加载扩展来激活其中的函数和类,即使你已经在之前的请求中持续加载了扩展。但由于之前已经加载了扩展,所以其中的静态数据(如数据库连接或线程)会被保存下来。
我们上面演示的dl_unrestricted()函数可以被修改为包含这个持久参数。
1 | /** |
而这个扩展允许我们动态加载扩展,同时保留扩展内部的持久化数据。
1 | // load the C++ extension stored in the same directory as this file, the |