news 2026/9/15 20:32:28

FrankenPHP 扩展开发完全指南:使用 Go 编写 PHP 扩展模块

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
FrankenPHP 扩展开发完全指南:使用 Go 编写 PHP 扩展模块

FrankenPHP 扩展开发完全指南:使用 Go 编写 PHP 扩展模块

【免费下载链接】frankenphp🧟 The modern PHP app server项目地址: https://gitcode.com/GitHub_Trending/fr/frankenphp

FrankenPHP 允许开发者使用 Go 语言编写 PHP 扩展模块,将高性能的原生函数直接暴露给 PHP 代码调用,并能在 PHP 中直接利用 goroutine 成熟的并发模型。本指南将完整讲解两条实现路线——官方推荐的「扩展生成器」自动化路线(无需手写任何 C 代码)与追求完全控制的「手动实现」路线,涵盖类型转换、数组与 callable 处理、原生类声明、常量与命名空间导出,以及最终通过 xcaddy 将扩展编译进 FrankenPHP 的全部流程。

PHP 扩展传统上使用 C 编写,而 FrankenPHP 借助其 Caddy 模块架构,为 Go 语言打通了通往 PHP 扩展世界的桥梁:你既可以利用现有 Go 库的能力,也可以在 PHP 代码中直接使用 goroutine 的并发模型。本文对应的官方文档为 docs/ja/extensions.md(英文版见 docs/extensions.md),源码实现位于 internal/extgen 与 types.go,文中的命令与代码均可直接复现。

两种编写 Go 扩展的路线

FrankenPHP 提供两条互补的路径:

  1. 扩展生成器(推荐):大多数场景下,用frankenphp extension-init命令自动生成所需的样板代码(C 文件、头文件、arginfo、PHP stub 等),开发者只需专注编写 Go 逻辑;
  2. 手动实现:适合需要精细控制扩展内部结构、或希望深入理解扩展工作机制的高级用户,代价是需要手写更多桥接代码。

两条路线最终殊途同归:扩展都会被编译进 FrankenPHP 二进制,并在运行时通过frankenphp.RegisterExtension()(见 ext.go)注册到 Zend 引擎。下文先讲上手更快的生成器路线,再展开手动实现。

方法一:使用扩展生成器

FrankenPHP 内置的生成器允许你只用 Go编写 PHP 扩展:既不需要写 C 代码,也不需要直接操作 CGO。配合 FrankenPHP 公开的类型 API,你完全不用操心 PHP/C 与 Go 之间的类型转换细节。

[!TIP] 若想理解扩展从零到一的工作原理,可跳过本节直接阅读后文的「手动实现」部分。

需要注意:该工具定位是「简单扩展的得力助手」,并非完整的扩展生成器——它不支持高级 PHP 扩展特性。当需要编写更复杂、更追求极致性能的扩展时,可能仍需回归 C 或直接使用 CGO。

前提条件:Go 模块与 PHP 源码

两条路线共用同一组前置条件:

  1. 创建 Go 模块
go mod init example.com/example
  1. 获取 PHP 源码:从 PHP 官方下载页获取 PHP 源码包,解压到任意目录(不必放在 Go 模块目录内),后续生成器与gen_stub.php脚本都要用到它:
tar xf php-*

编写你的第一个扩展函数

新建文件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(或按文档类型表返回对应 Go 原生类型),FrankenPHP 的类型 API 负责 C 与 Go 之间的转换。

[!TIP] 生成器解析源码时支持多种export_php:指令(函数、类、方法、常量、类常量、命名空间),仓库 internal/extgen/nodes.go 中定义了对应的phpFunctionphpClassphpConstant等内部数据结构。

类型转换:PHP/C 与 Go 的映射表

部分类型在 C/PHP 与 Go 之间的内存表示一致、可直接使用,其余则需要借助辅助函数。下表是官方文档给出的核心映射(Zend 引擎内部存储机制决定了这些差异):

PHP 类型Go 类型直接转换C→Go 辅助函数Go→C 辅助函数类方法支持
intint64--
?int*int64--
floatfloat64--
?float*float64--
boolbool--
?bool*bool--
string/?string*C.zend_stringfrankenphp.GoString()frankenphp.PHPString()
arrayfrankenphp.AssociativeArrayfrankenphp.GoAssociativeArray()frankenphp.PHPAssociativeArray()
arraymap[string]anyfrankenphp.GoMap()frankenphp.PHPMap()
array[]anyfrankenphp.GoPackedArray()frankenphp.PHPPackedArray()
mixedanyGoValue()PHPValue()
callable*C.zval-frankenphp.CallPHPCallable()
objectstruct未实现未实现

