diff --git a/docs/extensions.md b/docs/extensions.md index 03cc3e45e9..ce3dd7eb28 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -81,17 +81,18 @@ While the first point speaks for itself, the second may be harder to apprehend. While some variable types have the same memory representation between C/PHP and Go, some types require more logic to be directly used. This is maybe the hardest part when it comes to writing extensions because it requires understanding internals of the Zend Engine and how variables are stored internally in PHP. 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.Array` | ❌ | frankenphp.GoArray() | frankenphp.PHPArray() | ✅ | -| `object` | `struct` | ❌ | _Not yet implemented_ | _Not yet implemented_ | ❌ | +| 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.Array` | ❌ | frankenphp.GoArray() | frankenphp.PHPArray() | ✅ | +| `callable` | `*C.zval` | ❌ | - | frankenphp.CallPHPCallable() | ❌ | +| `object` | - | ❌ | _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. @@ -151,6 +152,54 @@ func process_data(arr *C.zval) unsafe.Pointer { * `At(index uint32) (PHPKey, interface{})` - Get key-value pair at index * `frankenphp.PHPArray(arr *frankenphp.Array) unsafe.Pointer` - Convert to PHP array +### 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.zval, callback *C.zval) unsafe.Pointer { + goArr := frankenphp.GoArray(unsafe.Pointer(arr)) + result := &frankenphp.Array{} + + for i := uint32(0); i < goArr.Len(); i++ { + key, value := goArr.At(i) + + callbackResult := frankenphp.CallPHPCallable(unsafe.Pointer(callback), []interface{}{value}) + + if key.Type == frankenphp.PHPIntKey { + result.SetInt(key.Int, callbackResult) + } else { + result.SetString(key.Str, callbackResult) + } + } + + return frankenphp.PHPArray(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 + 'hello', 'b' => 'world', 'c' => 'php']; +$result = my_array_map($strArray, 'strtoupper'); // $result will be ['a' => 'HELLO', 'b' => 'WORLD', 'c' => 'PHP'] + +$arr = [1, 2, 3, 4, [5, 6]]; +$result = my_array_map($arr, function($item) { + if (\is_array($item)) { + return my_array_map($item, function($subItem) { + return $subItem * 2; + }); + } + + return $item * 3; +}); // $result will be [3, 6, 9, 12, [10, 12]] +``` + ### 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: diff --git a/docs/fr/extensions.md b/docs/fr/extensions.md index d2be2b6fb0..05dab0acc1 100644 --- a/docs/fr/extensions.md +++ b/docs/fr/extensions.md @@ -151,6 +151,54 @@ func process_data(arr *C.zval) unsafe.Pointer { * `At(index uint32) (PHPKey, interface{})` - Obtenir la paire clé-valeur à l'index * `frankenphp.PHPArray(arr *frankenphp.Array) unsafe.Pointer` - Convertir vers un tableau PHP +### 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.zval, callback *C.zval) unsafe.Pointer { + goArr := frankenphp.GoArray(unsafe.Pointer(arr)) + result := &frankenphp.Array{} + + for i := uint32(0); i < goArr.Len(); i++ { + key, value := goArr.At(i) + + callbackResult := frankenphp.CallPHPCallable(unsafe.Pointer(callback), []interface{}{value}) + + if key.Type == frankenphp.PHPIntKey { + result.SetInt(key.Int, callbackResult) + } else { + result.SetString(key.Str, callbackResult) + } + } + + return frankenphp.PHPArray(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 + 'hello', 'b' => 'world', 'c' => 'php']; +$result = my_array_map($strArray, 'strtoupper'); // $result vaudra ['a' => 'HELLO', 'b' => 'WORLD', 'c' => 'PHP'] + +$arr = [1, 2, 3, 4, [5, 6]]; +$result = my_array_map($arr, function($item) { + if (\is_array($item)) { + return my_array_map($item, function($subItem) { + return $subItem * 2; + }); + } + + return $item * 3; +}); // $result vaudra [3, 6, 9, 12, [10, 12]] +``` + ### 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 : diff --git a/internal/extgen/paramparser.go b/internal/extgen/paramparser.go index bf9994df08..f95821249b 100644 --- a/internal/extgen/paramparser.go +++ b/internal/extgen/paramparser.go @@ -70,6 +70,8 @@ func (pp *ParameterParser) generateSingleParamDeclaration(param phpParameter) [] } case "array": decls = append(decls, fmt.Sprintf("zval *%s = NULL;", param.Name)) + case "callable": + decls = append(decls, fmt.Sprintf("zval *%s_callback;", param.Name)) } return decls @@ -119,6 +121,8 @@ func (pp *ParameterParser) generateParamParsingMacro(param phpParameter) string return fmt.Sprintf("\n Z_PARAM_BOOL_OR_NULL(%s, %s_is_null)", param.Name, param.Name) case "array": return fmt.Sprintf("\n Z_PARAM_ARRAY_OR_NULL(%s)", param.Name) + case "callable": + return fmt.Sprintf("\n Z_PARAM_ZVAL_OR_NULL(%s_callback)", param.Name) default: return "" } @@ -134,6 +138,8 @@ func (pp *ParameterParser) generateParamParsingMacro(param phpParameter) string return fmt.Sprintf("\n Z_PARAM_BOOL(%s)", param.Name) case "array": return fmt.Sprintf("\n Z_PARAM_ARRAY(%s)", param.Name) + case "callable": + return fmt.Sprintf("\n Z_PARAM_ZVAL(%s_callback)", param.Name) default: return "" } @@ -166,6 +172,8 @@ func (pp *ParameterParser) generateSingleGoCallParam(param phpParameter) string return fmt.Sprintf("%s_is_null ? NULL : &%s", param.Name, param.Name) case "array": return param.Name + case "callable": + return fmt.Sprintf("%s_callback", param.Name) default: return param.Name } @@ -181,6 +189,8 @@ func (pp *ParameterParser) generateSingleGoCallParam(param phpParameter) string return fmt.Sprintf("(int) %s", param.Name) case "array": return param.Name + case "callable": + return fmt.Sprintf("%s_callback", param.Name) default: return param.Name } diff --git a/internal/extgen/paramparser_test.go b/internal/extgen/paramparser_test.go index 5eb1649eb5..2fd915ed3d 100644 --- a/internal/extgen/paramparser_test.go +++ b/internal/extgen/paramparser_test.go @@ -163,6 +163,29 @@ func TestParameterParser_GenerateParamDeclarations(t *testing.T) { }, expected: " zend_string *name = NULL;\n zval *items = NULL;\n zend_long count = 5;", }, + { + name: "callable parameter", + params: []phpParameter{ + {Name: "callback", PhpType: "callable", HasDefault: false}, + }, + expected: " zval *callback_callback;", + }, + { + name: "nullable callable parameter", + params: []phpParameter{ + {Name: "callback", PhpType: "callable", HasDefault: false, IsNullable: true}, + }, + expected: " zval *callback_callback;", + }, + { + name: "mixed types with callable", + params: []phpParameter{ + {Name: "data", PhpType: "array", HasDefault: false}, + {Name: "callback", PhpType: "callable", HasDefault: false}, + {Name: "options", PhpType: "int", HasDefault: true, DefaultValue: "0"}, + }, + expected: " zval *data = NULL;\n zval *callback_callback;\n zend_long options = 0;", + }, } for _, tt := range tests { @@ -278,6 +301,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 { @@ -346,6 +392,16 @@ func TestParameterParser_GenerateParamParsingMacro(t *testing.T) { param: phpParameter{Name: "items", PhpType: "array", IsNullable: true}, expected: "\n Z_PARAM_ARRAY_OR_NULL(items)", }, + { + name: "callable parameter", + param: phpParameter{Name: "callback", PhpType: "callable"}, + expected: "\n Z_PARAM_ZVAL(callback_callback)", + }, + { + name: "nullable callable parameter", + param: phpParameter{Name: "callback", PhpType: "callable", IsNullable: true}, + expected: "\n Z_PARAM_ZVAL_OR_NULL(callback_callback)", + }, { name: "unknown type", param: phpParameter{Name: "unknown", PhpType: "unknown"}, @@ -456,6 +512,16 @@ func TestParameterParser_GenerateSingleGoCallParam(t *testing.T) { param: phpParameter{Name: "items", PhpType: "array", 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: "unknown"}, @@ -534,6 +600,16 @@ func TestParameterParser_GenerateSingleParamDeclaration(t *testing.T) { param: phpParameter{Name: "items", PhpType: "array", HasDefault: false, IsNullable: true}, expected: []string{"zval *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;"}, + }, } for _, tt := range tests { diff --git a/internal/extgen/templates/extension.c.tpl b/internal/extgen/templates/extension.c.tpl index f6882d2c7d..1a06590290 100644 --- a/internal/extgen/templates/extension.c.tpl +++ b/internal/extgen/templates/extension.c.tpl @@ -96,6 +96,8 @@ PHP_METHOD({{.ClassName}}, {{.PhpName}}) { zend_bool {{$param.Name}}_is_null = 0;{{end}} {{- else if eq $param.PhpType "array"}} zval *{{$param.Name}} = NULL; + {{- else if eq $param.PhpType "callable"}} + zval *{{$param.Name}}_callback; {{- end}} {{- end}} @@ -104,7 +106,7 @@ PHP_METHOD({{.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_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({{.Name}}){{else if eq .PhpType "callable"}}Z_PARAM_ZVAL({{.Name}}_callback){{end}}{{end}} {{end -}} ZEND_PARSE_PARAMETERS_END(); {{else}} @@ -113,19 +115,19 @@ PHP_METHOD({{.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 "callable"}}{{.Name}}_callback{{else}}{{.Name}}{{end}}{{end}}{{end}}{{end}}); RETURN_STR(result); {{- 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}}{{end}}{{else}}{{if eq .PhpType "array"}}{{.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"}}ZEND_FCI_INITIALIZED({{.Name}}_fci) ? &{{.Name}}_fci : NULL{{end}}{{else}}{{if eq .PhpType "array"}}{{.Name}}{{else if eq .PhpType "callable"}}&{{.Name}}_fci{{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}}{{end}}{{else}}{{if eq .PhpType "array"}}{{.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"}}ZEND_FCI_INITIALIZED({{.Name}}_fci) ? &{{.Name}}_fci : NULL{{end}}{{else}}{{if eq .PhpType "array"}}{{.Name}}{{else if eq .PhpType "callable"}}&{{.Name}}_fci{{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}}{{end}}{{else}}{{if eq .PhpType "array"}}{{.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"}}ZEND_FCI_INITIALIZED({{.Name}}_fci) ? &{{.Name}}_fci : NULL{{end}}{{else}}{{if eq .PhpType "array"}}{{.Name}}{{else if eq .PhpType "callable"}}&{{.Name}}_fci{{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}}{{end}}{{else}}{{.Name}}{{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"}}ZEND_FCI_INITIALIZED({{.Name}}_fci) ? &{{.Name}}_fci : NULL{{end}}{{else}}{{if eq .PhpType "callable"}}&{{.Name}}_fci{{else}}{{.Name}}{{end}}{{end}}{{end}}{{end}}); if (result != NULL) { HashTable *ht = (HashTable*)result; RETURN_ARR(ht); @@ -134,7 +136,7 @@ PHP_METHOD({{.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}}{{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}}{{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"}}ZEND_FCI_INITIALIZED({{.Name}}_fci) ? &{{.Name}}_fci : 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"}}{{.Name}}{{else if eq .PhpType "callable"}}&{{.Name}}_fci{{end}}{{end}}{{end}}{{end}}); {{- end}} } {{end}}{{end}} diff --git a/internal/extgen/validator.go b/internal/extgen/validator.go index 1e40e12d62..9d97fc5620 100644 --- a/internal/extgen/validator.go +++ b/internal/extgen/validator.go @@ -47,7 +47,7 @@ func (v *Validator) validateParameter(param phpParameter) error { return fmt.Errorf("invalid parameter name: %s", param.Name) } - validTypes := []string{"string", "int", "float", "bool", "array", "object", "mixed"} + validTypes := []string{"string", "int", "float", "bool", "array", "object", "mixed", "callable"} if !v.isValidType(param.PhpType, validTypes) { return fmt.Errorf("invalid parameter type: %s", param.PhpType) } @@ -90,7 +90,7 @@ func (v *Validator) validateClassProperty(prop phpClassProperty) error { return fmt.Errorf("invalid property name: %s", prop.Name) } - validTypes := []string{"string", "int", "float", "bool", "array", "object", "mixed"} + validTypes := []string{"string", "int", "float", "bool", "array", "object", "mixed", "callable"} if !v.isValidType(prop.PhpType, validTypes) { return fmt.Errorf("invalid property type: %s", prop.PhpType) } @@ -109,16 +109,16 @@ func (v *Validator) isValidType(typeStr string, validTypes []string) bool { // validateScalarTypes checks if PHP signature contains only supported scalar types func (v *Validator) validateScalarTypes(fn phpFunction) error { - supportedTypes := []string{"string", "int", "float", "bool", "array"} + supportedTypes := []string{"string", "int", "float", "bool", "array", "callable"} for i, param := range fn.Params { if !v.isScalarType(param.PhpType, supportedTypes) { - return fmt.Errorf("parameter %d (%s) has unsupported type '%s'. Only scalar types (string, int, float, bool, array) and their nullable variants are supported", i+1, param.Name, param.PhpType) + return fmt.Errorf("parameter %d (%s) has unsupported type '%s'. Only scalar types (string, int, float, bool, array, callable) and their nullable variants are supported", i+1, param.Name, param.PhpType) } } if fn.ReturnType != "void" && !v.isScalarType(fn.ReturnType, supportedTypes) { - return fmt.Errorf("return type '%s' is not supported. Only scalar types (string, int, float, bool, array), void, and their nullable variants are supported", fn.ReturnType) + return fmt.Errorf("return type '%s' is not supported. Only scalar types (string, int, float, bool, array, callable), void, and their nullable variants are supported", fn.ReturnType) } return nil @@ -175,8 +175,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 { @@ -220,11 +222,13 @@ func (v *Validator) phpTypeToGoType(phpType string, isNullable bool) string { baseType = "bool" case "array": baseType = "*C.zval" + case "callable": + baseType = "*C.zval" default: baseType = "interface{}" } - if isNullable && phpType != "string" && phpType != "array" { + if isNullable && phpType != "string" && phpType != "array" && phpType != "callable" { return "*" + baseType } diff --git a/internal/extgen/validator_test.go b/internal/extgen/validator_test.go index 9650169cc0..5dc1d7161b 100644 --- a/internal/extgen/validator_test.go +++ b/internal/extgen/validator_test.go @@ -60,6 +60,29 @@ func TestValidateFunction(t *testing.T) { }, expectError: false, }, + { + name: "valid function with callable parameter", + function: phpFunction{ + Name: "callableFunction", + ReturnType: "array", + Params: []phpParameter{ + {Name: "data", PhpType: "array"}, + {Name: "callback", PhpType: "callable"}, + }, + }, + expectError: false, + }, + { + name: "valid function with nullable callable parameter", + function: phpFunction{ + Name: "nullableCallableFunction", + ReturnType: "string", + Params: []phpParameter{ + {Name: "callback", PhpType: "callable", IsNullable: true}, + }, + }, + expectError: false, + }, { name: "empty function name", function: phpFunction{ @@ -304,6 +327,23 @@ func TestValidateParameter(t *testing.T) { }, expectError: false, }, + { + name: "valid callable parameter", + param: phpParameter{ + Name: "callbackParam", + PhpType: "callable", + }, + 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 +524,28 @@ func TestValidateScalarTypes(t *testing.T) { }, expectError: false, }, + { + name: "valid callable parameter", + function: phpFunction{ + Name: "callableFunction", + ReturnType: "array", + Params: []phpParameter{ + {Name: "callbackParam", PhpType: "callable"}, + }, + }, + expectError: false, + }, + { + name: "valid nullable callable parameter", + function: phpFunction{ + Name: "nullableCallableFunction", + ReturnType: "string", + Params: []phpParameter{ + {Name: "callbackParam", PhpType: "callable", IsNullable: true}, + }, + }, + expectError: false, + }, { name: "invalid object parameter", function: phpFunction{ @@ -612,7 +674,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", @@ -714,6 +776,50 @@ func TestValidateGoFunctionSignature(t *testing.T) { }, GoFunction: `func mixedFunc(data *C.zval, 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: "callable"}, + }, + 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: "callable", 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: "array"}, + {Name: "callback", PhpType: "callable"}, + {Name: "options", PhpType: "int"}, + }, + GoFunction: `func mixedCallableFunc(data *C.zval, callback *C.zval, options int64) unsafe.Pointer { + return nil }`, }, expectError: false, @@ -751,6 +857,8 @@ func TestPhpTypeToGoType(t *testing.T) { {"bool", true, "*bool"}, {"array", false, "*C.zval"}, {"array", true, "*C.zval"}, + {"callable", false, "*C.zval"}, + {"callable", true, "*C.zval"}, {"unknown", false, "interface{}"}, } diff --git a/types.c b/types.c index 9b9b8e4c33..21c6651c5a 100644 --- a/types.c +++ b/types.c @@ -13,3 +13,30 @@ Bucket *get_ht_bucket_data(HashTable *ht, uint32_t index) { } return NULL; } + +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); +} + +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); +} + +void __zval_long__(zval *z, zend_long l) { ZVAL_LONG(z, l); } + +void __zval_double__(zval *z, double d) { ZVAL_DOUBLE(z, d); } + +void __zval_string__(zval *z, const char *s) { ZVAL_STRING(z, s); } + +void __zval_bool__(zval *z, bool b) { ZVAL_BOOL(z, b); } + +void __zval_null__(zval *z) { ZVAL_NULL(z); } diff --git a/types.go b/types.go index ee9d6ee1c6..62f3eadb42 100644 --- a/types.go +++ b/types.go @@ -2,14 +2,6 @@ package frankenphp /* #include "types.h" - -static inline void* __emalloc(size_t size) { - return emalloc(size); -} - -static inline void __zend_hash_init(HashTable *ht, uint32_t nSize, dtor_func_t pDestructor, bool persistent) { - zend_hash_init(ht, nSize, null, pDestructor, persistent); -} */ import "C" import "unsafe" @@ -189,6 +181,43 @@ func PHPArray(arr *Array) unsafe.Pointer { return unsafe.Pointer(zendArray) } +// 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 C.__efree__(unsafe.Pointer(paramStorage)) + + for i, param := range params { + setGoValueToZval((*C.zval)(unsafe.Pointer(uintptr(unsafe.Pointer(paramStorage))+uintptr(i)*unsafe.Sizeof(C.zval{}))), param) + } + } + + var retval C.zval + + result := C.__call_user_function__(callback, &retval, C.uint32_t(paramCount), paramStorage) + if result != C.SUCCESS { + return nil + } + + return convertZvalToGo(&retval) +} + // convertZvalToGo converts a PHP zval to a Go interface{} func convertZvalToGo(zval *C.zval) interface{} { t := C.zval_get_type(zval) @@ -224,47 +253,44 @@ func convertZvalToGo(zval *C.zval) interface{} { } } -// convertGoToZval converts a Go interface{} to a PHP zval -func convertGoToZval(value interface{}) *C.zval { - zval := (*C.zval)(C.__emalloc(C.size_t(unsafe.Sizeof(C.zval{})))) - u1 := (*C.uint8_t)(unsafe.Pointer(&zval.u1[0])) - v0 := unsafe.Pointer(&zval.value[0]) - +// setGoValueToZval sets a Go value to an existing zval using PHP macros +func setGoValueToZval(zval *C.zval, value interface{}) { switch v := value.(type) { case nil: - *u1 = C.IS_NULL + C.__zval_null__(zval) case bool: - if v { - *u1 = C.IS_TRUE - } else { - *u1 = C.IS_FALSE - } + C.__zval_bool__(zval, C._Bool(v)) case int: - *u1 = C.IS_LONG - *(*C.zend_long)(v0) = C.zend_long(v) + C.__zval_long__(zval, C.zend_long(v)) case int64: - *u1 = C.IS_LONG - *(*C.zend_long)(v0) = C.zend_long(v) + C.__zval_long__(zval, C.zend_long(v)) case float64: - *u1 = C.IS_DOUBLE - *(*C.double)(v0) = C.double(v) + C.__zval_double__(zval, C.double(v)) case string: - *u1 = C.IS_STRING - *(**C.zend_string)(v0) = (*C.zend_string)(PHPString(v, false)) + cstr := C.CString(v) + defer C.free(unsafe.Pointer(cstr)) + C.__zval_string__(zval, cstr) case *Array: + u1 := (*C.uint8_t)(unsafe.Pointer(&zval.u1[0])) + v0 := unsafe.Pointer(&zval.value[0]) *u1 = C.IS_ARRAY *(**C.zend_array)(v0) = (*C.zend_array)(PHPArray(v)) default: - *u1 = C.IS_NULL + C.__zval_null__(zval) } +} +// convertGoToZval converts a Go interface{} to a PHP zval (allocates memory) +func convertGoToZval(value interface{}) *C.zval { + zval := (*C.zval)(C.__emalloc__(C.size_t(unsafe.Sizeof(C.zval{})))) + setGoValueToZval(zval, value) return zval } // createNewArray creates a new zend_array with the specified size. func createNewArray(size uint32) *C.HashTable { - ht := C.__emalloc(C.size_t(unsafe.Sizeof(C.HashTable{}))) - C.__zend_hash_init((*C.struct__zend_array)(ht), C.uint32_t(size), nil, C._Bool(false)) + ht := C.__emalloc__(C.size_t(unsafe.Sizeof(C.HashTable{}))) + C.__zend_hash_init__((*C.struct__zend_array)(ht), C.uint32_t(size), nil, C._Bool(false)) return (*C.HashTable)(ht) } diff --git a/types.h b/types.h index d32c19c744..8596a49a2e 100644 --- a/types.h +++ b/types.h @@ -10,4 +10,19 @@ 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_long__(zval *z, zend_long l); +void __zval_double__(zval *z, double d); +void __zval_string__(zval *z, const char *s); +void __zval_bool__(zval *z, bool b); +void __zval_null__(zval *z); + #endif