开发 PHP 扩展新途径 通过 FrankenPHP 用 Go 语言编写 PHP 扩展

通过 FrankenPHP 用 Go 语言编写 PHP 扩展

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 代码。有了这个基础,就可以开发各种功能了。

FrankenPHP 作为工具库

从这些实验中发现,尽管有了这些可能性,但是仍然需要花费大量时间编写 C 代码:

  • 扩展在 PHP 中的注册必须用 C 完成,因为这需要操作 Zend Engine 的一些内部指针。好消息是这段代码在所有扩展中基本相同;
  • C 和 Go 之间的类型转换。许多变量类型在 C 和 Go 之间兼容,可以直接使用,比如整数、浮点数和布尔值。然而,更复杂的结构如字符串、对象和数组需要转换,不能直接使用。例如,在 PHP 解释器中,字符串由包含数据和字符串长度的结构表示。因此需要深入了解 PHP 的内部机制,而这些机制有时文档不全。LLM 在解释 PHP 引擎的某些复杂机制方面已经表现出色,但这仍然需要底层知识,并且存在内存损坏的风险。
  • 必须编写 C 代码来调用 Go 代码。

FrankenPHP 在这里发挥了关键作用:作为工具库帮助开发者避免这三个问题。当然,你可以手工完成,相关文档解释了每个步骤,让你清楚了解从头开发扩展的各个阶段。但是,你也可以跳过这些步骤。

对于第一个问题,用 Go 编写的扩展将是 Go 模块,具体来说是 Caddy 模块。FrankenPHP 使用 Caddy 作为集成的 Web 服务器,这使得能够通过一行代码集成自定义模块。特别是,自定义模块可以是带有 init() 函数的 Go 文件,该函数在模块被 Caddy 启动时执行。这是注册扩展的理想位置。得益于项目的最新贡献,FrankenPHP 提供了一个 RegisterExtension() 方法,该方法抽象了注册扩展所需的所有 C 代码。不深入所有细节(如果你好奇,文档中有详细说明),在 Go 中注册扩展看起来是这样的:

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 文件示例:

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 代码。剩下的就是将此文件传递给生成器:

bash 复制代码
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 文件;
  • 包含想要避免编写的中间代码的 C 文件;
  • Go 文件,与原始文件非常相似,但稍作修改以与 C 代码正确协作;
  • 包含某些函数定义的头文件,使 C 和 Go 代码能够良好协作;
  • 带有 PHP 函数签名定义的存根文件;
  • 包含向 PHP 的 Zend Engine 注册新函数所需的所有指令的 arginfo 文件。

正如刚才看到的,Caddy 允许你添加额外的自定义模块来扩展其功能:只需给 Caddy 构建目录,它就会处理其余工作。扩展完全可用,无需手工编写一行 C 代码!

这个生成器的强大之处在于它对其他重要功能的支持。这是一个更完整的兼容文件示例:

go 复制代码
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 扩展提供的所有可能性。然而,对于没有过度高级功能的扩展项目,它可以提供真正的帮助。推荐你查看文档,其中描述了生成器支持的所有功能。

为什么选择 Go 扩展?

可能有人会问:为 PHP 编写 Go 扩展有什么意义?经过三十年的发展,看到 PHP 继续与更新的技术如此良好地集成确实令人振奋。与 Go 的接口需要 FrankenPHP。自从 PHP 基金会最近宣布正式将 FrankenPHP 纳入其管辖以来,它的使用前景将继续增长,长期发展得到了保障。

Go 扩展背后的理念可以用几个关键词概括:goroutines 和包装器。首先,goroutines 是该语言闻名的高性能并发模型。在 PHP 代码中对可能繁重和/或耗时的操作使用 goroutines 的能力带来了广阔的可能性。其次,现有库的包装器同样带来了许多新的可能性。许多 Go 库以其质量而闻名,但在 PHP 中不可用。一个例子是 etcd 缓存系统,开发者 Kévin 为此创建了一个完整的 Go 扩展供 PHP 使用。你可以在扩展仓库中找到这个示例。

提供扩展生成器是朝着 PHP 扩展创建民主化迈出的重要一步,降低了开发门槛。任何人都可以快速尝试,探索生成的代码以了解其工作原理,甚至开发出 PHP 生态系统中的下一个优秀扩展。

如果你想了解更多关于如何编写自己的扩展,文档将解释如何使用生成器,以及如何在不使用生成器的情况下编写自己的 Go 扩展。期待看到这两种语言之间独特的协作关系将如何被运用。

原文-开发 PHP 扩展新途径 通过 FrankenPHP 用 Go 语言编写 PHP 扩展

相关推荐
一直向钱7 小时前
android 基于okhttp的socket封装
android·okhttp
夜晚中的人海8 小时前
【C++】异常介绍
android·java·c++
蜀中廖化12 小时前
Android Studio 导入 opencv
android·opencv·android studio
奋斗的小鹰12 小时前
ASM Bytecode Viewer 插件查看kotlin和java文件的字节码
android·kotlin·asm
come1123413 小时前
Go Modules 包管理 (Go 模块)
开发语言·后端·golang
欢喜躲在眉梢里14 小时前
mysql中的日志
android·运维·数据库·mysql·adb·日志·mysql日志
Hello.Reader15 小时前
优化 Flink 基于状态的 ETL少 Shuffle、不膨胀、可落地的工程
flink·php·etl
路上^_^16 小时前
安卓基础组件019-引导页布局001
android·安卓
梦终剧16 小时前
【Android之路】UI消息循环机制
android·ui
zh_xuan16 小时前
Android android.util.LruCache源码阅读
android·源码阅读·lrucache