[!NOTE] 此表仍在完善中,会随 FrankenPHP 类型 API 的完整化而补齐。类方法目前仅支持标量类型与数组;对象尚不能作为方法参数或返回值类型。

回看上一节的repeat_this():首个参数*C.zend_string与返回值都用到了转换辅助函数,而第二、三个参数(int64bool)因底层内存表示与 Go 一致而无需转换。这些辅助函数定义在 types.go:例如GoString()通过C.GoStringNzend_string复制为 Go 字符串(types.go),PHPString()则调用zend_string_init分配新字符串,其第二个布尔参数控制分配持久内存(persistent,请求结束由 ZMM 自动释放)还是持久内存(需自行释放)(types.go)。

数组操作:关联数组、映射与切片

FrankenPHP 通过frankenphp.AssociativeArray结构体或直接转换为 map / slice 提供 PHP 数组的原生支持。AssociativeArray表示一个哈希映射:由Map: map[string]any与可选的Order: []string字段构成(Go 的 map 本身无序,因此用Order保留 PHP 关联数组的键序)。

无需顺序或关联语义时,也可直接转换为[]any切片或无序的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 { // 将 PHP 关联数组转为 Go,并保留顺序 associativeArray, err := frankenphp.GoAssociativeArrayany) if err != nil { // 错误处理 } // 按顺序遍历条目 for _, key := range associativeArray.Order { value, _ := associativeArray.Map[key] // 对键值进行处理 } // 返回有序数组 // 当 'Order' 非空时,仅考虑 'Order' 中的键值对 return frankenphp.PHPAssociativeArraystring } // export_php:function process_data_unordered(array $input): array func process_data_unordered_map(arr *C.zend_array) unsafe.Pointer { // 将 PHP 关联数组转为 Go map,不保留顺序 // 忽略顺序可提升性能 goMap, err := frankenphp.GoMapany) if err != nil { // 错误处理 } // 无序遍历条目 for key, value := range goMap { // 对键值进行处理 } // 返回无序数组 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 { // 将 PHP packed 数组转为 Go 切片 goSlice, err := frankenphp.GoPackedArray(unsafe.Pointer(arr)) if err != nil { // 错误处理 } // 按序遍历切片 for index, value := range goSlice { // 对索引与值进行处理 } // 返回 packed 数组 return frankenphp.PHPPackedArray([]string{"value1", "value2", "value3"}) }

数组转换的核心特性:

  • 有序键值对—— 关联数组可选择保留顺序;
  • 按场景优化—— 可丢弃顺序以提升性能,也可直接转为切片;
  • 自动列表检测—— 转回 PHP 时自动判断数组应为 packed 列表还是哈希映射;
  • 嵌套数组—— 数组可嵌套,int64float64stringboolnilAssociativeArraymap[string]any[]any等类型均自动转换;
  • 不支持对象—— 目前仅支持标量与数组作为值;传入对象在 PHP 数组中会变成null

packed 与 associative 的可用方法一览:

  • frankenphp.PHPAssociativeArray(arr frankenphp.AssociativeArray) unsafe.Pointer—— 转为带键值对的有序 PHP 数组
  • frankenphp.PHPMap(arr map[string]any) unsafe.Pointer—— 将 map 转为无序键值对 PHP 数组
  • frankenphp.PHPPackedArray(slice []any) unsafe.Pointer—— 将切片转为仅含索引值的 PHP packed 数组
  • frankenphp.GoAssociativeArray(arr unsafe.Pointer, ordered bool) frankenphp.AssociativeArray—— 将 PHP 数组转为有序 GoAssociativeArray
  • frankenphp.GoMap(arr unsafe.Pointer) map[string]any—— 将 PHP 数组转为无序 Go map
  • frankenphp.GoPackedArray(arr unsafe.Pointer) []any—— 将 PHP 数组转为 Go 切片
  • frankenphp.IsPacked(zval *C.zend_array) bool—— 判断 PHP 数组是 packed(仅索引)还是 associative(键值对)

从源码看,这些转换最终都收敛到goArray()函数:它遍历zend_array的 bucket(哈希槽),通过zval_get_type过滤IS_UNDEF项,并将键名经由GoString()转成字符串键(types.go);AssociativeArray[T]是泛型结构体(types.go),因此你可以指定具体的元素类型而非any

Callables:从 Go 调用 PHP 回调

frankenphp.CallPHPCallable允许 Go 代码调用 PHP 函数或方法。下面实现一个自定义array_map():接收 callable 与数组,对每个元素应用回调并返回新数组:

// 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.GoPackedArrayany) 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()接收指向 callable 的指针与参数数组,返回回调执行结果(实现在 types.go)。PHP 侧可沿用熟悉的 callable 语法:

<?php $result = my_array_map([1, 2, 3], function($x) { return $x * 2; }); // $result will be [2, 4, 6] $result = my_array_map(['hello', 'world'], 'strtoupper'); // $result will be ['HELLO', 'WORLD']

仓库的集成测试 internal/extgen/integration_test.go 对 callable 场景做了完整验证,覆盖闭包、函数名字符串、null回调(透传)以及类方法内回调等情形。

声明原生 PHP 类:不透明类(Opaque Classes)

生成器支持将 Go 结构体声明为不透明类——PHP 侧可见但内部属性完全隐藏的类,通过//export_php:class指令声明:

package example //export_php:class User type UserStruct struct { Name string Age int }

不透明类的特性:

  • 属性不可直接访问—— PHP 无法直接读写属性($user->name不可用);
  • 仅通过方法交互—— 所有操作必须经由 Go 定义的方法;
  • 更好的封装—— 内部数据结构完全由 Go 代码掌控;
  • 类型安全—— PHP 侧无法用错误类型破坏内部状态;
  • 更干净的 API—— 强制你设计合理的公开接口。

这种设计防止 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 }

