该页面已由社区翻译,可能不是最新的。参见参考版本(英文)。
该页面已由社区翻译,可能不是最新的。参见参考版本(英文)。
该页面已由社区翻译,可能不是最新的。参见参考版本(英文)。
使用 FrankenPHP,你可以使用 Go 编写 PHP 扩展,这允许你创建高性能的原生函数,可以直接从 PHP 调用。你的应用程序可以利用任何现有或新的 Go 库,以及直接从你的 PHP 代码中使用协程(goroutines)的并发模型。
编写 PHP 扩展通常使用 C 语言完成,但通过一些额外的工作,也可以使用其他语言编写。PHP 扩展允许你利用底层语言的强大功能来扩展 PHP 的功能,例如,通过添加原生函数或优化特定操作。
借助 Caddy 模块,你可以使用 Go 编写 PHP 扩展,并将其快速集成到 FrankenPHP 中。
FrankenPHP 提供两种方式来创建 Go 语言的 PHP 扩展:
我们将从生成器方法开始,因为这是最简单的入门方式,然后为那些需要完全控制的人展示手动实现。
FrankenPHP 捆绑了一个工具,允许你仅使用 Go 创建 PHP 扩展。无需编写 C 代码或直接使用 CGO:FrankenPHP 还包含一个公共类型 API,帮助你在 Go 中编写扩展,而无需担心PHP/C 和 Go 之间的类型转换。
Tip
如果你想了解如何从头开始在 Go 中编写扩展,可以阅读下面的手动实现部分,该部分演示了如何在不使用生成器的情况下在 Go 中编写 PHP 扩展。
请记住,此工具不是功能齐全的扩展生成器。它旨在帮助你在 Go 中编写简单的扩展,但它不提供 PHP 扩展的最高级功能。如果你需要编写更复杂和优化的扩展,你可能需要编写一些 C 代码或直接使用 CGO。
正如下面的手动实现部分所涵盖的,你需要获取 PHP 源代码并创建一个新的 Go 模块。
在 Go 中编写 PHP 扩展的第一步是创建一个新的 Go 模块。你可以使用以下命令:
go mod init example.com/example
第二步是为后续步骤获取 PHP 源代码。获取后,将它们解压到你选择的目录中,不要放在你的 Go 模块内:
tar xf php-*
现在一切都设置好了,可以在 Go 中编写你的原生函数。创建一个名为 stringext.go 的新文件。我们的第一个函数将接受一个字符串作为参数,重复次数,一个布尔值来指示是否反转字符串,并返回结果字符串。这应该看起来像这样:
package example
// #include <Zend/zend_types.h>
import "C"
import (
"strings"
"unsafe"
"github.com/dunglas/frankenphp"
)
//export_php:function repeat_this(string $str, int $count, bool $reverse): string
func repeat_this(s *C.zend_string, count int64, reverse bool) unsafe.Pointer {
str := frankenphp.GoString(unsafe.Pointer(s))
result := strings.Repeat(str, int(count))
if reverse {
runes := []rune(result)
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
runes[i], runes[j] = runes[j], runes[i]
}
result = string(runes)
}
return frankenphp.PHPString(result, false)
}
这里有两个重要的事情要注意:
//export_php:function 定义了 PHP 中的函数签名。这是生成器知道如何使用正确的参数和返回类型生成 PHP 函数的方式;unsafe.Pointer。FrankenPHP 提供了一个 API 来帮助你在 C 和 Go 之间进行类型转换。虽然第一点不言自明,但第二点可能更难理解。让我们在本指南的后面深入了解类型转换。
这就是魔法发生的地方,现在可以生成你的扩展。你可以使用以下命令运行生成器:
GEN_STUB_SCRIPT=php-src/build/gen_stub.php frankenphp extension-init my_extension.go
Note
不要忘记将
GEN_STUB_SCRIPT环境变量设置为你之前下载的 PHP 源代码中gen_stub.php文件的路径。这是在手动实现部分中提到的同一个gen_stub.php脚本。
如果一切顺利,你的项目目录应该包含以下扩展文件:
my_extension.go - 你的原始源文件(保持不变)my_extension_generated.go - 生成的文件,包含调用你的函数的 CGO 包装器my_extension.stub.php - 用于 IDE 自动补全的 PHP 存根文件my_extension_arginfo.h - PHP 参数信息my_extension.h - C 头文件my_extension.c - C 实现文件README.md - 文档Important
你的源文件(
my_extension.go)从未被修改。 生成器会创建一个单独的_generated.go文件,其中包含调用你的原始函数的 CGO 包装器。这意味着你可以安全地对源文件进行版本控制,而无需担心生成的代码污染它。
我们的扩展现在已准备好编译并集成到 FrankenPHP 中。为此,请参阅 FrankenPHP 编译文档以了解如何编译 FrankenPHP。使用 --with 标志添加模块,指向你的模块路径:
CGO_ENABLED=1 \
XCADDY_GO_BUILD_FLAGS="-ldflags='-w -s' -tags=nobadger,nomysql,nopgx" \
CGO_CFLAGS=$(php-config --includes) \
CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" \
xcaddy build \
--output frankenphp \
--with github.com/my-account/my-module/build
请注意,你指向在生成步骤中创建的 /build 子目录。但是,这不是强制性的:你也可以将生成的文件复制到你的模块目录并直接指向它。
你可以创建一个 PHP 文件来测试你创建的函数和类。例如,创建一个包含以下内容的 index.php 文件:
<?php
// 使用全局常量
var_dump(repeat_this('Hello World', 5, STR_REVERSE));
// 使用类常量
$processor = new StringProcessor();
echo $processor->process('Hello World', StringProcessor::MODE_LOWERCASE); // "hello world"
echo $processor->process('Hello World', StringProcessor::MODE_UPPERCASE); // "HELLO WORLD"
一旦你按照上一节所示将扩展集成到 FrankenPHP 中,你就可以使用 ./frankenphp php-server 运行此测试文件,你应该看到你的扩展正在工作。
虽然一些变量类型在 C/PHP 和 Go 之间具有相同的内存表示,但某些类型需要更多逻辑才能直接使用。这可能是编写扩展时最困难的部分,因为它需要了解 Zend 引擎的内部结构以及变量在 PHP 中的内部存储方式。此表总结了你需要知道的内容:
| PHP 类型 | Go 类型 | 直接转换 | C 到 Go 助手 | Go 到 C 助手 | 类方法支持 |
|---|---|---|---|---|---|
int | int64 | ✅ | - | - | ✅ |
?int | *int64 | ✅ | - | - | ✅ |
float | float64 | ✅ | - | - | ✅ |
?float | *float64 | ✅ | - | - | ✅ |
bool | bool | ✅ | - | - | ✅ |
?bool | *bool | ✅ | - | - | ✅ |
string/?string | *C.zend_string | ❌ | frankenphp.GoString() | frankenphp.PHPString() | ✅ |
array | frankenphp.AssociativeArray | ❌ | frankenphp.GoAssociativeArray() | frankenphp.PHPAssociativeArray() | ✅ |
array | map[string]any | ❌ | frankenphp.GoMap() | frankenphp.PHPMap() | ✅ |
array | []any | ❌ | frankenphp.GoPackedArray() | frankenphp.PHPPackedArray() | ✅ |
mixed | any | ❌ | GoValue() | PHPValue() | ❌ |
callable | *C.zval | ❌ | - | frankenphp.CallPHPCallable() | ❌ |
object | struct | ❌ | 尚未实现 | 尚未实现 | ❌ |
Note
此表尚不详尽,将随着 FrankenPHP 类型 API 变得更加完整而完善。
特别是对于类方法,目前支持原始类型和数组。对象尚不能用作方法参数或返回类型。
如果你参考上一节的代码片段,你可以看到助手用于转换第一个参数和返回值。我们的 repeat_this() 函数的第二和第三个参数不需要转换,因为底层类型的内存表示对于 C 和 Go 都是相同的。
FrankenPHP 通过 frankenphp.AssociativeArray 或直接转换为 map 或 slice 为 PHP 数组提供原生支持。
AssociativeArray 表示一个哈希映射,由 Map: map[string]any 字段和可选的 Order: []string 字段组成(与 PHP“关联数组”不同,Go map 不是有序的)。
如果不需要顺序或关联,也可以直接转换为 slice []any 或无序 map map[string]any。
在 Go 中创建和操作数组:
package example
// #include <Zend/zend_types.h>
import "C"
import (
"unsafe"
"github.com/dunglas/frankenphp"
)
// export_php:function process_data_ordered(array $input): array
func process_data_ordered_map(arr *C.zend_array) unsafe.Pointer {
// Convert PHP associative array to Go while keeping the order
associativeArray, err := frankenphp.GoAssociativeArray[any](/zh/docs/unsafe.Pointer(arr))
if err != nil {
// handle error
}
// loop over the entries in order
for _, key := range associativeArray.Order {
value, _ := associativeArray.Map[key]
// do something with key and value
_ = value // suppress unused error
}
// return an ordered array
// if 'Order' is not empty, only the key-value pairs in 'Order' will be respected
return frankenphp.PHPAssociativeArray[string](/zh/docs/frankenphp.AssociativeArray[string]{
Map: map[string]string{
"key1": "value1",
"key2": "value2",
},
Order: []string{"key1", "key2"},
})
}
// export_php:function process_data_unordered(array $input): array
func process_data_unordered_map(arr *C.zend_array) unsafe.Pointer {
// Convert PHP associative array to a Go map without keeping the order
// ignoring the order will be more performant
goMap, err := frankenphp.GoMap[any](/zh/docs/unsafe.Pointer(arr))
if err != nil {
// handle error
}
// loop over the entries in no specific order
for key, value := range goMap {
// do something with key and value
_, _ = key, value // suppress unused error
}
// return an unordered array
return frankenphp.PHPMap(map[string]string {
"key1": "value1",
"key2": "value2",
})
}
// export_php:function process_data_packed(array $input): array
func process_data_packed(arr *C.zend_array) unsafe.Pointer {
// Convert PHP packed array to Go
goSlice, err := frankenphp.GoPackedArray(unsafe.Pointer(arr))
if err != nil {
// handle error
}
// loop over the slice in order
for index, value := range goSlice {
// do something with index and value
_, _ = index, value // suppress unused error
}
// return a packed array
return frankenphp.PHPPackedArray([]string{"value1", "value2", "value3"})
}
数组转换的关键特性:
int64、float64、string、bool、nil、AssociativeArray、map[string]any、[]any)null 值。frankenphp.PHPAssociativeArray(arr frankenphp.AssociativeArray) unsafe.Pointer - 转换为具有键值对的有序 PHP 数组frankenphp.PHPMap(arr map[string]any) unsafe.Pointer - 将 map 转换为具有键值对的无序 PHP 数组frankenphp.PHPPackedArray(slice []any) unsafe.Pointer - 将 slice 转换为仅包含索引值的 PHP 打包数组frankenphp.GoAssociativeArray(arr unsafe.Pointer, ordered bool) frankenphp.AssociativeArray - 将 PHP 数组转换为有序 Go AssociativeArray(带顺序的 map)frankenphp.GoMap(arr unsafe.Pointer) map[string]any - 将 PHP 数组转换为无序 Go mapfrankenphp.GoPackedArray(arr unsafe.Pointer) []any - 将 PHP 数组转换为 Go slicefrankenphp.IsPacked(zval *C.zend_array) bool - 检查 PHP 数组是否为打包(仅索引)或关联(键值对)FrankenPHP 提供了一种使用 frankenphp.CallPHPCallable 助手来处理 PHP 可调用对象的方法。这允许你从 Go 代码中调用 PHP 函数或方法。
为了演示这一点,让我们创建自己的 array_map() 函数,它接受一个可调用对象和一个数组,将可调用对象应用于数组的每个元素,并返回一个包含结果的新数组:
// export_php:function my_array_map(array $data, callable $callback): array
func my_array_map(arr *C.zend_array, callback *C.zval) unsafe.Pointer {
goSlice, err := frankenphp.GoPackedArray[any](/zh/docs/unsafe.Pointer(arr))
if err != nil {
panic(err)
}
result := make([]any, len(goSlice))
for index, value := range goSlice {
result[index] = frankenphp.CallPHPCallable(unsafe.Pointer(callback), []interface{}{value})
}
return frankenphp.PHPPackedArray(result)
}
请注意我们如何使用 frankenphp.CallPHPCallable() 调用作为参数传递的 PHP 可调用对象。此函数接受指向可调用对象的指针和参数数组,并返回可调用对象执行的结果。你可以使用你习惯的可调用语法:
<?php
$result = my_array_map([1, 2, 3], function($x) { return $x * 2; });
// $result 将是 [2, 4, 6]
$result = my_array_map(['hello', 'world'], 'strtoupper');
// $result 将是 ['HELLO', 'WORLD']
生成器支持将 Go 结构体声明为不透明类,可用于创建 PHP 对象。你可以使用 //export_php:class 指令注释来定义 PHP 类。例如:
package example
//export_php:class User
type UserStruct struct {
Name string
Age int
}
不透明类是内部结构(属性)对 PHP 代码隐藏的类。这意味着:
$user->name 不起作用)这种方法提供了更好的封装,并防止 PHP 代码意外破坏 Go 对象的内部状态。与对象的所有交互都必须通过你明确定义的方法进行。
由于属性不能直接访问,你必须定义方法来与不透明类交互。使用 //export_php:method 指令来定义行为:
package example
// #include <Zend/zend_types.h>
import "C"
import (
"unsafe"
"github.com/dunglas/frankenphp"
)
//export_php:class User
type UserStruct struct {
Name string
Age int
}
//export_php:method User::getName(): string
func (us *UserStruct) GetUserName() unsafe.Pointer {
return frankenphp.PHPString(us.Name, false)
}
//export_php:method User::setAge(int $age): void
func (us *UserStruct) SetUserAge(age int64) {
us.Age = int(age)
}
//export_php:method User::getAge(): int
func (us *UserStruct) GetUserAge() int64 {
return int64(us.Age)
}
//export_php:method User::setNamePrefix(string $prefix = "User"): void
func (us *UserStruct) SetNamePrefix(prefix *C.zend_string) {
us.Name = frankenphp.GoString(unsafe.Pointer(prefix)) + ": " + us.Name
}
生成器支持在 PHP 签名中使用 ? 前缀的可空参数。当参数可空时,它在你的 Go 函数中变成指针,允许你检查值在 PHP 中是否为 null:
package example
// #include <Zend/zend_types.h>
import "C"
import (
"unsafe"
"github.com/dunglas/frankenphp"
)
//export_php:method User::updateInfo(?string $name, ?int $age, ?bool $active): void
func (us *UserStruct) UpdateInfo(name *C.zend_string, age *int64, active *bool) {
// 检查是否提供了 name(不为 null)
if name != nil {
us.Name = frankenphp.GoString(unsafe.Pointer(name))
}
// 检查是否提供了 age(不为 null)
if age != nil {
us.Age = int(*age)
}
// 检查是否提供了 active(不为 null)
if active != nil {
us.Active = *active
}
}
关于可空参数的要点:
?int、?float、?bool)在 Go 中变成指针(*int64、*float64、*bool)?string)仍然是 *C.zend_string,但可以是 nilnilnull 变成 Go nil - 当 PHP 传递 null 时,你的 Go 函数接收 nil 指针Warning
目前,类方法有以下限制。不支持对象作为参数类型或返回类型。完全支持数组作为参数和返回类型。支持的类型:
string、int、float、bool、array和void(用于返回类型)。完全支持可空参数类型,适用于所有标量类型(?string、?int、?float、?bool)。
生成扩展后,你将被允许在 PHP 中使用类及其方法。请注意,你不能直接访问属性:
<?php
$user = new User();
// ✅ 这可以工作 - 使用方法
$user->setAge(25);
echo $user->getName(); // 输出:(空,默认值)
echo $user->getAge(); // 输出:25
$user->setNamePrefix("Employee");
// ✅ 这也可以工作 - 可空参数
$user->updateInfo("John", 30, true); // 提供所有参数
$user->updateInfo("Jane", null, false); // Age 为 null
$user->updateInfo(null, 25, null); // Name 和 active 为 null
// ❌ 这不会工作 - 直接属性访问
// echo $user->name; // 错误:无法访问私有属性
// $user->age = 30; // 错误:无法访问私有属性
这种设计确保你的 Go 代码完全控制如何访问和修改对象的状态,提供更好的封装和类型安全。
生成器支持使用两个指令将 Go 常量导出到 PHP://export_php:const 用于全局常量,//export_php:classconst 用于类常量。这允许你在 Go 和 PHP 代码之间共享配置值、状态代码和其他常量。
使用 //export_php:const 指令创建全局 PHP 常量:
package example
//export_php:const
const MAX_CONNECTIONS = 100
//export_php:const
const API_VERSION = "1.2.3"
//export_php:const
const (
STATUS_OK = iota
STATUS_ERROR
)
Note
PHP 常量将采用 Go 常量的名称,因此建议使用大写字母。
使用 //export_php:classconst ClassName 指令创建属于特定 PHP 类的常量:
package example
//export_php:classconst User
const STATUS_ACTIVE = 1
//export_php:classconst User
const STATUS_INACTIVE = 0
//export_php:classconst User
const ROLE_ADMIN = "admin"
//export_php:classconst Order
const (
STATE_PENDING = iota
STATE_PROCESSING
STATE_COMPLETED
)
Note
就像全局常量一样,类常量将采用 Go 常量的名称。
类常量在 PHP 中使用类名作用域访问:
<?php
// 全局常量
echo MAX_CONNECTIONS; // 100
echo API_VERSION; // "1.2.3"
// 类常量
echo User::STATUS_ACTIVE; // 1
echo User::ROLE_ADMIN; // "admin"
echo Order::STATE_PENDING; // 0
该指令支持各种值类型,包括字符串、整数、布尔值、浮点数和 iota 常量。使用 iota 时,生成器自动分配顺序值(0、1、2 等)。全局常量在你的 PHP 代码中作为全局常量可用,而类常量使用公共可见性限定在各自的类中。使用整数时,支持不同的可能记法(二进制、十六进制、八进制)并在 PHP 存根文件中按原样转储。
你可以像在 Go 代码中习惯的那样使用常量。例如,让我们采用我们之前声明的 repeat_this() 函数,并将最后一个参数更改为整数:
package example
// #include <Zend/zend_types.h>
import "C"
import (
"strings"
"unsafe"
"github.com/dunglas/frankenphp"
)
//export_php:const
const STR_REVERSE = iota
//export_php:const
const STR_NORMAL = iota
//export_php:classconst StringProcessor
const MODE_LOWERCASE = 1
//export_php:classconst StringProcessor
const MODE_UPPERCASE = 2
//export_php:function repeat_this(string $str, int $count, int $mode): string
func repeat_this(s *C.zend_string, count int64, mode int) unsafe.Pointer {
str := frankenphp.GoString(unsafe.Pointer(s))
result := strings.Repeat(str, int(count))
if mode == STR_REVERSE {
// reverse the string
}
if mode == STR_NORMAL {
// no-op, just to showcase the constant
}
return frankenphp.PHPString(result, false)
}
//export_php:class StringProcessor
type StringProcessorStruct struct {
// internal fields
}
//export_php:method StringProcessor::process(string $input, int $mode): string
func (sp *StringProcessorStruct) Process(input *C.zend_string, mode int64) unsafe.Pointer {
str := frankenphp.GoString(unsafe.Pointer(input))
switch mode {
case MODE_LOWERCASE:
str = strings.ToLower(str)
case MODE_UPPERCASE:
str = strings.ToUpper(str)
}
return frankenphp.PHPString(str, false)
}
生成器支持使用 //export_php:namespace 指令将 PHP 扩展的函数、类和常量组织在命名空间下。这有助于避免命名冲突,并为扩展的 API 提供更好的组织。
在你的 Go 文件顶部使用 //export_php:namespace 指令,将所有导出的符号放在特定命名空间下:
//export_php:namespace My\Extension
package example
import (
"unsafe"
"github.com/dunglas/frankenphp"
)
//export_php:function hello(): string
func hello() string {
return "Hello from My\\Extension namespace!"
}
//export_php:class User
type UserStruct struct {
// internal fields
}
//export_php:method User::getName(): string
func (u *UserStruct) GetName() unsafe.Pointer {
return frankenphp.PHPString("John Doe", false)
}
//export_php:const
const STATUS_ACTIVE = 1
当声明命名空间时,所有函数、类和常量都放在 PHP 中的该命名空间下:
<?php
echo My\Extension\hello(); // "Hello from My\Extension namespace!"
$user = new My\Extension\User();
echo $user->getName(); // "John Doe"
echo My\Extension\STATUS_ACTIVE; // 1
\)作为分隔符。如果你想了解扩展的工作原理或需要完全控制你的扩展,你可以手动编写它们。这种方法给你完全的控制,但需要更多的样板代码。
我们将看到如何在 Go 中编写一个简单的 PHP 扩展,定义一个新的原生函数。此函数将从 PHP 调用,并将触发一个在 Caddy 日志中记录消息的协程。此函数不接受任何参数并且不返回任何内容。
在你的模块中,你需要定义一个新的原生函数,该函数将从 PHP 调用。为此,创建一个你想要的名称的文件,例如 extension.go,并添加以下代码:
package example
// #include "extension.h"
import "C"
import (
"log/slog"
"unsafe"
"github.com/dunglas/frankenphp"
)
func init() {
frankenphp.RegisterExtension(unsafe.Pointer(&C.ext_module_entry))
}
//export go_print_something
func go_print_something() {
go func() {
slog.Info("Hello from a goroutine!")
}()
}
frankenphp.RegisterExtension() 函数通过处理内部 PHP 注册逻辑简化了扩展注册过程。go_print_something 函数使用 //export 指令表示它将在我们将编写的 C 代码中可访问,这要归功于 CGO。
在此示例中,我们的新函数将触发一个在 Caddy 日志中记录消息的协程。
为了允许 PHP 调用我们的函数,我们需要定义相应的 PHP 函数。为此,我们将创建一个存根文件,例如 extension.stub.php,其中包含以下代码:
<?php
/** @generate-class-entries */
function go_print(): void {}
此文件定义了 go_print() 函数的签名,该函数将从 PHP 调用。@generate-class-entries 指令允许 PHP 自动为我们的扩展生成函数条目。
这不是手动完成的,而是使用 PHP 源代码中提供的脚本(确保根据你的 PHP 源代码所在位置调整 gen_stub.php 脚本的路径):
php ../php-src/build/gen_stub.php extension.stub.php
此脚本将生成一个名为 extension_arginfo.h 的文件,其中包含 PHP 知道如何定义和调用我们函数所需的信息。
现在,我们需要编写 Go 和 C 之间的桥梁。在你的模块目录中创建一个名为 extension.h 的文件,内容如下:
#ifndef _EXTENSION_H
#define _EXTENSION_H
#include <php.h>
extern zend_module_entry ext_module_entry;
#endif
接下来,创建一个名为 extension.c 的文件,该文件将执行以下步骤:
go_print();让我们首先包含所需的头文件:
#include <php.h>
#include "extension.h"
#include "extension_arginfo.h"
// 包含 Go 导出的符号
#include "_cgo_export.h"
然后我们将 PHP 函数定义为原生语言函数:
PHP_FUNCTION(go_print)
{
ZEND_PARSE_PARAMETERS_NONE();
go_print_something();
}
zend_module_entry ext_module_entry = {
STANDARD_MODULE_HEADER,
"ext_go",
ext_functions, /* Functions */
NULL, /* MINIT */
NULL, /* MSHUTDOWN */
NULL, /* RINIT */
NULL, /* RSHUTDOWN */
NULL, /* MINFO */
"0.1.1",
STANDARD_MODULE_PROPERTIES
};
在这种情况下,我们的函数不接受参数并且不返回任何内容。它只是调用我们之前定义的 Go 函数,使用 //export 指令导出。
最后,我们在 zend_module_entry 结构中定义扩展的元数据,例如其名称、版本和属性。这些信息对于 PHP 识别和加载我们的扩展是必需的。请注意,ext_functions 是指向我们定义的 PHP 函数的指针数组,它由 gen_stub.php 脚本在 extension_arginfo.h 文件中自动生成。
扩展注册由我们在 Go 代码中调用的 FrankenPHP 的 RegisterExtension() 函数自动处理。
现在我们知道了如何在 Go 中创建基本的 PHP 扩展,让我们复杂化我们的示例。我们现在将创建一个 PHP 函数,该函数接受一个字符串作为参数并返回其大写版本。
为了定义新的 PHP 函数,我们将修改我们的 extension.stub.php 文件以包含新的函数签名:
<?php
/** @generate-class-entries */
/**
* Converts a string to uppercase.
*
* @param string $string The string to convert.
* @return string The uppercase version of the string.
*/
function go_upper(string $string): string {}
Tip
不要忽视函数的文档!你可能会与其他开发人员共享扩展存根,以记录如何使用你的扩展以及哪些功能可用。
通过使用 gen_stub.php 脚本重新生成存根文件,extension_arginfo.h 文件应该如下所示:
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_go_upper, 0, 1, IS_STRING, 0)
ZEND_ARG_TYPE_INFO(0, string, IS_STRING, 0)
ZEND_END_ARG_INFO()
ZEND_FUNCTION(go_upper);
static const zend_function_entry ext_functions[] = {
ZEND_FE(go_upper, arginfo_go_upper)
ZEND_FE_END
};
我们可以看到 go_upper 函数定义了一个 string 类型的参数和一个 string 的返回类型。
你的 Go 函数不能直接接受 PHP 字符串作为参数。你需要将其转换为 Go 字符串。幸运的是,FrankenPHP 提供了助手函数来处理 PHP 字符串和 Go 字符串之间的转换,类似于我们在生成器方法中看到的。
头文件保持简单:
#ifndef _EXTENSION_H
#define _EXTENSION_H
#include <php.h>
extern zend_module_entry ext_module_entry;
#endif
我们现在可以在我们的 extension.c 文件中编写 Go 和 C 之间的桥梁。我们将 PHP 字符串直接传递给我们的 Go 函数:
PHP_FUNCTION(go_upper)
{
zend_string *str;
ZEND_PARSE_PARAMETERS_START(1, 1)
Z_PARAM_STR(str)
ZEND_PARSE_PARAMETERS_END();
zend_string *result = go_upper(str);
RETVAL_STR(result);
}
你可以在 PHP 内部手册 的专门页面中了解更多关于 ZEND_PARSE_PARAMETERS_START 和参数解析的信息。在这里,我们告诉 PHP 我们的函数接受一个 string 类型的强制参数作为 zend_string。然后我们将此字符串直接传递给我们的 Go 函数,并使用 RETVAL_STR 返回结果。
只剩下一件事要做:在 Go 中实现 go_upper 函数。
我们的 Go 函数将接受 *C.zend_string 作为参数,使用 FrankenPHP 的助手函数将其转换为 Go 字符串,处理它,并将结果作为新的 *C.zend_string 返回。助手函数为我们处理所有内存管理和转换复杂性。
package example
// #include <Zend/zend_types.h>
import "C"
import (
"unsafe"
"strings"
"github.com/dunglas/frankenphp"
)
//export go_upper
func go_upper(s *C.zend_string) *C.zend_string {
str := frankenphp.GoString(unsafe.Pointer(s))
upper := strings.ToUpper(str)
return (*C.zend_string)(frankenphp.PHPString(upper, false))
}
这种方法比手动内存管理更清洁、更安全。FrankenPHP 的助手函数自动处理 PHP 的 zend_string 格式和 Go 字符串之间的转换。PHPString() 中的 false 参数表示我们想要创建一个新的非持久字符串(在请求结束时释放)。
Tip
在此示例中,我们不执行任何错误处理,但你应该始终检查指针不是
nil并且数据在 Go 函数中使用之前是有效的。
我们的扩展现在已准备好编译并集成到 FrankenPHP 中。为此,请参阅 FrankenPHP 编译文档以了解如何编译 FrankenPHP。使用 --with 标志添加模块,指向你的模块路径:
CGO_ENABLED=1 \
XCADDY_GO_BUILD_FLAGS="-ldflags='-w -s' -tags=nobadger,nomysql,nopgx" \
CGO_CFLAGS=$(php-config --includes) \
CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" \
xcaddy build \
--output frankenphp \
--with github.com/my-account/my-module
就是这样!你的扩展现在集成到 FrankenPHP 中,可以在你的 PHP 代码中使用。
将扩展集成到 FrankenPHP 后,你可以为你实现的函数创建一个包含示例的 index.php 文件:
<?php
// 测试基本函数
go_print();
// 测试高级函数
echo go_upper("hello world") . "\n";
你现在可以使用 ./frankenphp php-server 运行带有此文件的 FrankenPHP,你应该看到你的扩展正在工作。