feat(extgen): add support for callable in parameters (#1731)

This commit is contained in:
Alexandre Daubois
2025-12-15 12:50:50 +01:00
committed by GitHub
parent 58a63703b4
commit bb1c3678dc
17 changed files with 743 additions and 77 deletions

View File

@@ -88,19 +88,20 @@ While some variable types have the same memory representation between C/PHP and
This table summarizes what you need to know:
| PHP type | Go type | Direct conversion | C to Go helper | Go to C helper | Class Methods Support |
| ------------------ | ----------------------------- | ----------------- | --------------------------------- | ---------------------------------- | --------------------- |
| `int` | `int64` | ✅ | - | - | ✅ |
| `?int` | `*int64` | ✅ | - | - | ✅ |
| `float` | `float64` | ✅ | - | - | ✅ |
| `?float` | `*float64` | ✅ | - | - | ✅ |
| `bool` | `bool` | ✅ | - | - | ✅ |
| `?bool` | `*bool` | ✅ | - | - | ✅ |
| `string`/`?string` | `*C.zend_string` | ❌ | `frankenphp.GoString()` | `frankenphp.PHPString()` | ✅ |
| `array` | `frankenphp.AssociativeArray` | ❌ | `frankenphp.GoAssociativeArray()` | `frankenphp.PHPAssociativeArray()` | ✅ |
| `array` | `map[string]any` | ❌ | `frankenphp.GoMap()` | `frankenphp.PHPMap()` | ✅ |
| `array` | `[]any` | ❌ | `frankenphp.GoPackedArray()` | `frankenphp.PHPPackedArray()` | ✅ |
| `mixed` | `any` | ❌ | `GoValue()` | `PHPValue()` | ❌ |
| `object` | `struct` | ❌ | _Not yet implemented_ | _Not yet implemented_ | ❌ |
|--------------------|-------------------------------|-------------------|-----------------------------------|------------------------------------|-----------------------|
| `int` | `int64` | ✅ | - | - | ✅ |
| `?int` | `*int64` | ✅ | - | - | ✅ |
| `float` | `float64` | ✅ | - | - | ✅ |
| `?float` | `*float64` | ✅ | - | - | ✅ |
| `bool` | `bool` | ✅ | - | - | ✅ |
| `?bool` | `*bool` | ✅ | - | - | ✅ |
| `string`/`?string` | `*C.zend_string` | ❌ | `frankenphp.GoString()` | `frankenphp.PHPString()` | ✅ |
| `array` | `frankenphp.AssociativeArray` | ❌ | `frankenphp.GoAssociativeArray()` | `frankenphp.PHPAssociativeArray()` | ✅ |
| `array` | `map[string]any` | ❌ | `frankenphp.GoMap()` | `frankenphp.PHPMap()` | ✅ |
| `array` | `[]any` | ❌ | `frankenphp.GoPackedArray()` | `frankenphp.PHPPackedArray()` | ✅ |
| `mixed` | `any` | ❌ | `GoValue()` | `PHPValue()` | ❌ |
| `callable` | `*C.zval` | ❌ | - | frankenphp.CallPHPCallable() | ❌ |
| `object` | `struct` | ❌ | _Not yet implemented_ | _Not yet implemented_ | ❌ |
> [!NOTE]
>
@@ -212,6 +213,42 @@ func process_data_packed(arr *C.zend_array) unsafe.Pointer {
- `frankenphp.GoMap(arr unsafe.Pointer) map[string]any` - Convert a PHP array to an unordered Go map
- `frankenphp.GoPackedArray(arr unsafe.Pointer) []any` - Convert a PHP array to a Go slice
### Working with Callables
FrankenPHP provides a way to work with PHP callables using the `frankenphp.CallPHPCallable` helper. This allows you to call PHP functions or methods from Go code.
To showcase this, let's create our own `array_map()` function that takes a callable and an array, applies the callable to each element of the array, and returns a new array with the results:
```go
// 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.GoPackedArray[any](unsafe.Pointer(arr))
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)
}
```
Notice how we use `frankenphp.CallPHPCallable()` to call the PHP callable passed as a parameter. This function takes a pointer to the callable and an array of arguments, and it returns the result of the callable execution. You can use the callable syntax you're used to:
```php
<?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']
```
### Declaring a Native PHP Class
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:

View File

@@ -210,6 +210,42 @@ func process_data_packed(arr *C.zval) unsafe.Pointer {
- `frankenphp.GoMap(arr unsafe.Pointer) map[string]any` - Convertir un tableau PHP vers une map Go non ordonnée
- `frankenphp.GoPackedArray(arr unsafe.Pointer) []any` - Convertir un tableau PHP vers un slice Go
### Travailler avec des Callables
FrankenPHP propose un moyen de travailler avec les _callables_ PHP grâce au helper `frankenphp.CallPHPCallable()`. Cela permet d'appeler des fonctions ou des méthodes PHP depuis du code Go.
Pour illustrer cela, créons notre propre fonction `array_map()` qui prend un _callable_ et un tableau, applique le _callable_ à chaque élément du tableau, et retourne un nouveau tableau avec les résultats :
```go
// 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.GoPackedArray[any](unsafe.Pointer(arr))
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)
}
```
Remarquez comment nous utilisons `frankenphp.CallPHPCallable()` pour appeler le _callable_ PHP passé en paramètre. Cette fonction prend un pointeur vers le _callable_ et un tableau d'arguments, et elle retourne le résultat de l'exécution du _callable_. Vous pouvez utiliser la syntaxe habituelle des _callables_ :
```php
<?php
$result = my_array_map([1, 2, 3], function($x) { return $x * 2; });
// $result vaudra [2, 4, 6]
$result = my_array_map(['hello', 'world'], 'strtoupper');
// $result vaudra ['HELLO', 'WORLD']
```
### Déclarer une Classe PHP Native
Le générateur prend en charge la déclaration de **classes opaques** comme structures Go, qui peuvent être utilisées pour créer des objets PHP. Vous pouvez utiliser la directive `//export_php:class` pour définir une classe PHP. Par exemple :

View File

@@ -128,14 +128,15 @@ type GoParameter struct {
Type string
}
var phpToGoTypeMap = map[phpType]string{
phpString: "string",
phpInt: "int64",
phpFloat: "float64",
phpBool: "bool",
phpArray: "*frankenphp.Array",
phpMixed: "any",
phpVoid: "",
var phpToGoTypeMap= map[phpType]string{
phpString: "string",
phpInt: "int64",
phpFloat: "float64",
phpBool: "bool",
phpArray: "*frankenphp.Array",
phpMixed: "any",
phpVoid: "",
phpCallable: "*C.zval",
}
func (gg *GoFileGenerator) phpTypeToGoType(phpT phpType) string {

View File

@@ -703,6 +703,125 @@ func testGoFileExportedFunctions(t *testing.T, content string, functions []phpFu
}
}
func TestGoFileGenerator_MethodWrapperWithCallableParams(t *testing.T) {
tmpDir := t.TempDir()
sourceContent := `package main
import "C"
//export_php:class CallableClass
type CallableStruct struct{}
//export_php:method CallableClass::processCallback(callable $callback): string
func (cs *CallableStruct) ProcessCallback(callback *C.zval) string {
return "processed"
}
//export_php:method CallableClass::processOptionalCallback(?callable $callback): string
func (cs *CallableStruct) ProcessOptionalCallback(callback *C.zval) string {
return "processed_optional"
}`
sourceFile := filepath.Join(tmpDir, "test.go")
require.NoError(t, os.WriteFile(sourceFile, []byte(sourceContent), 0644))
methods := []phpClassMethod{
{
Name: "ProcessCallback",
PhpName: "processCallback",
ClassName: "CallableClass",
Signature: "processCallback(callable $callback): string",
ReturnType: phpString,
Params: []phpParameter{
{Name: "callback", PhpType: phpCallable, IsNullable: false},
},
GoFunction: `func (cs *CallableStruct) ProcessCallback(callback *C.zval) string {
return "processed"
}`,
},
{
Name: "ProcessOptionalCallback",
PhpName: "processOptionalCallback",
ClassName: "CallableClass",
Signature: "processOptionalCallback(?callable $callback): string",
ReturnType: phpString,
Params: []phpParameter{
{Name: "callback", PhpType: phpCallable, IsNullable: true},
},
GoFunction: `func (cs *CallableStruct) ProcessOptionalCallback(callback *C.zval) string {
return "processed_optional"
}`,
},
}
classes := []phpClass{
{
Name: "CallableClass",
GoStruct: "CallableStruct",
Methods: methods,
},
}
generator := &Generator{
BaseName: "callable_test",
SourceFile: sourceFile,
Classes: classes,
BuildDir: tmpDir,
}
goGen := GoFileGenerator{generator}
content, err := goGen.buildContent()
require.NoError(t, err)
expectedCallableWrapperSignature := "func ProcessCallback_wrapper(handle C.uintptr_t, callback *C.zval) unsafe.Pointer"
assert.Contains(t, content, expectedCallableWrapperSignature, "Generated content should contain callable wrapper signature: %s", expectedCallableWrapperSignature)
expectedOptionalCallableWrapperSignature := "func ProcessOptionalCallback_wrapper(handle C.uintptr_t, callback *C.zval) unsafe.Pointer"
assert.Contains(t, content, expectedOptionalCallableWrapperSignature, "Generated content should contain optional callable wrapper signature: %s", expectedOptionalCallableWrapperSignature)
expectedCallableCall := "structObj.ProcessCallback(callback)"
assert.Contains(t, content, expectedCallableCall, "Generated content should contain callable method call: %s", expectedCallableCall)
expectedOptionalCallableCall := "structObj.ProcessOptionalCallback(callback)"
assert.Contains(t, content, expectedOptionalCallableCall, "Generated content should contain optional callable method call: %s", expectedOptionalCallableCall)
assert.Contains(t, content, "//export ProcessCallback_wrapper", "Generated content should contain ProcessCallback export directive")
assert.Contains(t, content, "//export ProcessOptionalCallback_wrapper", "Generated content should contain ProcessOptionalCallback export directive")
}
func TestGoFileGenerator_phpTypeToGoType(t *testing.T) {
generator := &Generator{}
goGen := GoFileGenerator{generator}
tests := []struct {
phpType phpType
expected string
}{
{phpString, "string"},
{phpInt, "int64"},
{phpFloat, "float64"},
{phpBool, "bool"},
{phpArray, "*frankenphp.Array"},
{phpMixed, "any"},
{phpVoid, ""},
{phpCallable, "*C.zval"},
}
for _, tt := range tests {
t.Run(string(tt.phpType), func(t *testing.T) {
result := goGen.phpTypeToGoType(tt.phpType)
assert.Equal(t, tt.expected, result, "phpTypeToGoType(%s) should return %s", tt.phpType, tt.expected)
})
}
t.Run("unknown_type", func(t *testing.T) {
unknownType := phpType("unknown")
result := goGen.phpTypeToGoType(unknownType)
assert.Equal(t, "any", result, "phpTypeToGoType should fallback to interface{} for unknown types")
})
}
func testGoFileInternalFunctions(t *testing.T, content string) {
internalIndicators := []string{
"func internalHelper",

View File

@@ -118,6 +118,30 @@ func (s *IntegrationTestSuite) runExtensionInit(sourceFile string) error {
return nil
}
// cleanupGeneratedFiles removes generated files from the original source directory
func (s *IntegrationTestSuite) cleanupGeneratedFiles(originalSourceFile string) {
s.t.Helper()
sourceDir := filepath.Dir(originalSourceFile)
baseName := SanitizePackageName(strings.TrimSuffix(filepath.Base(originalSourceFile), ".go"))
generatedFiles := []string{
baseName + ".stub.php",
baseName + "_arginfo.h",
baseName + ".h",
baseName + ".c",
baseName + ".go",
"README.md",
}
for _, file := range generatedFiles {
fullPath := filepath.Join(sourceDir, file)
if _, err := os.Stat(fullPath); err == nil {
os.Remove(fullPath)
}
}
}
// compileFrankenPHP compiles FrankenPHP with the generated extension
func (s *IntegrationTestSuite) compileFrankenPHP(moduleDir string) (string, error) {
s.t.Helper()
@@ -250,6 +274,7 @@ func TestBasicFunction(t *testing.T) {
sourceFile := filepath.Join("..", "..", "testdata", "integration", "basic_function.go")
sourceFile, err := filepath.Abs(sourceFile)
require.NoError(t, err)
defer suite.cleanupGeneratedFiles(sourceFile)
targetFile, err := suite.createGoModule(sourceFile)
require.NoError(t, err)
@@ -326,6 +351,7 @@ func TestClassMethodsIntegration(t *testing.T) {
sourceFile := filepath.Join("..", "..", "testdata", "integration", "class_methods.go")
sourceFile, err := filepath.Abs(sourceFile)
require.NoError(t, err)
defer suite.cleanupGeneratedFiles(sourceFile)
targetFile, err := suite.createGoModule(sourceFile)
require.NoError(t, err)
@@ -437,6 +463,7 @@ func TestConstants(t *testing.T) {
sourceFile := filepath.Join("..", "..", "testdata", "integration", "constants.go")
sourceFile, err := filepath.Abs(sourceFile)
require.NoError(t, err)
defer suite.cleanupGeneratedFiles(sourceFile)
targetFile, err := suite.createGoModule(sourceFile)
require.NoError(t, err)
@@ -536,6 +563,7 @@ func TestNamespace(t *testing.T) {
sourceFile := filepath.Join("..", "..", "testdata", "integration", "namespace.go")
sourceFile, err := filepath.Abs(sourceFile)
require.NoError(t, err)
defer suite.cleanupGeneratedFiles(sourceFile)
targetFile, err := suite.createGoModule(sourceFile)
require.NoError(t, err)
@@ -625,6 +653,7 @@ func TestInvalidSignature(t *testing.T) {
sourceFile := filepath.Join("..", "..", "testdata", "integration", "invalid_signature.go")
sourceFile, err := filepath.Abs(sourceFile)
require.NoError(t, err)
defer suite.cleanupGeneratedFiles(sourceFile)
targetFile, err := suite.createGoModule(sourceFile)
require.NoError(t, err)
@@ -640,6 +669,7 @@ func TestTypeMismatch(t *testing.T) {
sourceFile := filepath.Join("..", "..", "testdata", "integration", "type_mismatch.go")
sourceFile, err := filepath.Abs(sourceFile)
require.NoError(t, err)
defer suite.cleanupGeneratedFiles(sourceFile)
targetFile, err := suite.createGoModule(sourceFile)
require.NoError(t, err)
@@ -681,3 +711,83 @@ func dummy() {}
assert.Error(t, err, "should fail when gen_stub.php is missing")
assert.Contains(t, err.Error(), "gen_stub.php", "error should mention missing script")
}
func TestCallable(t *testing.T) {
suite := setupTest(t)
sourceFile := filepath.Join("..", "..", "testdata", "integration", "callable.go")
sourceFile, err := filepath.Abs(sourceFile)
require.NoError(t, err)
defer suite.cleanupGeneratedFiles(sourceFile)
targetFile, err := suite.createGoModule(sourceFile)
require.NoError(t, err)
err = suite.runExtensionInit(targetFile)
require.NoError(t, err)
_, err = suite.compileFrankenPHP(filepath.Dir(targetFile))
require.NoError(t, err)
err = suite.verifyPHPSymbols(
[]string{"my_array_map", "my_filter"},
[]string{"Processor"},
[]string{},
)
require.NoError(t, err, "all functions and classes should be accessible from PHP")
err = suite.verifyFunctionBehavior(`<?php
$result = my_array_map([1, 2, 3], function($x) { return $x * 2; });
if ($result !== [2, 4, 6]) {
echo "FAIL: my_array_map with closure expected [2, 4, 6], got " . json_encode($result);
exit(1);
}
$result = my_array_map(['hello', 'world'], 'strtoupper');
if ($result !== ['HELLO', 'WORLD']) {
echo "FAIL: my_array_map with function name expected ['HELLO', 'WORLD'], got " . json_encode($result);
exit(1);
}
$result = my_array_map([], function($x) { return $x; });
if ($result !== []) {
echo "FAIL: my_array_map with empty array expected [], got " . json_encode($result);
exit(1);
}
$result = my_filter([1, 2, 3, 4, 5, 6], function($x) { return $x % 2 === 0; });
if ($result !== [2, 4, 6]) {
echo "FAIL: my_filter expected [2, 4, 6], got " . json_encode($result);
exit(1);
}
$result = my_filter([1, 2, 3, 4], null);
if ($result !== [1, 2, 3, 4]) {
echo "FAIL: my_filter with null callback expected [1, 2, 3, 4], got " . json_encode($result);
exit(1);
}
$processor = new Processor();
$result = $processor->transform('hello', function($s) { return strtoupper($s); });
if ($result !== 'HELLO') {
echo "FAIL: Processor::transform with closure expected 'HELLO', got '$result'";
exit(1);
}
$result = $processor->transform('world', 'strtoupper');
if ($result !== 'WORLD') {
echo "FAIL: Processor::transform with function name expected 'WORLD', got '$result'";
exit(1);
}
$result = $processor->transform(' test ', 'trim');
if ($result !== 'test') {
echo "FAIL: Processor::transform with trim expected 'test', got '$result'";
exit(1);
}
echo "OK";
`, "OK")
require.NoError(t, err, "all callable tests should pass")
}

View File

@@ -9,17 +9,18 @@ import (
type phpType string
const (
phpString phpType = "string"
phpInt phpType = "int"
phpFloat phpType = "float"
phpBool phpType = "bool"
phpArray phpType = "array"
phpObject phpType = "object"
phpMixed phpType = "mixed"
phpVoid phpType = "void"
phpNull phpType = "null"
phpTrue phpType = "true"
phpFalse phpType = "false"
phpString phpType = "string"
phpInt phpType = "int"
phpFloat phpType = "float"
phpBool phpType = "bool"
phpArray phpType = "array"
phpObject phpType = "object"
phpMixed phpType = "mixed"
phpVoid phpType = "void"
phpNull phpType = "null"
phpTrue phpType = "true"
phpFalse phpType = "false"
phpCallable phpType = "callable"
)
type phpFunction struct {

View File

@@ -68,8 +68,12 @@ func (pp *ParameterParser) generateSingleParamDeclaration(param phpParameter) []
if param.IsNullable {
decls = append(decls, fmt.Sprintf("zend_bool %s_is_null = 0;", param.Name))
}
case phpArray, phpMixed:
case phpArray:
decls = append(decls, fmt.Sprintf("zend_array *%s = NULL;", param.Name))
case phpMixed:
decls = append(decls, fmt.Sprintf("zval *%s = NULL;", param.Name))
case "callable":
decls = append(decls, fmt.Sprintf("zval *%s_callback;", param.Name))
}
return decls
@@ -118,9 +122,11 @@ func (pp *ParameterParser) generateParamParsingMacro(param phpParameter) string
case phpBool:
return fmt.Sprintf("\n Z_PARAM_BOOL_OR_NULL(%s, %s_is_null)", param.Name, param.Name)
case phpArray:
return fmt.Sprintf("\n Z_PARAM_ARRAY_OR_NULL(%s)", param.Name)
return fmt.Sprintf("\n Z_PARAM_ARRAY_HT_OR_NULL(%s)", param.Name)
case phpMixed:
return fmt.Sprintf("\n Z_PARAM_ZVAL_OR_NULL(%s)", param.Name)
case phpCallable:
return fmt.Sprintf("\n Z_PARAM_ZVAL_OR_NULL(%s_callback)", param.Name)
default:
return ""
}
@@ -135,9 +141,11 @@ func (pp *ParameterParser) generateParamParsingMacro(param phpParameter) string
case phpBool:
return fmt.Sprintf("\n Z_PARAM_BOOL(%s)", param.Name)
case phpArray:
return fmt.Sprintf("\n Z_PARAM_ARRAY(%s)", param.Name)
return fmt.Sprintf("\n Z_PARAM_ARRAY_HT(%s)", param.Name)
case phpMixed:
return fmt.Sprintf("\n Z_PARAM_ZVAL(%s)", param.Name)
case phpCallable:
return fmt.Sprintf("\n Z_PARAM_ZVAL(%s_callback)", param.Name)
default:
return ""
}
@@ -168,6 +176,8 @@ func (pp *ParameterParser) generateSingleGoCallParam(param phpParameter) string
return fmt.Sprintf("%s_is_null ? NULL : &%s", param.Name, param.Name)
case phpBool:
return fmt.Sprintf("%s_is_null ? NULL : &%s", param.Name, param.Name)
case phpCallable:
return fmt.Sprintf("%s_callback", param.Name)
default:
return param.Name
}
@@ -180,6 +190,8 @@ func (pp *ParameterParser) generateSingleGoCallParam(param phpParameter) string
return fmt.Sprintf("(double) %s", param.Name)
case phpBool:
return fmt.Sprintf("(int) %s", param.Name)
case phpCallable:
return fmt.Sprintf("%s_callback", param.Name)
default:
return param.Name
}

View File

@@ -145,14 +145,14 @@ func TestParameterParser_GenerateParamDeclarations(t *testing.T) {
params: []phpParameter{
{Name: "items", PhpType: phpArray, HasDefault: false},
},
expected: " zval *items = NULL;",
expected: " zend_array *items = NULL;",
},
{
name: "nullable array parameter",
params: []phpParameter{
{Name: "items", PhpType: phpArray, HasDefault: false, IsNullable: true},
},
expected: " zval *items = NULL;",
expected: " zend_array *items = NULL;",
},
{
name: "mixed types with array",
@@ -161,7 +161,7 @@ func TestParameterParser_GenerateParamDeclarations(t *testing.T) {
{Name: "items", PhpType: phpArray, HasDefault: false},
{Name: "count", PhpType: phpInt, HasDefault: true, DefaultValue: "5"},
},
expected: " zend_string *name = NULL;\n zval *items = NULL;\n zend_long count = 5;",
expected: " zend_string *name = NULL;\n zend_array *items = NULL;\n zend_long count = 5;",
},
{
name: "mixed parameter",
@@ -177,6 +177,29 @@ func TestParameterParser_GenerateParamDeclarations(t *testing.T) {
},
expected: " zval *m = NULL;",
},
{
name: "callable parameter",
params: []phpParameter{
{Name: "callback", PhpType: phpCallable, HasDefault: false},
},
expected: " zval *callback_callback;",
},
{
name: "nullable callable parameter",
params: []phpParameter{
{Name: "callback", PhpType: phpCallable, HasDefault: false, IsNullable: true},
},
expected: " zval *callback_callback;",
},
{
name: "mixed types with callable",
params: []phpParameter{
{Name: "data", PhpType: phpArray, HasDefault: false},
{Name: "callback", PhpType: phpCallable, HasDefault: false},
{Name: "options", PhpType: phpInt, HasDefault: true, DefaultValue: "0"},
},
expected: " zend_array *data = NULL;\n zval *callback_callback;\n zend_long options = 0;",
},
}
for _, tt := range tests {
@@ -292,6 +315,29 @@ func TestParameterParser_GenerateGoCallParams(t *testing.T) {
},
expected: "name, items, (long) count",
},
{
name: "callable parameter",
params: []phpParameter{
{Name: "callback", PhpType: "callable"},
},
expected: "callback_callback",
},
{
name: "nullable callable parameter",
params: []phpParameter{
{Name: "callback", PhpType: "callable", IsNullable: true},
},
expected: "callback_callback",
},
{
name: "mixed parameters with callable",
params: []phpParameter{
{Name: "data", PhpType: "array"},
{Name: "callback", PhpType: "callable"},
{Name: "limit", PhpType: "int"},
},
expected: "data, callback_callback, (long) limit",
},
}
for _, tt := range tests {
@@ -353,12 +399,12 @@ func TestParameterParser_GenerateParamParsingMacro(t *testing.T) {
{
name: "array parameter",
param: phpParameter{Name: "items", PhpType: phpArray},
expected: "\n Z_PARAM_ARRAY(items)",
expected: "\n Z_PARAM_ARRAY_HT(items)",
},
{
name: "nullable array parameter",
param: phpParameter{Name: "items", PhpType: phpArray, IsNullable: true},
expected: "\n Z_PARAM_ARRAY_OR_NULL(items)",
expected: "\n Z_PARAM_ARRAY_HT_OR_NULL(items)",
},
{
name: "mixed parameter",
@@ -370,6 +416,16 @@ func TestParameterParser_GenerateParamParsingMacro(t *testing.T) {
param: phpParameter{Name: "m", PhpType: phpMixed, IsNullable: true},
expected: "\n Z_PARAM_ZVAL_OR_NULL(m)",
},
{
name: "callable parameter",
param: phpParameter{Name: "callback", PhpType: phpCallable},
expected: "\n Z_PARAM_ZVAL(callback_callback)",
},
{
name: "nullable callable parameter",
param: phpParameter{Name: "callback", PhpType: phpCallable, IsNullable: true},
expected: "\n Z_PARAM_ZVAL_OR_NULL(callback_callback)",
},
{
name: "unknown type",
param: phpParameter{Name: "unknown", PhpType: phpType("unknown")},
@@ -480,6 +536,16 @@ func TestParameterParser_GenerateSingleGoCallParam(t *testing.T) {
param: phpParameter{Name: "items", PhpType: phpArray, IsNullable: true},
expected: "items",
},
{
name: "callable parameter",
param: phpParameter{Name: "callback", PhpType: "callable"},
expected: "callback_callback",
},
{
name: "nullable callable parameter",
param: phpParameter{Name: "callback", PhpType: "callable", IsNullable: true},
expected: "callback_callback",
},
{
name: "unknown type",
param: phpParameter{Name: "unknown", PhpType: phpType("unknown")},
@@ -551,12 +617,22 @@ func TestParameterParser_GenerateSingleParamDeclaration(t *testing.T) {
{
name: "array parameter",
param: phpParameter{Name: "items", PhpType: phpArray, HasDefault: false},
expected: []string{"zval *items = NULL;"},
expected: []string{"zend_array *items = NULL;"},
},
{
name: "nullable array parameter",
param: phpParameter{Name: "items", PhpType: phpArray, HasDefault: false, IsNullable: true},
expected: []string{"zval *items = NULL;"},
expected: []string{"zend_array *items = NULL;"},
},
{
name: "callable parameter",
param: phpParameter{Name: "callback", PhpType: "callable", HasDefault: false},
expected: []string{"zval *callback_callback;"},
},
{
name: "nullable callable parameter",
param: phpParameter{Name: "callback", PhpType: "callable", HasDefault: false, IsNullable: true},
expected: []string{"zval *callback_callback;"},
},
}

View File

@@ -107,8 +107,8 @@ func TestPHPFunctionGenerator_Generate(t *testing.T) {
},
contains: []string{
"PHP_FUNCTION(process_array)",
"zval *input = NULL;",
"Z_PARAM_ARRAY(input)",
"zend_array *input = NULL;",
"Z_PARAM_ARRAY_HT(input)",
"zend_array *result = process_array(input);",
"RETURN_ARR(result)",
},
@@ -126,10 +126,10 @@ func TestPHPFunctionGenerator_Generate(t *testing.T) {
},
contains: []string{
"PHP_FUNCTION(filter_array)",
"zval *data = NULL;",
"zend_array *data = NULL;",
"zend_string *key = NULL;",
"zend_long limit = 10;",
"Z_PARAM_ARRAY(data)",
"Z_PARAM_ARRAY_HT(data)",
"Z_PARAM_STR(key)",
"Z_PARAM_LONG(limit)",
"ZEND_PARSE_PARAMETERS_START(2, 3)",
@@ -201,7 +201,7 @@ func TestPHPFunctionGenerator_GenerateParamDeclarations(t *testing.T) {
{Name: "items", PhpType: phpArray},
},
contains: []string{
"zval *items = NULL;",
"zend_array *items = NULL;",
},
},
{
@@ -213,7 +213,7 @@ func TestPHPFunctionGenerator_GenerateParamDeclarations(t *testing.T) {
},
contains: []string{
"zend_string *name = NULL;",
"zval *data = NULL;",
"zend_array *data = NULL;",
"zend_long count = 0;",
},
},

View File

@@ -95,7 +95,9 @@ PHP_METHOD({{namespacedClassName $.Namespace .ClassName}}, {{.PhpName}}) {
zend_bool {{$param.Name}} = {{if $param.HasDefault}}{{if eq $param.DefaultValue "true"}}1{{else}}0{{end}}{{else}}0{{end}};{{if $param.IsNullable}}
zend_bool {{$param.Name}}_is_null = 0;{{end}}
{{- else if eq $param.PhpType "array"}}
zval *{{$param.Name}} = NULL;
zend_array *{{$param.Name}} = NULL;
{{- else if eq $param.PhpType "callable"}}
zval *{{$param.Name}}_callback;
{{- end}}
{{- end}}
@@ -104,7 +106,7 @@ PHP_METHOD({{namespacedClassName $.Namespace .ClassName}}, {{.PhpName}}) {
{{$optionalStarted := false}}{{range .Params}}{{if .HasDefault}}{{if not $optionalStarted -}}
Z_PARAM_OPTIONAL
{{$optionalStarted = true}}{{end}}{{end -}}
{{if .IsNullable}}{{if eq .PhpType "string"}}Z_PARAM_STR_OR_NULL({{.Name}}, {{.Name}}_is_null){{else if eq .PhpType "int"}}Z_PARAM_LONG_OR_NULL({{.Name}}, {{.Name}}_is_null){{else if eq .PhpType "float"}}Z_PARAM_DOUBLE_OR_NULL({{.Name}}, {{.Name}}_is_null){{else if eq .PhpType "bool"}}Z_PARAM_BOOL_OR_NULL({{.Name}}, {{.Name}}_is_null){{else if eq .PhpType "array"}}Z_PARAM_ARRAY_OR_NULL({{.Name}}){{end}}{{else}}{{if eq .PhpType "string"}}Z_PARAM_STR({{.Name}}){{else if eq .PhpType "int"}}Z_PARAM_LONG({{.Name}}){{else if eq .PhpType "float"}}Z_PARAM_DOUBLE({{.Name}}){{else if eq .PhpType "bool"}}Z_PARAM_BOOL({{.Name}}){{else if eq .PhpType "array"}}Z_PARAM_ARRAY({{.Name}}){{end}}{{end}}
{{if .IsNullable}}{{if eq .PhpType "string"}}Z_PARAM_STR_OR_NULL({{.Name}}, {{.Name}}_is_null){{else if eq .PhpType "int"}}Z_PARAM_LONG_OR_NULL({{.Name}}, {{.Name}}_is_null){{else if eq .PhpType "float"}}Z_PARAM_DOUBLE_OR_NULL({{.Name}}, {{.Name}}_is_null){{else if eq .PhpType "bool"}}Z_PARAM_BOOL_OR_NULL({{.Name}}, {{.Name}}_is_null){{else if eq .PhpType "array"}}Z_PARAM_ARRAY_HT_OR_NULL({{.Name}}){{else if eq .PhpType "callable"}}Z_PARAM_ZVAL_OR_NULL({{.Name}}_callback){{end}}{{else}}{{if eq .PhpType "string"}}Z_PARAM_STR({{.Name}}){{else if eq .PhpType "int"}}Z_PARAM_LONG({{.Name}}){{else if eq .PhpType "float"}}Z_PARAM_DOUBLE({{.Name}}){{else if eq .PhpType "bool"}}Z_PARAM_BOOL({{.Name}}){{else if eq .PhpType "array"}}Z_PARAM_ARRAY_HT({{.Name}}){{else if eq .PhpType "callable"}}Z_PARAM_ZVAL({{.Name}}_callback){{end}}{{end}}
{{end -}}
ZEND_PARSE_PARAMETERS_END();
{{else}}
@@ -113,22 +115,22 @@ PHP_METHOD({{namespacedClassName $.Namespace .ClassName}}, {{.PhpName}}) {
{{- if ne .ReturnType "void"}}
{{- if eq .ReturnType "string"}}
zend_string* result = {{.Name}}_wrapper(intern->go_handle{{if .Params}}{{range .Params}}, {{if .IsNullable}}{{if eq .PhpType "string"}}{{.Name}}_is_null ? NULL : {{.Name}}{{else if eq .PhpType "int"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "float"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "bool"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "array"}}{{.Name}}{{end}}{{else}}{{.Name}}{{end}}{{end}}{{end}});
zend_string* result = {{.Name}}_wrapper(intern->go_handle{{if .Params}}{{range .Params}}, {{if .IsNullable}}{{if eq .PhpType "string"}}{{.Name}}_is_null ? NULL : {{.Name}}{{else if eq .PhpType "int"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "float"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "bool"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "array"}}{{.Name}}{{else if eq .PhpType "callable"}}{{.Name}}_callback{{end}}{{else}}{{if eq .PhpType "array"}}{{.Name}}{{else if eq .PhpType "callable"}}{{.Name}}_callback{{else}}{{.Name}}{{end}}{{end}}{{end}}{{end}});
if (result) {
RETURN_STR(result);
}
RETURN_EMPTY_STRING();
{{- else if eq .ReturnType "int"}}
zend_long result = {{.Name}}_wrapper(intern->go_handle{{if .Params}}{{range .Params}}, {{if .IsNullable}}{{if eq .PhpType "string"}}{{.Name}}_is_null ? NULL : {{.Name}}{{else if eq .PhpType "int"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "float"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "bool"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "array"}}{{.Name}} ? Z_ARRVAL_P({{.Name}}) : NULL{{end}}{{else}}{{if eq .PhpType "array"}}Z_ARRVAL_P({{.Name}}){{else}}(long){{.Name}}{{end}}{{end}}{{end}}{{end}});
zend_long result = {{.Name}}_wrapper(intern->go_handle{{if .Params}}{{range .Params}}, {{if .IsNullable}}{{if eq .PhpType "string"}}{{.Name}}_is_null ? NULL : {{.Name}}{{else if eq .PhpType "int"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "float"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "bool"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "array"}}{{.Name}}{{else if eq .PhpType "callable"}}{{.Name}}_callback{{end}}{{else}}{{if eq .PhpType "array"}}{{.Name}}{{else if eq .PhpType "callable"}}{{.Name}}_callback{{else}}(long){{.Name}}{{end}}{{end}}{{end}}{{end}});
RETURN_LONG(result);
{{- else if eq .ReturnType "float"}}
double result = {{.Name}}_wrapper(intern->go_handle{{if .Params}}{{range .Params}}, {{if .IsNullable}}{{if eq .PhpType "string"}}{{.Name}}_is_null ? NULL : {{.Name}}{{else if eq .PhpType "int"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "float"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "bool"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "array"}}{{.Name}} ? Z_ARRVAL_P({{.Name}}) : NULL{{end}}{{else}}{{if eq .PhpType "array"}}Z_ARRVAL_P({{.Name}}){{else}}(double){{.Name}}{{end}}{{end}}{{end}}{{end}});
double result = {{.Name}}_wrapper(intern->go_handle{{if .Params}}{{range .Params}}, {{if .IsNullable}}{{if eq .PhpType "string"}}{{.Name}}_is_null ? NULL : {{.Name}}{{else if eq .PhpType "int"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "float"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "bool"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "array"}}{{.Name}}{{else if eq .PhpType "callable"}}{{.Name}}_callback{{end}}{{else}}{{if eq .PhpType "array"}}{{.Name}}{{else if eq .PhpType "callable"}}{{.Name}}_callback{{else}}(double){{.Name}}{{end}}{{end}}{{end}}{{end}});
RETURN_DOUBLE(result);
{{- else if eq .ReturnType "bool"}}
int result = {{.Name}}_wrapper(intern->go_handle{{if .Params}}{{range .Params}}, {{if .IsNullable}}{{if eq .PhpType "string"}}{{.Name}}_is_null ? NULL : {{.Name}}{{else if eq .PhpType "int"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "float"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "bool"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "array"}}{{.Name}} ? Z_ARRVAL_P({{.Name}}) : NULL{{end}}{{else}}{{if eq .PhpType "array"}}Z_ARRVAL_P({{.Name}}){{else}}(int){{.Name}}{{end}}{{end}}{{end}}{{end}});
int result = {{.Name}}_wrapper(intern->go_handle{{if .Params}}{{range .Params}}, {{if .IsNullable}}{{if eq .PhpType "string"}}{{.Name}}_is_null ? NULL : {{.Name}}{{else if eq .PhpType "int"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "float"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "bool"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "array"}}{{.Name}}{{else if eq .PhpType "callable"}}{{.Name}}_callback{{end}}{{else}}{{if eq .PhpType "array"}}{{.Name}}{{else if eq .PhpType "callable"}}{{.Name}}_callback{{else}}(int){{.Name}}{{end}}{{end}}{{end}}{{end}});
RETURN_BOOL(result);
{{- else if eq .ReturnType "array"}}
void* result = {{.Name}}_wrapper(intern->go_handle{{if .Params}}{{range .Params}}, {{if .IsNullable}}{{if eq .PhpType "string"}}{{.Name}}_is_null ? NULL : {{.Name}}{{else if eq .PhpType "int"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "float"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "bool"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "array"}}{{.Name}} ? Z_ARRVAL_P({{.Name}}) : NULL{{end}}{{else}}{{if eq .PhpType "array"}}Z_ARRVAL_P({{.Name}}){{else}}{{.Name}}{{end}}{{end}}{{end}}{{end}});
void* result = {{.Name}}_wrapper(intern->go_handle{{if .Params}}{{range .Params}}, {{if .IsNullable}}{{if eq .PhpType "string"}}{{.Name}}_is_null ? NULL : {{.Name}}{{else if eq .PhpType "int"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "float"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "bool"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "array"}}{{.Name}}{{else if eq .PhpType "callable"}}{{.Name}}_callback{{end}}{{else}}{{if eq .PhpType "array"}}{{.Name}}{{else if eq .PhpType "callable"}}{{.Name}}_callback{{else}}{{.Name}}{{end}}{{end}}{{end}}{{end}});
if (result != NULL) {
HashTable *ht = (HashTable*)result;
RETURN_ARR(ht);
@@ -137,7 +139,7 @@ PHP_METHOD({{namespacedClassName $.Namespace .ClassName}}, {{.PhpName}}) {
}
{{- end}}
{{- else}}
{{.Name}}_wrapper(intern->go_handle{{if .Params}}{{range .Params}}, {{if .IsNullable}}{{if eq .PhpType "string"}}{{.Name}}_is_null ? NULL : {{.Name}}{{else if eq .PhpType "int"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "float"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "bool"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "array"}}{{.Name}} ? Z_ARRVAL_P({{.Name}}) : NULL{{end}}{{else}}{{if eq .PhpType "string"}}{{.Name}}{{else if eq .PhpType "int"}}(long){{.Name}}{{else if eq .PhpType "float"}}(double){{.Name}}{{else if eq .PhpType "bool"}}(int){{.Name}}{{else if eq .PhpType "array"}}Z_ARRVAL_P({{.Name}}){{end}}{{end}}{{end}}{{end}});
{{.Name}}_wrapper(intern->go_handle{{if .Params}}{{range .Params}}, {{if .IsNullable}}{{if eq .PhpType "string"}}{{.Name}}_is_null ? NULL : {{.Name}}{{else if eq .PhpType "int"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "float"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "bool"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "array"}}{{.Name}}{{else if eq .PhpType "callable"}}{{.Name}}_callback{{end}}{{else}}{{if eq .PhpType "string"}}{{.Name}}{{else if eq .PhpType "int"}}(long){{.Name}}{{else if eq .PhpType "float"}}(double){{.Name}}{{else if eq .PhpType "bool"}}(int){{.Name}}{{else if eq .PhpType "array"}}{{.Name}}{{else if eq .PhpType "callable"}}{{.Name}}_callback{{end}}{{end}}{{end}}{{end}});
{{- end}}
}
{{end}}{{end}}

View File

@@ -76,7 +76,7 @@ func create_{{.GoStruct}}_object() C.uintptr_t {
{{- end}}
{{- range .Methods}}
//export {{.Name}}_wrapper
func {{.Name}}_wrapper(handle C.uintptr_t{{range .Params}}{{if eq .PhpType "string"}}, {{.Name}} *C.zend_string{{else if eq .PhpType "array"}}, {{.Name}} *C.zval{{else}}, {{.Name}} {{if .IsNullable}}*{{end}}{{phpTypeToGoType .PhpType}}{{end}}{{end}}){{if not (isVoid .ReturnType)}}{{if isStringOrArray .ReturnType}} unsafe.Pointer{{else}} {{phpTypeToGoType .ReturnType}}{{end}}{{end}} {
func {{.Name}}_wrapper(handle C.uintptr_t{{range .Params}}{{if eq .PhpType "string"}}, {{.Name}} *C.zend_string{{else if eq .PhpType "array"}}, {{.Name}} *C.zval{{else if eq .PhpType "callable"}}, {{.Name}} *C.zval{{else}}, {{.Name}} {{if .IsNullable}}*{{end}}{{phpTypeToGoType .PhpType}}{{end}}{{end}}){{if not (isVoid .ReturnType)}}{{if isStringOrArray .ReturnType}} unsafe.Pointer{{else}} {{phpTypeToGoType .ReturnType}}{{end}}{{end}} {
obj := getGoObject(handle)
if obj == nil {
{{- if not (isVoid .ReturnType)}}

View File

@@ -11,10 +11,10 @@ import (
)
var (
paramTypes = []phpType{phpString, phpInt, phpFloat, phpBool, phpArray, phpObject, phpMixed}
paramTypes = []phpType{phpString, phpInt, phpFloat, phpBool, phpArray, phpObject, phpMixed, phpCallable}
returnTypes = []phpType{phpVoid, phpString, phpInt, phpFloat, phpBool, phpArray, phpObject, phpMixed, phpNull, phpTrue, phpFalse}
propTypes = []phpType{phpString, phpInt, phpFloat, phpBool, phpArray, phpObject, phpMixed}
supportedTypes = []phpType{phpString, phpInt, phpFloat, phpBool, phpArray, phpMixed}
supportedTypes = []phpType{phpString, phpInt, phpFloat, phpBool, phpArray, phpMixed, phpCallable}
functionNameRegex = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)
parameterNameRegex = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)
@@ -160,8 +160,10 @@ func (v *Validator) validateGoFunctionSignatureWithOptions(phpFunc phpFunction,
effectiveGoParamCount = goParamCount - 1
}
if len(phpFunc.Params) != effectiveGoParamCount {
return fmt.Errorf("parameter count mismatch: PHP function has %d parameters but Go function has %d", len(phpFunc.Params), effectiveGoParamCount)
expectedGoParams := len(phpFunc.Params)
if expectedGoParams != effectiveGoParamCount {
return fmt.Errorf("parameter count mismatch: PHP function has %d parameters (expecting %d Go parameters) but Go function has %d", len(phpFunc.Params), expectedGoParams, effectiveGoParamCount)
}
if goFunc.Type.Params != nil && len(phpFunc.Params) > 0 {
@@ -207,11 +209,13 @@ func (v *Validator) phpTypeToGoType(t phpType, isNullable bool) string {
baseType = "*C.zend_array"
case phpMixed:
baseType = "*C.zval"
case phpCallable:
baseType = "*C.zval"
default:
baseType = "any"
}
if isNullable && t != phpString && t != phpArray {
if isNullable && t != phpString && t != phpArray && t != phpCallable {
return "*" + baseType
}

View File

@@ -60,6 +60,53 @@ func TestValidateFunction(t *testing.T) {
},
expectError: false,
},
{
name: "valid function with array parameter",
function: phpFunction{
Name: "arrayFunction",
ReturnType: "array",
Params: []phpParameter{
{Name: "items", PhpType: phpArray},
{Name: "filter", PhpType: phpString},
},
},
expectError: false,
},
{
name: "valid function with nullable array parameter",
function: phpFunction{
Name: "nullableArrayFunction",
ReturnType: "string",
Params: []phpParameter{
{Name: "items", PhpType: phpArray, IsNullable: true},
{Name: "name", PhpType: phpString},
},
},
expectError: false,
},
{
name: "valid function with callable parameter",
function: phpFunction{
Name: "callableFunction",
ReturnType: "array",
Params: []phpParameter{
{Name: "data", PhpType: phpArray},
{Name: "callback", PhpType: phpCallable},
},
},
expectError: false,
},
{
name: "valid function with nullable callable parameter",
function: phpFunction{
Name: "nullableCallableFunction",
ReturnType: "string",
Params: []phpParameter{
{Name: "callback", PhpType: phpCallable, IsNullable: true},
},
},
expectError: false,
},
{
name: "empty function name",
function: phpFunction{
@@ -304,6 +351,23 @@ func TestValidateParameter(t *testing.T) {
},
expectError: false,
},
{
name: "valid callable parameter",
param: phpParameter{
Name: "callbackParam",
PhpType: phpCallable,
},
expectError: false,
},
{
name: "valid nullable callable parameter",
param: phpParameter{
Name: "nullableCallbackParam",
PhpType: "callable",
IsNullable: true,
},
expectError: false,
},
{
name: "empty parameter name",
param: phpParameter{
@@ -484,6 +548,28 @@ func TestValidateTypes(t *testing.T) {
},
expectError: false,
},
{
name: "valid callable parameter",
function: phpFunction{
Name: "callableFunction",
ReturnType: "array",
Params: []phpParameter{
{Name: "callbackParam", PhpType: phpCallable},
},
},
expectError: false,
},
{
name: "valid nullable callable parameter",
function: phpFunction{
Name: "nullableCallableFunction",
ReturnType: "string",
Params: []phpParameter{
{Name: "callbackParam", PhpType: phpCallable, IsNullable: true},
},
},
expectError: false,
},
{
name: "invalid object parameter",
function: phpFunction{
@@ -600,7 +686,7 @@ func TestValidateGoFunctionSignature(t *testing.T) {
}`,
},
expectError: true,
errorMsg: "parameter count mismatch: PHP function has 2 parameters but Go function has 1",
errorMsg: "parameter count mismatch: PHP function has 2 parameters (expecting 2 Go parameters) but Go function has 1",
},
{
name: "parameter type mismatch",
@@ -702,6 +788,50 @@ func TestValidateGoFunctionSignature(t *testing.T) {
},
GoFunction: `func mixedFunc(data *C.zend_array, filter *C.zend_string, limit int64) unsafe.Pointer {
return nil
}`,
},
expectError: false,
},
{
name: "valid callable parameter",
phpFunc: phpFunction{
Name: "callableFunc",
ReturnType: "array",
Params: []phpParameter{
{Name: "callback", PhpType: phpCallable},
},
GoFunction: `func callableFunc(callback *C.zval) unsafe.Pointer {
return nil
}`,
},
expectError: false,
},
{
name: "valid nullable callable parameter",
phpFunc: phpFunction{
Name: "nullableCallableFunc",
ReturnType: "string",
Params: []phpParameter{
{Name: "callback", PhpType: phpCallable, IsNullable: true},
},
GoFunction: `func nullableCallableFunc(callback *C.zval) unsafe.Pointer {
return nil
}`,
},
expectError: false,
},
{
name: "mixed callable and other parameters",
phpFunc: phpFunction{
Name: "mixedCallableFunc",
ReturnType: "array",
Params: []phpParameter{
{Name: "data", PhpType: phpArray},
{Name: "callback", PhpType: phpCallable},
{Name: "options", PhpType: "int"},
},
GoFunction: `func mixedCallableFunc(data *C.zend_array, callback *C.zval, options int64) unsafe.Pointer {
return nil
}`,
},
expectError: false,
@@ -739,6 +869,8 @@ func TestPhpTypeToGoType(t *testing.T) {
{"bool", true, "*bool"},
{"array", false, "*C.zend_array"},
{"array", true, "*C.zend_array"},
{"callable", false, "*C.zval"},
{"callable", true, "*C.zval"},
{"unknown", false, "any"},
}

64
testdata/integration/callable.go vendored Normal file
View File

@@ -0,0 +1,64 @@
package testintegration
// #include <Zend/zend_types.h>
import "C"
import (
"unsafe"
"github.com/dunglas/frankenphp"
)
// export_php:function my_array_map(array $data, callable $callback): array
func my_array_map(arr *C.zend_array, callback *C.zval) unsafe.Pointer {
goArray, err := frankenphp.GoPackedArray[any](unsafe.Pointer(arr))
if err != nil {
return nil
}
result := make([]any, len(goArray))
for i, item := range goArray {
callResult := frankenphp.CallPHPCallable(unsafe.Pointer(callback), []any{item})
result[i] = callResult
}
return frankenphp.PHPPackedArray[any](result)
}
// export_php:function my_filter(array $data, ?callable $callback): array
func my_filter(arr *C.zend_array, callback *C.zval) unsafe.Pointer {
goArray, err := frankenphp.GoPackedArray[any](unsafe.Pointer(arr))
if err != nil {
return nil
}
if callback == nil {
return unsafe.Pointer(arr)
}
result := make([]any, 0)
for _, item := range goArray {
callResult := frankenphp.CallPHPCallable(unsafe.Pointer(callback), []any{item})
if boolResult, ok := callResult.(bool); ok && boolResult {
result = append(result, item)
}
}
return frankenphp.PHPPackedArray[any](result)
}
// export_php:class Processor
type Processor struct{}
// export_php:method Processor::transform(string $input, callable $transformer): string
func (p *Processor) Transform(input *C.zend_string, callback *C.zval) unsafe.Pointer {
goInput := frankenphp.GoString(unsafe.Pointer(input))
callResult := frankenphp.CallPHPCallable(unsafe.Pointer(callback), []any{goInput})
resultStr, ok := callResult.(string)
if !ok {
return unsafe.Pointer(input)
}
return frankenphp.PHPString(resultStr, false)
}

10
types.c
View File

@@ -16,6 +16,8 @@ Bucket *get_ht_bucket_data(HashTable *ht, uint32_t index) {
void *__emalloc__(size_t size) { return emalloc(size); }
void __efree__(void *ptr) { efree(ptr); }
void __zend_hash_init__(HashTable *ht, uint32_t nSize, dtor_func_t pDestructor,
bool persistent) {
zend_hash_init(ht, nSize, NULL, pDestructor, persistent);
@@ -36,3 +38,11 @@ void __zval_empty_string__(zval *zv) { ZVAL_EMPTY_STRING(zv); }
void __zval_arr__(zval *zv, zend_array *arr) { ZVAL_ARR(zv, arr); }
zend_array *__zend_new_array__(uint32_t size) { return zend_new_array(size); }
int __zend_is_callable__(zval *cb) { return zend_is_callable(cb, 0, NULL); }
int __call_user_function__(zval *function_name, zval *retval,
uint32_t param_count, zval params[]) {
return call_user_function(CG(function_table), NULL, function_name, retval,
param_count, params);
}

View File

@@ -228,12 +228,14 @@ func phpArray[T any](entries map[string]T, order []string) unsafe.Pointer {
val := entries[key]
zval := phpValue(val)
C.zend_hash_str_update(zendArray, toUnsafeChar(key), C.size_t(len(key)), zval)
C.__efree__(unsafe.Pointer(zval))
}
} else {
zendArray = createNewArray((uint32)(len(entries)))
for key, val := range entries {
zval := phpValue(val)
C.zend_hash_str_update(zendArray, toUnsafeChar(key), C.size_t(len(key)), zval)
C.__efree__(unsafe.Pointer(zval))
}
}
@@ -246,6 +248,7 @@ func PHPPackedArray[T any](slice []T) unsafe.Pointer {
for _, val := range slice {
zval := phpValue(val)
C.zend_hash_next_index_insert(zendArray, zval)
C.__efree__(unsafe.Pointer(zval))
}
return unsafe.Pointer(zendArray)
@@ -368,42 +371,43 @@ func PHPValue(value any) unsafe.Pointer {
}
func phpValue(value any) *C.zval {
var zval C.zval
zval := (*C.zval)(C.__emalloc__(C.size_t(unsafe.Sizeof(C.zval{}))))
if toZvalObj, ok := value.(toZval); ok {
toZvalObj.toZval(&zval)
return &zval
toZvalObj.toZval(zval)
return zval
}
switch v := value.(type) {
case nil:
C.__zval_null__(&zval)
C.__zval_null__(zval)
case bool:
C.__zval_bool__(&zval, C._Bool(v))
C.__zval_bool__(zval, C._Bool(v))
case int:
C.__zval_long__(&zval, C.zend_long(v))
C.__zval_long__(zval, C.zend_long(v))
case int64:
C.__zval_long__(&zval, C.zend_long(v))
C.__zval_long__(zval, C.zend_long(v))
case float64:
C.__zval_double__(&zval, C.double(v))
C.__zval_double__(zval, C.double(v))
case string:
if v == "" {
C.__zval_empty_string__(&zval)
C.__zval_empty_string__(zval)
break
}
str := (*C.zend_string)(PHPString(v, false))
C.__zval_string__(&zval, str)
C.__zval_string__(zval, str)
case AssociativeArray[any]:
C.__zval_arr__(&zval, (*C.zend_array)(PHPAssociativeArray[any](v)))
C.__zval_arr__(zval, (*C.zend_array)(PHPAssociativeArray[any](v)))
case map[string]any:
C.__zval_arr__(&zval, (*C.zend_array)(PHPMap[any](v)))
C.__zval_arr__(zval, (*C.zend_array)(PHPMap[any](v)))
case []any:
C.__zval_arr__(&zval, (*C.zend_array)(PHPPackedArray[any](v)))
C.__zval_arr__(zval, (*C.zend_array)(PHPPackedArray[any](v)))
default:
C.__efree__(unsafe.Pointer(zval))
panic(fmt.Sprintf("unsupported Go type %T", v))
}
return &zval
return zval
}
// createNewArray creates a new zend_array with the specified size.
@@ -456,3 +460,56 @@ func zendHashDestroy(p unsafe.Pointer) {
ht := (*C.zend_array)(p)
C.zend_hash_destroy(ht)
}
// EXPERIMENTAL: CallPHPCallable executes a PHP callable with the given parameters.
// Returns the result of the callable as a Go interface{}, or nil if the call failed.
func CallPHPCallable(cb unsafe.Pointer, params []interface{}) interface{} {
if cb == nil {
return nil
}
callback := (*C.zval)(cb)
if callback == nil {
return nil
}
if C.__zend_is_callable__(callback) == 0 {
return nil
}
paramCount := len(params)
var paramStorage *C.zval
if paramCount > 0 {
paramStorage = (*C.zval)(C.__emalloc__(C.size_t(paramCount) * C.size_t(unsafe.Sizeof(C.zval{}))))
defer func() {
for i := 0; i < paramCount; i++ {
targetZval := (*C.zval)(unsafe.Pointer(uintptr(unsafe.Pointer(paramStorage)) + uintptr(i)*unsafe.Sizeof(C.zval{})))
C.zval_ptr_dtor(targetZval)
}
C.__efree__(unsafe.Pointer(paramStorage))
}()
for i, param := range params {
targetZval := (*C.zval)(unsafe.Pointer(uintptr(unsafe.Pointer(paramStorage)) + uintptr(i)*unsafe.Sizeof(C.zval{})))
sourceZval := phpValue(param)
*targetZval = *sourceZval
C.__efree__(unsafe.Pointer(sourceZval))
}
}
var retval C.zval
result := C.__call_user_function__(callback, &retval, C.uint32_t(paramCount), paramStorage)
if result != C.SUCCESS {
return nil
}
goResult, err := goValue[any](&retval)
C.zval_ptr_dtor(&retval)
if err != nil {
return nil
}
return goResult
}

View File

@@ -11,9 +11,14 @@ zval *get_ht_packed_data(HashTable *, uint32_t index);
Bucket *get_ht_bucket_data(HashTable *, uint32_t index);
void *__emalloc__(size_t size);
void __efree__(void *ptr);
void __zend_hash_init__(HashTable *ht, uint32_t nSize, dtor_func_t pDestructor,
bool persistent);
int __zend_is_callable__(zval *cb);
int __call_user_function__(zval *function_name, zval *retval,
uint32_t param_count, zval params[]);
void __zval_null__(zval *zv);
void __zval_bool__(zval *zv, bool val);
void __zval_long__(zval *zv, zend_long val);