注意方法指令语法User::methodName(PHP 签名)getAge(): int对应返回int64的 Go 方法,setNamePrefix(string $prefix = "User")展示了带默认值的参数(PHP 签名与 Go 参数声明需一一对应)。

Nullable 参数

生成器支持 PHP 签名中以?前缀声明的 nullable 参数,在 Go 函数中以指针形态出现,可通过nil判断 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 需为 UserStruct 的字段,例如: Active bool // us.Active = *active } }

Nullable 参数要点:

  • 标量类型(?int?float?bool)在 Go 中对应指针(*int64*float64*bool);
  • nullable 字符串(?string)仍是*C.zend_string,但可能为nil
  • 解引用指针前务必检查nil
  • PHP 的null对应 Go 的nil——PHP 传入null时 Go 函数收到nil指针。

[!WARNING] 类方法当前限制:对象不能作为参数或返回类型数组作为参数与返回类型完全支持;支持的类型为stringintfloatboolarrayvoid(返回)。Nullable 参数对所有标量类型(?string?int?float?bool)完整支持

扩展生成后,PHP 侧即可使用类及其方法,但不能直接访问属性

<?php $user = new User(); // ✅ 方法调用正常 $user->setAge(25); echo $user->getName(); // 输出: (empty、默认值) echo $user->getAge(); // 输出: 25 $user->setNamePrefix("Employee"); // ✅ nullable 参数同样正常 $user->updateInfo("John", 30, true); // 全部指定 $user->updateInfo("Jane", null, false); // Age 为 null $user->updateInfo(null, 25, null); // Name 与 active 为 null // ❌ 直接访问属性会报错 // echo $user->name; // 错误: 无法访问 private 属性 // $user->age = 30; // 错误: 无法访问 private 属性

从生成模板 internal/extgen/templates/extension.go.tpl 可以看到对象生命周期的实现:生成代码用cgo.NewHandle持有 Go 对象句柄,通过create_<Struct>_object创建实例、<method>_wrapper包装方法调用、removeGoObject在销毁时释放句柄。集成测试 internal/extgen/integration_test.go 验证了多实例状态隔离、nullable 参数(传入null保持原值)等行为。

声明常量

生成器支持两种指令导出常量:全局常量//export_php:const与类常量//export_php:classconst,便于在 Go 与 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 常量的名称,因此建议使用大写命名。

类常量
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 的 stub 文件中。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 STR_NORMAL ) //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 { // 反转字符串 } if mode == STR_NORMAL { // 无操作,仅为展示常量用法 } 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) }

常量的解析由 internal/extgen/constparser.go 负责,phpConstant结构体中的IsIota标记与CValue()方法(处理八进制等格式的 C 兼容转换)参见 internal/extgen/nodes.go。集成测试 internal/extgen/integration_test.go 验证了字符串、整数、布尔、浮点、iota 常量与类常量在 PHP 侧的取值。

