在 PHPVerse 2025
大会上(JetBrains 为纪念 PHP 语言 30 周年而组织的会议),FrankenPHP
开发者 Kévin Dunglas 做了一个开创性的宣布:通过 FrankenPHP
,可以使用 Go
语言创建 PHP
扩展。虽然这个功能从项目诞生之初就存在,但今天要深入介绍这个小小的革命,让它变得更加容易上手。
扩展是直接附加到 PHP 解释器的代码段,让你能够实现几乎任何想要的功能。之所以如此强大,主要是因为扩展通常用 C 语言编写,能够提供对机器的高级和低级访问能力。最著名的扩展包括 Parallel(在 PHP 中启用并行代码执行)、GD(图像处理)以及 PHP Redis(与 Redis 缓存服务器通信,还有其替代品如 Snapchat KeyDB)。
最近,也有很多使用 Rust 和 C++ 编写的扩展(虽然 C++ 已经存在很长时间,但不太流行)。如果你想了解更多,Packagist
列出了可以用 PIE
安装的扩展以及它们使用的编程语言。然而,所有这些语言都是非常底层的,学习门槛较高。对于 PHP 扩展开发来说更是如此,通常需要对 PHP 内部机制有相当深入的了解。既然 FrankenPHP
是用更高级的语言 Go 编写的,为什么不借此机会尝试用 Go 来编写 PHP 扩展呢?实际上,FrankenPHP 集成了 Go 运行时,所以完全可以使用这种语言,除了之前没人这么做过之外,没有任何特殊的技术限制。
Kévin Dunglas
团队开始探索这种可能性,并开发了一个概念验证,在其中向 PHP 添加一个新的原生函数来执行 Go 代码。经过几天的研究,结果出炉了:成功从 PHP 启动了一个 goroutine(与主代码并行执行的代码段)!这一切的关键在于 CGO
库,它让 C 代码能够调用 Go 代码,也能够从 Go 调用 C 代码。有了这个基础,就可以开发各种功能了。
从这些实验中发现,尽管有了这些可能性,但是仍然需要花费大量时间编写 C 代码:
FrankenPHP 在这里发挥了关键作用:作为工具库帮助开发者避免这三个问题。当然,你可以手工完成,相关文档解释了每个步骤,让你清楚了解从头开发扩展的各个阶段。但是,你也可以跳过这些步骤。
对于第一个问题,用 Go 编写的扩展将是 Go 模块,具体来说是 Caddy 模块。FrankenPHP 使用 Caddy 作为集成的 Web 服务器,这使得能够通过一行代码集成自定义模块。特别是,自定义模块可以是带有 init()
函数的 Go 文件,该函数在模块被 Caddy 启动时执行。这是注册扩展的理想位置。得益于项目的最新贡献,FrankenPHP 提供了一个 RegisterExtension()
方法,该方法抽象了注册扩展所需的所有 C 代码。不深入所有细节(如果你好奇,文档中有详细说明),在 Go 中注册扩展看起来是这样的:
package ext
/*
#include "ext.h"
*/
import "C"
import "github.com/dunglas/frankenphp"
func init() {
frankenphp.RegisterExtension(unsafe.Pointer(&C.ext_module_entry))
}
这里没有写一行 C 代码,扩展就成功注册了!
对于类型转换问题,FrankenPHP 再次提供了公开的功能来抽象内部类型机制。正如之前看到的,一些标量类型不需要转换。但其他类型需要,比如字符串。这就是为什么 FrankenPHP 提供了诸如 frankenphp.GoString()
(从 C 字符串获取 Go 字符串)和 frankenphp.PHPString()
(将 Go 字符串转换为 PHP 可用的字符串)等方法,这些方法自动处理转换。随着时间的推移,会为其他数据类型添加更多工具方法。数组已经得到支持,可调用对象正在开发中。
再次强调,这篇文章只是触及了这些新功能的表面,完整的文档已经在线,其中有详细的解释。
如你所见,FrankenPHP 在促进 PHP 扩展创建方面发挥着关键作用,提供了大大简化扩展编码的工具。然而,仍有最后一个问题需要解决:编写调用 Go 代码所需的 C 代码。接下来看看如何解决这个问题。
意识到需要编写数十行甚至数百行 C 代码来实现 C 和 Go 之间的"桥接",开发团队思考了下一步。是否可能创建一个 PHP 扩展生成器,它接收一个简单的 Go 文件作为输入(可能带有一些特殊语法),然后完成其余工作?答案是肯定的!经过几周的开发,一个新工具已被集成到 FrankenPHP 中作为子命令:扩展生成器。主要目标很明确:开发者必须能够编译和集成 PHP 扩展,而无需编写一行 C 代码。
生成器定义了特定的 Go 指令。把 Go 指令想象成 PHP 中的注解或属性。这是一个与扩展生成器兼容的 Go 文件示例:
package main
// export_php:function multiply(int $a, int $b): int
func multiply(a int64, b int64) int64 {
return a * b
}
// export_php:function is_even(int $a): bool
func is_even(a int64) bool {
return a%2 == 0
}
// export_php:function float_div(float $a, float $b): float
func float_div(a float64, b float64) float64 {
return a / b
}
通过 // export_php:function
指令后跟函数签名,生成器将负责定义所有中间 C 代码。剩下的就是将此文件传递给生成器:
alex@alex-macos frankenphp % frankenphp extension-init ext-dir/ext.go
2025/06/20 09:49:09.273 INFO PHP 扩展 "ext" 在 "ext-dir/build" 中初始化成功
alex@alex-macos frankenphp % ls -la ext-dir/build
total 48
drwxr-xr-x@ 8 alex staff 256 Jun 20 11:49 .
drwxr-xr-x@ 8 alex staff 256 Jun 20 11:49 ..
-rw-r--r--@ 1 alex staff 418 Jun 20 11:49 README.md
-rw-r--r--@ 1 alex staff 1673 Jun 20 11:49 ext.c
-rw-r--r--@ 1 alex staff 396 Jun 20 11:49 ext.go
-rw-r--r--@ 1 alex staff 226 Jun 20 11:49 ext.h
-rw-r--r--@ 1 alex staff 168 Jun 20 11:49 ext.stub.php
-rw-r--r--@ 1 alex staff 865 Jun 20 11:49 ext_arginfo.h
FrankenPHP 创建了 PHP 扩展所需的所有文件:
README.md
文件;正如刚才看到的,Caddy 允许你添加额外的自定义模块来扩展其功能:只需给 Caddy 构建目录,它就会处理其余工作。扩展完全可用,无需手工编写一行 C 代码!
这个生成器的强大之处在于它对其他重要功能的支持。这是一个更完整的兼容文件示例:
package main
// export_php:namespace Go\MyExtension
import (
"C"
"github.com/dunglas/frankenphp"
"strings"
"unsafe"
)
// export_php:const
const MY_GLOBAL_CONSTANT = "Hello, World!"
// export_php:classconst MySuperClass
const STR_REVERSE = iota
// export_php:classconst MySuperClass
const STR_NORMAL = iota
// export_php:class MySuperClass
type MyClass struct {
Name string
}
// export_php:method MySuperClass::setName(string $name): void
func (mc *MyClass) SetName(v *C.zend_string) {
// 将 C 字符串转换为 Go 字符串
mc.Name = frankenphp.GoString(unsafe.Pointer(v))
}
// export_php:method MySuperClass::getName(): string
func (mc *MyClass) GetName() unsafe.Pointer {
// 将 Go 字符串转换为 PHP 字符串
return frankenphp.PHPString(mc.Name, false)
}
// export_php:method MySuperClass::repeatName(int $count, ?int $mode): void
func (mc *MyClass) RepeatName(count int64, mode *int64) {
str := mc.Name
// 重复字符串指定次数
result := strings.Repeat(str, int(count))
if mode != nil && *mode == STR_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)
}
if mode == nil || *mode == STR_NORMAL {
// 正常模式,无需操作,只是为了使用常量 :)
}
mc.Name = result
}
在这里可以看到全局常量、类、类常量和类方法的声明。另外需要注意字符串类型转换方法的使用。需要说明的是,生成器几乎无法覆盖 PHP 扩展提供的所有可能性。然而,对于没有过度高级功能的扩展项目,它可以提供真正的帮助。推荐你查看文档,其中描述了生成器支持的所有功能。
可能有人会问:为 PHP 编写 Go 扩展有什么意义?经过三十年的发展,看到 PHP 继续与更新的技术如此良好地集成确实令人振奋。与 Go 的接口需要 FrankenPHP。自从 PHP 基金会最近宣布正式将 FrankenPHP 纳入其管辖以来,它的使用前景将继续增长,长期发展得到了保障。
Go 扩展背后的理念可以用几个关键词概括:goroutines
和包装器。首先,goroutines 是该语言闻名的高性能并发模型。在 PHP 代码中对可能繁重和/或耗时的操作使用 goroutines 的能力带来了广阔的可能性。其次,现有库的包装器同样带来了许多新的可能性。许多 Go 库以其质量而闻名,但在 PHP 中不可用。一个例子是 etcd
缓存系统,开发者 Kévin 为此创建了一个完整的 Go 扩展供 PHP 使用。你可以在扩展仓库中找到这个示例。
提供扩展生成器是朝着 PHP 扩展创建民主化迈出的重要一步,降低了开发门槛。任何人都可以快速尝试,探索生成的代码以了解其工作原理,甚至开发出 PHP 生态系统中的下一个优秀扩展。
如果你想了解更多关于如何编写自己的扩展,文档将解释如何使用生成器,以及如何在不使用生成器的情况下编写自己的 Go 扩展。期待看到这两种语言之间独特的协作关系将如何被运用。