Skip to content

Commit 5514491

Browse files
authored
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 8a0b9c1. * fix docs * fix docs * cleanup template * fix template * fix tests
1 parent c42d287 commit 5514491

30 files changed

Lines changed: 303 additions & 253 deletions

caddy/extinit.go

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@ package caddy
22

33
import (
44
"errors"
5-
"github.com/dunglas/frankenphp/internal/extgen"
65
"log"
76
"os"
87
"path/filepath"
98
"strings"
109

10+
"github.com/dunglas/frankenphp/internal/extgen"
11+
1112
caddycmd "github.com/caddyserver/caddy/v2/cmd"
1213
"github.com/spf13/cobra"
1314
)
@@ -27,27 +28,21 @@ Initializes a PHP extension from a Go file. This command generates the necessary
2728
})
2829
}
2930

30-
func cmdInitExtension(fs caddycmd.Flags) (int, error) {
31+
func cmdInitExtension(_ caddycmd.Flags) (int, error) {
3132
if len(os.Args) < 3 {
3233
return 1, errors.New("the path to the Go source is required")
3334
}
3435

3536
sourceFile := os.Args[2]
37+
baseName := extgen.SanitizePackageName(strings.TrimSuffix(filepath.Base(sourceFile), ".go"))
3638

37-
baseName := strings.TrimSuffix(filepath.Base(sourceFile), ".go")
38-
39-
baseName = extgen.SanitizePackageName(baseName)
40-
41-
sourceDir := filepath.Dir(sourceFile)
42-
buildDir := filepath.Join(sourceDir, "build")
43-
44-
generator := extgen.Generator{BaseName: baseName, SourceFile: sourceFile, BuildDir: buildDir}
39+
generator := extgen.Generator{BaseName: baseName, SourceFile: sourceFile, BuildDir: filepath.Dir(sourceFile)}
4540

4641
if err := generator.Generate(); err != nil {
4742
return 1, err
4843
}
4944

50-
log.Printf("PHP extension %q initialized successfully in %q", baseName, generator.BuildDir)
45+
log.Printf("PHP extension %q initialized successfully in directory %q", baseName, generator.BuildDir)
5146

5247
return 0, nil
5348
}

docs/cn/extensions.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -146,11 +146,11 @@ func process_data(arr *C.zval) unsafe.Pointer {
146146

147147
**可用方法:**
148148

149-
- `SetInt(key int64, value interface{})` - 使用整数键设置值
150-
- `SetString(key string, value interface{})` - 使用字符串键设置值
151-
- `Append(value interface{})` - 使用下一个可用整数键添加值
149+
- `SetInt(key int64, value any)` - 使用整数键设置值
150+
- `SetString(key string, value any)` - 使用字符串键设置值
151+
- `Append(value any)` - 使用下一个可用整数键添加值
152152
- `Len() uint32` - 获取元素数量
153-
- `At(index uint32) (PHPKey, interface{})` - 获取索引处的键值对
153+
- `At(index uint32) (PHPKey, any)` - 获取索引处的键值对
154154
- `frankenphp.PHPArray(arr *frankenphp.Array) unsafe.Pointer` - 转换为 PHP 数组
155155

156156
### 声明原生 PHP 类

docs/extensions.md

Lines changed: 102 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ As covered in the manual implementation section below as well, you need to [get
3333
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:
3434

3535
```console
36-
go mod init github.com/my-account/my-module
36+
go mod init example.com/example
3737
```
3838

3939
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-*
4747
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:
4848

4949
```go
50+
package example
51+
52+
// #include <Zend/zend_types.h>
53+
import "C"
5054
import (
51-
"C"
52-
"github.com/dunglas/frankenphp"
5355
"strings"
56+
57+
"github.com/dunglas/frankenphp"
5458
)
5559

5660
//export_php:function repeat_this(string $str, int $count, bool $reverse): string
@@ -98,6 +102,7 @@ This table summarizes what you need to know:
98102
| `object` | `struct` || _Not yet implemented_ | _Not yet implemented_ ||
99103

100104
> [!NOTE]
105+
>
101106
> This table is not exhaustive yet and will be completed as the FrankenPHP types API gets more complete.
102107
>
103108
> 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
115120
**Creating and manipulating arrays in Go:**
116121

117122
```go
123+
package example
124+
125+
// #include <Zend/zend_types.h>
126+
import "C"
127+
import (
128+
"unsafe"
129+
130+
"github.com/dunglas/frankenphp"
131+
)
132+
118133
// export_php:function process_data_ordered(array $input): array
119134
func process_data_ordered_map(arr *C.zval) unsafe.Pointer {
120135
// Convert PHP associative array to Go while keeping the order
@@ -128,7 +143,7 @@ func process_data_ordered_map(arr *C.zval) unsafe.Pointer {
128143

129144
// return an ordered array
130145
// if 'Order' is not empty, only the key-value pairs in 'Order' will be respected
131-
return frankenphp.PHPAssociativeArray(AssociativeArray{
146+
return frankenphp.PHPAssociativeArray(frankenphp.AssociativeArray{
132147
Map: map[string]any{
133148
"key1": "value1",
134149
"key2": "value2",
@@ -192,6 +207,8 @@ func process_data_packed(arr *C.zval) unsafe.Pointer {
192207
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:
193208

194209
```go
210+
package example
211+
195212
//export_php:class User
196213
type UserStruct struct {
197214
Name string
@@ -216,6 +233,16 @@ This approach provides better encapsulation and prevents PHP code from accidenta
216233
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:
217234

218235
```go
236+
package example
237+
238+
// #include <Zend/zend_types.h>
239+
import "C"
240+
import (
241+
"unsafe"
242+
243+
"github.com/dunglas/frankenphp"
244+
)
245+
219246
//export_php:class User
220247
type UserStruct struct {
221248
Name string
@@ -248,6 +275,16 @@ func (us *UserStruct) SetNamePrefix(prefix *C.zend_string) {
248275
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:
249276

250277
```go
278+
package example
279+
280+
// #include <Zend/zend_types.h>
281+
import "C"
282+
import (
283+
"unsafe"
284+
285+
"github.com/dunglas/frankenphp"
286+
)
287+
251288
//export_php:method User::updateInfo(?string $name, ?int $age, ?bool $active): void
252289
func (us *UserStruct) UpdateInfo(name *C.zend_string, age *int64, active *bool) {
253290
// Check if name was provided (not null)
@@ -275,6 +312,7 @@ func (us *UserStruct) UpdateInfo(name *C.zend_string, age *int64, active *bool)
275312
- **PHP `null` becomes Go `nil`** - when PHP passes `null`, your Go function receives a `nil` pointer
276313

277314
> [!WARNING]
315+
>
278316
> 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`).
279317
280318
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
311349
Use the `//export_php:const` directive to create global PHP constants:
312350

313351
```go
352+
package example
353+
314354
//export_php:const
315355
const MAX_CONNECTIONS = 100
316356

@@ -329,6 +369,8 @@ const STATUS_ERROR = iota
329369
Use the `//export_php:classconstant ClassName` directive to create constants that belong to a specific PHP class:
330370

331371
```go
372+
package example
373+
332374
//export_php:classconstant User
333375
const STATUS_ACTIVE = 1
334376

@@ -368,10 +410,15 @@ The directive supports various value types including strings, integers, booleans
368410
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:
369411

370412
```go
413+
package example
414+
415+
// #include <Zend/zend_types.h>
416+
import "C"
371417
import (
372-
"C"
373-
"github.com/dunglas/frankenphp"
374-
"strings"
418+
"strings"
419+
"unsafe"
420+
421+
"github.com/dunglas/frankenphp"
375422
)
376423

377424
//export_php:const
@@ -388,37 +435,37 @@ const MODE_UPPERCASE = 2
388435

389436
//export_php:function repeat_this(string $str, int $count, int $mode): string
390437
func repeat_this(s *C.zend_string, count int64, mode int) unsafe.Pointer {
391-
str := frankenphp.GoString(unsafe.Pointer(s))
438+
str := frankenphp.GoString(unsafe.Pointer(s))
392439

393-
result := strings.Repeat(str, int(count))
394-
if mode == STR_REVERSE {
395-
// reverse the string
396-
}
440+
result := strings.Repeat(str, int(count))
441+
if mode == STR_REVERSE {
442+
// reverse the string
443+
}
397444

398-
if mode == STR_NORMAL {
399-
// no-op, just to showcase the constant
400-
}
445+
if mode == STR_NORMAL {
446+
// no-op, just to showcase the constant
447+
}
401448

402-
return frankenphp.PHPString(result, false)
449+
return frankenphp.PHPString(result, false)
403450
}
404451

405452
//export_php:class StringProcessor
406453
type StringProcessorStruct struct {
407-
// internal fields
454+
// internal fields
408455
}
409456

410457
//export_php:method StringProcessor::process(string $input, int $mode): string
411458
func (sp *StringProcessorStruct) Process(input *C.zend_string, mode int64) unsafe.Pointer {
412-
str := frankenphp.GoString(unsafe.Pointer(input))
459+
str := frankenphp.GoString(unsafe.Pointer(input))
413460

414-
switch mode {
415-
case MODE_LOWERCASE:
416-
str = strings.ToLower(str)
417-
case MODE_UPPERCASE:
418-
str = strings.ToUpper(str)
419-
}
461+
switch mode {
462+
case MODE_LOWERCASE:
463+
str = strings.ToLower(str)
464+
case MODE_UPPERCASE:
465+
str = strings.ToUpper(str)
466+
}
420467

421-
return frankenphp.PHPString(str, false)
468+
return frankenphp.PHPString(str, false)
422469
}
423470
```
424471

@@ -432,9 +479,13 @@ Use the `//export_php:namespace` directive at the top of your Go file to place a
432479

433480
```go
434481
//export_php:namespace My\Extension
435-
package main
482+
package example
436483

437-
import "C"
484+
import (
485+
"unsafe"
486+
487+
"github.com/dunglas/frankenphp"
488+
)
438489

439490
//export_php:function hello(): string
440491
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
537588
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:
538589

539590
```go
540-
package ext_go
591+
package example
541592

542-
//#include "extension.h"
593+
// #include "extension.h"
543594
import "C"
544595
import (
545-
"unsafe"
546-
"github.com/caddyserver/caddy/v2"
547-
"github.com/dunglas/frankenphp"
596+
"log/slog"
597+
"unsafe"
598+
599+
"github.com/dunglas/frankenphp"
548600
)
549601

550602
func init() {
551-
frankenphp.RegisterExtension(unsafe.Pointer(&C.ext_module_entry))
603+
frankenphp.RegisterExtension(unsafe.Pointer(&C.ext_module_entry))
552604
}
553605

554606
//export go_print_something
555607
func go_print_something() {
556-
go func() {
557-
caddy.Log().Info("Hello from a goroutine!")
558-
}()
608+
go func() {
609+
slog.Info("Hello from a goroutine!")
610+
}()
559611
}
560612
```
561613

@@ -731,7 +783,16 @@ There's only one thing left to do: implement the `go_upper` function in Go.
731783
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.
732784
733785
```go
734-
import "strings"
786+
package example
787+
788+
// #include <Zend/zend_types.h>
789+
import "C"
790+
import (
791+
"unsafe"
792+
"strings"
793+
794+
"github.com/dunglas/frankenphp"
795+
)
735796
736797
//export go_upper
737798
func go_upper(s *C.zend_string) *C.zend_string {
@@ -743,9 +804,12 @@ func go_upper(s *C.zend_string) *C.zend_string {
743804
}
744805
```
745806

746-
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).
807+
This approach is much cleaner and safer than manual memory management.
808+
FrankenPHP's helper functions handle the conversion between PHP's `zend_string` format and Go strings automatically.
809+
The `false` parameter in `PHPString()` indicates that we want to create a new non-persistent string (freed at the end of the request).
747810

748811
> [!TIP]
812+
>
749813
> 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.
750814
751815
### Integrating the Extension into FrankenPHP

docs/fr/extensions.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -146,11 +146,11 @@ func process_data(arr *C.zval) unsafe.Pointer {
146146

147147
**Méthodes disponibles :**
148148

149-
- `SetInt(key int64, value interface{})` - Définir une valeur avec une clé entière
150-
- `SetString(key string, value interface{})` - Définir une valeur avec une clé chaîne
151-
- `Append(value interface{})` - Ajouter une valeur avec la prochaine clé entière disponible
149+
- `SetInt(key int64, value any)` - Définir une valeur avec une clé entière
150+
- `SetString(key string, value any)` - Définir une valeur avec une clé chaîne
151+
- `Append(value any)` - Ajouter une valeur avec la prochaine clé entière disponible
152152
- `Len() uint32` - Obtenir le nombre d'éléments
153-
- `At(index uint32) (PHPKey, interface{})` - Obtenir la paire clé-valeur à l'index
153+
- `At(index uint32) (PHPKey, any)` - Obtenir la paire clé-valeur à l'index
154154
- `frankenphp.PHPArray(arr *frankenphp.Array) unsafe.Pointer` - Convertir vers un tableau PHP
155155

156156
### Déclarer une Classe PHP Native

internal/extgen/cfile_namespace_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
package extgen
22

33
import (
4-
"github.com/stretchr/testify/assert"
5-
"github.com/stretchr/testify/require"
64
"os"
75
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
89
)
910

1011
func TestNamespacedClassName(t *testing.T) {

internal/extgen/cfile_phpmethod_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
package extgen
22

33
import (
4-
"github.com/stretchr/testify/require"
54
"testing"
5+
6+
"github.com/stretchr/testify/require"
67
)
78

89
func TestCFile_NamespacedPHPMethods(t *testing.T) {

0 commit comments

Comments
 (0)