生成扩展

一切就绪后运行生成器:

GEN_STUB_SCRIPT=php-src/build/gen_stub.php frankenphp extension-init my_extension.go

[!NOTE] 别忘了将GEN_STUB_SCRIPT环境变量指向先前下载的 PHP 源码中的gen_stub.php文件——它与手动实现路线使用的是同一个脚本。

顺利的话,项目目录下会出现以下文件:

  • my_extension.go—— 原始源文件(保持不变)
  • my_extension_generated.go—— 生成的调用 Go 函数的 CGO 包装文件
  • my_extension.stub.php—— 供 IDE 自动补全的 PHP stub 文件
  • my_extension_arginfo.h—— PHP 参数信息
  • my_extension.h—— C 头文件
  • my_extension.c—— C 实现文件
  • README.md—— 文档

[!IMPORTANT]原始源文件(my_extension.go)不会被改动。生成器把调用原函数的 CGO 包装器放到单独的_generated.go文件中,因此生成代码不会污染源码,可以放心进行版本管理。

extension-init命令注册于 caddy/extinit.go:它解析 Go 源文件路径、调用extgen.Generator.Generate()(internal/extgen/generator.go),后者依次执行 stub 生成、arginfo 生成、头文件、C 文件、Go 文件与文档生成六个步骤。若源文件中没有找到任何函数、类或常量,生成会直接报错(见 internal/extgen/generator.go,测试用例在 internal/extgen/integration_test.go)。

将生成的扩展集成到 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子目录——这不是硬性要求,你也可以把生成的文件复制到模块目录后直接引用该目录。仓库集成测试 internal/extgen/integration_test.go 展示了同样的构建流程:设置CGO_ENABLED=1CGO_CFLAGS(来自php-config --includes)与CGO_LDFLAGS--ldflags--libs),再通过 xcaddy 一并引入 FrankenPHP 本体与测试扩展模块。

测试生成的扩展

创建一个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 php-server即可验证扩展是否生效。仓库的集成测试 internal/extgen/integration_test.go 提供了可参考的验证范式:先用function_exists/class_exists/defined断言符号存在,再用实际 PHP 代码断言函数行为与预期输出一致。

使用命名空间

//export_php:namespace指令可将扩展的函数、类、常量统一归入某个命名空间,避免命名冲突并让 API 更有条理。

声明命名空间

在 Go 文件顶部声明,作用于该文件所有导出符号:

//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
重要注意事项
  • 每个文件只允许一个命名空间指令,多个会触发生成器错误;
  • 命名空间作用于文件内所有导出符号(函数、类、方法、常量);
  • 命名空间名遵循 PHP 规则,使用反斜杠(\)作分隔符;
  • 未声明时,符号照常导出到全局命名空间。

命名空间的解析在 internal/extgen/nsparser.go 中实现,Generator结构体通过Namespace字段传递该值(internal/extgen/generator.go)。集成测试 internal/extgen/integration_test.go 验证了带命名空间的函数、类、类常量在 PHP 侧(含use导入)均可正确访问。

方法二:手动实现

若想彻底理解扩展机制或追求完全控制,也可以手写扩展。该路线功能完整可控,但需要更多样板代码。

基础函数:从 Go 触发 goroutine

下面手动实现一个极简扩展:新函数go_print()无参数、无返回值,调用后由 goroutine 向 Caddy 日志输出消息——直观展示如何在 PHP 中借助 Go 的并发模型。

定义 Go 函数

创建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 注册逻辑,简化扩展注册流程(其实现见 ext.go,最终通过registerExtensions()一次性把扩展注册进 Zend 引擎)。//export指令借助 CGO 让即将编写的 C 代码能调用go_print_something

定义 PHP 函数

要让 PHP 能调用 Go 函数,需先用 stub 文件声明对应 PHP 函数,创建extension.stub.php

<?php /** @generate-class-entries */ function go_print(): void {}

@generate-class-entries指令让 PHP 为扩展自动生成函数条目。随后用 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,完成三件事:引入 PHP 头文件、声明新的原生 PHP 函数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 };

本例函数无参数无返回,仅调用之前通过//export导出的 Go 函数。zend_module_entry结构体声明扩展的名称、版本与属性等元数据,是 PHP 识别并加载扩展的依据;ext_functions是已定义 PHP 函数的指针数组,由gen_stub.php生成的extension_arginfo.h提供。扩展注册则由 Go 代码中调用的RegisterExtension()自动处理。

