feat(extgen): support for mixed type (#1913)

* feat(extgent): support for mixed type

* refactor: use unsafe.Pointer

* Revert "refactor: use unsafe.Pointer"

This reverts commit 8a0b9c1beb.

* fix docs

* fix docs

* cleanup template

* fix template

* fix tests
This commit is contained in:
Kévin Dunglas
2025-10-09 14:10:45 +02:00
committed by GitHub
parent c42d287138
commit 5514491a18
30 changed files with 303 additions and 253 deletions

View File

@@ -146,11 +146,11 @@ func process_data(arr *C.zval) unsafe.Pointer {
**可用方法:**
- `SetInt(key int64, value interface{})` - 使用整数键设置值
- `SetString(key string, value interface{})` - 使用字符串键设置值
- `Append(value interface{})` - 使用下一个可用整数键添加值
- `SetInt(key int64, value any)` - 使用整数键设置值
- `SetString(key string, value any)` - 使用字符串键设置值
- `Append(value any)` - 使用下一个可用整数键添加值
- `Len() uint32` - 获取元素数量
- `At(index uint32) (PHPKey, interface{})` - 获取索引处的键值对
- `At(index uint32) (PHPKey, any)` - 获取索引处的键值对
- `frankenphp.PHPArray(arr *frankenphp.Array) unsafe.Pointer` - 转换为 PHP 数组
### 声明原生 PHP 类

View File

@@ -33,7 +33,7 @@ As covered in the manual implementation section below as well, you need to [get
The first step to writing a PHP extension in Go is to create a new Go module. You can use the following command for this:
```console
go mod init github.com/my-account/my-module
go mod init example.com/example
```
The second step is to [get the PHP sources](https://www.php.net/downloads.php) for the next steps. Once you have them, decompress them into the directory of your choice, not inside your Go module:
@@ -47,10 +47,14 @@ tar xf php-*
Everything is now setup to write your native function in Go. Create a new file named `stringext.go`. Our first function will take a string as an argument, the number of times to repeat it, a boolean to indicate whether to reverse the string, and return the resulting string. This should look like this:
```go
package example
// #include <Zend/zend_types.h>
import "C"
import (
"C"
"github.com/dunglas/frankenphp"
"strings"
"github.com/dunglas/frankenphp"
)
//export_php:function repeat_this(string $str, int $count, bool $reverse): string
@@ -98,6 +102,7 @@ This table summarizes what you need to know:
| `object` | `struct` | ❌ | _Not yet implemented_ | _Not yet implemented_ | ❌ |
> [!NOTE]
>
> This table is not exhaustive yet and will be completed as the FrankenPHP types API gets more complete.
>
> For class methods specifically, primitive types and arrays are currently supported. Objects cannot be used as method parameters or return types yet.
@@ -115,6 +120,16 @@ If order or association are not needed, it's also possible to directly convert t
**Creating and manipulating arrays in Go:**
```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.zval) unsafe.Pointer {
// Convert PHP associative array to Go while keeping the order
@@ -128,7 +143,7 @@ func process_data_ordered_map(arr *C.zval) unsafe.Pointer {
// return an ordered array
// if 'Order' is not empty, only the key-value pairs in 'Order' will be respected
return frankenphp.PHPAssociativeArray(AssociativeArray{
return frankenphp.PHPAssociativeArray(frankenphp.AssociativeArray{
Map: map[string]any{
"key1": "value1",
"key2": "value2",
@@ -192,6 +207,8 @@ func process_data_packed(arr *C.zval) unsafe.Pointer {
The generator supports declaring **opaque classes** as Go structs, which can be used to create PHP objects. You can use the `//export_php:class` directive comment to define a PHP class. For example:
```go
package example
//export_php:class User
type UserStruct struct {
Name string
@@ -216,6 +233,16 @@ This approach provides better encapsulation and prevents PHP code from accidenta
Since properties are not directly accessible, you **must define methods** to interact with your opaque classes. Use the `//export_php:method` directive to define behavior:
```go
package example
// #include <Zend/zend_types.h>
import "C"
import (
"unsafe"
"github.com/dunglas/frankenphp"
)
//export_php:class User
type UserStruct struct {
Name string
@@ -248,6 +275,16 @@ func (us *UserStruct) SetNamePrefix(prefix *C.zend_string) {
The generator supports nullable parameters using the `?` prefix in PHP signatures. When a parameter is nullable, it becomes a pointer in your Go function, allowing you to check if the value was `null` in PHP:
```go
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) {
// Check if name was provided (not null)
@@ -275,6 +312,7 @@ func (us *UserStruct) UpdateInfo(name *C.zend_string, age *int64, active *bool)
- **PHP `null` becomes Go `nil`** - when PHP passes `null`, your Go function receives a `nil` pointer
> [!WARNING]
>
> Currently, class methods have the following limitations. **Objects are not supported** as parameter types or return types. **Arrays are fully supported** for both parameters and return types. Supported types: `string`, `int`, `float`, `bool`, `array`, and `void` (for return type). **Nullable parameter types are fully supported** for all scalar types (`?string`, `?int`, `?float`, `?bool`).
After generating the extension, you will be allowed to use the class and its methods in PHP. Note that you **cannot access properties directly**:
@@ -311,6 +349,8 @@ The generator supports exporting Go constants to PHP using two directives: `//ex
Use the `//export_php:const` directive to create global PHP constants:
```go
package example
//export_php:const
const MAX_CONNECTIONS = 100
@@ -329,6 +369,8 @@ const STATUS_ERROR = iota
Use the `//export_php:classconstant ClassName` directive to create constants that belong to a specific PHP class:
```go
package example
//export_php:classconstant User
const STATUS_ACTIVE = 1
@@ -368,10 +410,15 @@ The directive supports various value types including strings, integers, booleans
You can use constants just like you are used to in the Go code. For example, let's take the `repeat_this()` function we declared earlier and change the last argument to an integer:
```go
package example
// #include <Zend/zend_types.h>
import "C"
import (
"C"
"github.com/dunglas/frankenphp"
"strings"
"strings"
"unsafe"
"github.com/dunglas/frankenphp"
)
//export_php:const
@@ -388,37 +435,37 @@ 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))
str := frankenphp.GoString(unsafe.Pointer(s))
result := strings.Repeat(str, int(count))
if mode == STR_REVERSE {
// reverse the string
}
result := strings.Repeat(str, int(count))
if mode == STR_REVERSE {
// reverse the string
}
if mode == STR_NORMAL {
// no-op, just to showcase the constant
}
if mode == STR_NORMAL {
// no-op, just to showcase the constant
}
return frankenphp.PHPString(result, false)
return frankenphp.PHPString(result, false)
}
//export_php:class StringProcessor
type StringProcessorStruct struct {
// internal fields
// 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))
str := frankenphp.GoString(unsafe.Pointer(input))
switch mode {
case MODE_LOWERCASE:
str = strings.ToLower(str)
case MODE_UPPERCASE:
str = strings.ToUpper(str)
}
switch mode {
case MODE_LOWERCASE:
str = strings.ToLower(str)
case MODE_UPPERCASE:
str = strings.ToUpper(str)
}
return frankenphp.PHPString(str, false)
return frankenphp.PHPString(str, false)
}
```
@@ -432,9 +479,13 @@ Use the `//export_php:namespace` directive at the top of your Go file to place a
```go
//export_php:namespace My\Extension
package main
package example
import "C"
import (
"unsafe"
"github.com/dunglas/frankenphp"
)
//export_php:function hello(): string
func hello() string {
@@ -537,25 +588,26 @@ We'll see how to write a simple PHP extension in Go that defines a new native fu
In your module, you need to define a new native function that will be called from PHP. To do this, create a file with the name you want, for example, `extension.go`, and add the following code:
```go
package ext_go
package example
//#include "extension.h"
// #include "extension.h"
import "C"
import (
"unsafe"
"github.com/caddyserver/caddy/v2"
"github.com/dunglas/frankenphp"
"log/slog"
"unsafe"
"github.com/dunglas/frankenphp"
)
func init() {
frankenphp.RegisterExtension(unsafe.Pointer(&C.ext_module_entry))
frankenphp.RegisterExtension(unsafe.Pointer(&C.ext_module_entry))
}
//export go_print_something
func go_print_something() {
go func() {
caddy.Log().Info("Hello from a goroutine!")
}()
go func() {
slog.Info("Hello from a goroutine!")
}()
}
```
@@ -731,7 +783,16 @@ There's only one thing left to do: implement the `go_upper` function in Go.
Our Go function will take a `*C.zend_string` as a parameter, convert it to a Go string using FrankenPHP's helper function, process it, and return the result as a new `*C.zend_string`. The helper functions handle all the memory management and conversion complexity for us.
```go
import "strings"
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 {
@@ -743,9 +804,12 @@ func go_upper(s *C.zend_string) *C.zend_string {
}
```
This approach is much cleaner and safer than manual memory management. FrankenPHP's helper functions handle the conversion between PHP's `zend_string` format and Go strings automatically. The `false` parameter in `PHPString()` indicates that we want to create a new non-persistent string (freed at the end of the request).
This approach is much cleaner and safer than manual memory management.
FrankenPHP's helper functions handle the conversion between PHP's `zend_string` format and Go strings automatically.
The `false` parameter in `PHPString()` indicates that we want to create a new non-persistent string (freed at the end of the request).
> [!TIP]
>
> In this example, we don't perform any error handling, but you should always check that pointers are not `nil` and that the data is valid before using it in your Go functions.
### Integrating the Extension into FrankenPHP

View File

@@ -146,11 +146,11 @@ func process_data(arr *C.zval) unsafe.Pointer {
**Méthodes disponibles :**
- `SetInt(key int64, value interface{})` - Définir une valeur avec une clé entière
- `SetString(key string, value interface{})` - Définir une valeur avec une clé chaîne
- `Append(value interface{})` - Ajouter une valeur avec la prochaine clé entière disponible
- `SetInt(key int64, value any)` - Définir une valeur avec une clé entière
- `SetString(key string, value any)` - Définir une valeur avec une clé chaîne
- `Append(value any)` - Ajouter une valeur avec la prochaine clé entière disponible
- `Len() uint32` - Obtenir le nombre d'éléments
- `At(index uint32) (PHPKey, interface{})` - Obtenir la paire clé-valeur à l'index
- `At(index uint32) (PHPKey, any)` - Obtenir la paire clé-valeur à l'index
- `frankenphp.PHPArray(arr *frankenphp.Array) unsafe.Pointer` - Convertir vers un tableau PHP
### Déclarer une Classe PHP Native