进阶用法:字符串参数与返回值

基础版之外,再看一个接收字符串、返回大写结果的函数,它展示了参数解析与字符串类型转换的完整链路。

定义 PHP 函数 stub

修改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] 别忽视函数文档!当把扩展 stub 分享给其他开发者时,文档是传达功能与用法的关键手段。

重新运行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/C 之间的类型转换

Go 函数无法直接接收 PHP 字符串,需要转换——幸运的是 FrankenPHP 提供了与生成器路线相同的转换辅助函数。头文件保持不变:

#ifndef _EXTENSION_H #define _EXTENSION_H #include <php.h> extern zend_module_entry ext_module_entry; #endif

extension.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); }

ZEND_PARSE_PARAMETERS_START等参数解析宏的细节可参考 PHP Internals Book 中关于参数解析的章节。本例向 PHP 声明该函数接收一个必填string参数(以zend_string表示),将其直接传给 Go 函数,并用RETVAL_STR返回结果。

实现 Go 函数

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)) }

这种方式比手动内存管理更干净、更安全:GoString()PHPString()自动完成zend_string与 Go 字符串之间的双向转换。PHPString()false参数表示创建非持久字符串(请求结束时自动释放)。

[!TIP] 本例省略了错误处理,但生产代码中应始终检查指针是否为nil、数据是否有效。

手动扩展的集成与测试

手动扩展的编译集成与生成器路线一致,详见 编译文档,同样通过--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

集成完成后,创建index.php测试实现的功能:

<?php // 基础函数测试 go_print(); // 进阶函数测试 echo go_upper("hello world") . "\n";

运行./frankenphp php-server,即可验证扩展是否正确工作。

总结

两条路线共享同一套底层支撑:FrankenPHP 的类型转换 API(types.go)与扩展注册机制(ext.go)是 Go 与 Zend 引擎之间的桥梁;生成器路线将 C 样板(头文件、C 文件、arginfo、stub)自动化,手动路线则把控制权完整交还开发者。无论选择哪种方式,最终产物都是通过 xcaddy 编译进 FrankenPHP 二进制的原生扩展,为 PHP 应用带来 Go 生态的库与并发能力。

【免费下载链接】frankenphp🧟 The modern PHP app server项目地址: https://gitcode.com/GitHub_Trending/fr/frankenphp

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 20:32:27

OpenCV形状检测实战:从轮廓提取到工业级应用

1. 项目概述&#xff1a;这不是“画个圈圈诅咒你”&#xff0c;而是让计算机真正“看见”物体轮廓的底层能力“OpenCV形状检测”这六个字&#xff0c;乍一听像教科书里的一个课后习题&#xff0c;但在我带过的二十多个工业视觉项目里&#xff0c;它几乎就是产线质检、机器人抓取…

作者头像 李华
网站建设 2026/9/15 20:32:25

深入昇腾ops-nn算子仓库:从Tiling到融合优化

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/15 20:31:04

Mysql for Linux安装配置之—— 源码安装

1. 安装--假设已经有mysql-5.5.10.tar.gz及cmake-2.8.4.tar.gz两个源码压缩文件 1&#xff09;先安装cmake--注&#xff1a;1&#xff09;mysql5.5以后通过cmake编译。# tar -zxv -f cmake-2.8.4.tar.gz# cd cmake-2.8.4# ./configure# make# make install2&#xff09;创建mys…

作者头像 李华
网站建设 2026/9/15 20:29:08

SAC算法实战:BipedalWalker Hardcore调参全攻略

都说强化学习入门容易精通难&#xff0c;真正劝退大家的往往不是算法推导&#xff0c;而是调参。尤其是连续控制经典环境BipedalWalker&#xff0c;从普通版到Hardcore版&#xff0c;每一步都在跟超参数较劲。我前后在BipedalWalker和BipedalWalkerHardcore上用SAC&#xff08;…

作者头像 李华
网站建设 2026/9/15 20:28:24

青岛菲斯曼壁挂炉检修电话|地暖暖气片不热检查|欧米到家服务电话

文章简介青岛壁挂炉冬季频繁出现不点火、热水忽冷忽热、地暖制热不足、运行反复掉压、管路漏水等常见故障&#xff0c;受本地气候、水质及采暖系统使用习惯影响&#xff0c;故障成因更具地域性&#xff0c;需结合设备型号、采暖管路系统、运行工况全方位检测排查。欧米到家专注…

作者头像 李华