From 1df81d6d4e639e7207e54e9da20b8c8d071ec81a Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Fri, 4 Jul 2025 11:10:09 +0200 Subject: [PATCH 1/2] feat(extgen): add support for arrays as parameters and return types --- docs/extensions.md | 78 +++++- docs/fr/extensions.md | 78 +++++- internal/extgen/cfile_test.go | 58 ++-- internal/extgen/classparser.go | 22 +- internal/extgen/classparser_test.go | 56 ++-- internal/extgen/constparser.go | 14 +- internal/extgen/constparser_test.go | 50 ++-- internal/extgen/docs_test.go | 50 ++-- internal/extgen/funcparser.go | 4 +- internal/extgen/funcparser_test.go | 26 +- internal/extgen/gofile.go | 111 ++------ internal/extgen/gofile_test.go | 196 +++++++++++++- internal/extgen/nodes.go | 29 +- internal/extgen/paramparser.go | 50 ++-- internal/extgen/paramparser_test.go | 194 ++++++++++---- internal/extgen/phpfunc.go | 42 +-- internal/extgen/phpfunc_test.go | 154 ++++++++--- internal/extgen/stub.go | 18 +- internal/extgen/stub_test.go | 28 +- internal/extgen/templates/extension.c.tpl | 24 +- internal/extgen/templates/extension.go.tpl | 20 +- internal/extgen/validator.go | 78 ++++-- internal/extgen/validator_test.go | 296 +++++++++++++-------- types.c | 22 ++ types.go | 271 ++++++++++++++++++- types.h | 17 ++ 26 files changed, 1421 insertions(+), 565 deletions(-) create mode 100644 types.c create mode 100644 types.h diff --git a/docs/extensions.md b/docs/extensions.md index 2a666f4f8..0efc34f6d 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -81,25 +81,77 @@ 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` | `slice`/`map` | ❌ | _Not yet implemented_ | _Not yet implemented_ | ❌ | -| `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() | ✅ | +| `object` | `struct` | ❌ | _Not yet implemented_ | _Not yet implemented_ | ❌ | > [!NOTE] > This table is not exhaustive yet and will be completed as the FrankenPHP types API gets more complete. > -> For class methods specifically, only primitive types are currently supported. Arrays and objects cannot be used as method parameters or return types yet. +> For class methods specifically, primitive types and arrays are currently supported. Objects cannot be used as method parameters or return types yet. If you refer to the code snippet of the previous section, you can see that helpers are used to convert the first parameter and the return value. The second and third parameter of our `repeat_this()` function don't need to be converted as memory representation of the underlying types are the same for both C and Go. +#### Working with Arrays + +FrankenPHP provides native support for PHP arrays through the `frankenphp.Array` type. This type represents both PHP indexed arrays (lists) and associative arrays (hashmaps) with ordered key-value pairs. + +**Creating and manipulating arrays in Go:** + +```go +//export_php:function process_data(array $input): array +func process_data(arr *C.zval) unsafe.Pointer { + // Convert PHP array to Go + goArray := frankenphp.GoArray(unsafe.Pointer(arr)) + + result := &frankenphp.Array{} + + result.SetInt(0, "first") + result.SetInt(1, "second") + result.Append("third") // Automatically assigns next integer key + + result.SetString("name", "John") + result.SetString("age", int64(30)) + + for i := uint32(0); i < goArray.Len(); i++ { + key, value := goArray.At(i) + if key.Type == frankenphp.PHPStringKey { + result.SetString("processed_"+key.Str, value) + } else { + result.SetInt(key.Int+100, value) + } + } + + // Convert back to PHP array + return frankenphp.PHPArray(result) +} +``` + +**Key features of `frankenphp.Array`:** + +* **Ordered key-value pairs** - Maintains insertion order like PHP arrays +* **Mixed key types** - Supports both integer and string keys in the same array +* **Type safety** - The `PHPKey` type ensures proper key handling +* **Automatic list detection** - When converting to PHP, automatically detects if array should be a packed list or hashmap +* **Objects are not supported** - Currently, only scalar types and arrays can be used as values. Providing an object will result in a `null` value in the PHP array. + +**Available methods:** + +* `SetInt(key int64, value interface{})` - Set value with integer key +* `SetString(key string, value interface{})` - Set value with string key +* `Append(value interface{})` - Add value with next available integer key +* `Len() uint32` - Get number of elements +* `At(index uint32) (PHPKey, interface{})` - Get key-value pair at index +* `frankenphp.PHPArray(arr *frankenphp.Array) unsafe.Pointer` - Convert to PHP array + ### 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: @@ -188,7 +240,7 @@ func (us *UserStruct) UpdateInfo(name *C.zend_string, age *int64, active *bool) * **PHP `null` becomes Go `nil`** - when PHP passes `null`, your Go function receives a `nil` pointer > [!WARNING] -> Currently, class methods have the following limitations. **Arrays and objects are not supported** as parameter types or return types. Only scalar types are supported: `string`, `int`, `float`, `bool` and `void` (for return type). **Nullable parameter types are fully supported** for all scalar types (`?string`, `?int`, `?float`, `?bool`). +> Currently, class methods have the following limitations. **Objects are not supported** as parameter types or return types. **Arrays are fully supported** for both parameters and return types. Supported types: `string`, `int`, `float`, `bool`, `array`, and `void` (for return type). **Nullable parameter types are fully supported** for all scalar types (`?string`, `?int`, `?float`, `?bool`). After generating the extension, you will be allowed to use the class and its methods in PHP. Note that you **cannot access properties directly**: diff --git a/docs/fr/extensions.md b/docs/fr/extensions.md index 5de4ecf27..a5de7e161 100644 --- a/docs/fr/extensions.md +++ b/docs/fr/extensions.md @@ -81,25 +81,77 @@ Alors que le premier point parle de lui-même, le second peut être plus diffici Bien que certains types de variables aient la même représentation mémoire entre C/PHP et Go, certains types nécessitent plus de logique pour être directement utilisés. C'est peut-être la partie la plus difficile quand il s'agit d'écrire des extensions car cela nécessite de comprendre les fonctionnements internes du moteur Zend et comment les variables sont stockées dans le moteur de PHP. Ce tableau résume ce que vous devez savoir : -| Type PHP | Type Go | Conversion directe | Assistant C vers Go | Assistant Go vers C | Support des Méthodes de Classe | -|--------------------|------------------|--------------------|-------------------------|-------------------------|--------------------------------| -| `int` | `int64` | ✅ | - | - | ✅ | -| `?int` | `*int64` | ✅ | - | - | ✅ | -| `float` | `float64` | ✅ | - | - | ✅ | -| `?float` | `*float64` | ✅ | - | - | ✅ | -| `bool` | `bool` | ✅ | - | - | ✅ | -| `?bool` | `*bool` | ✅ | - | - | ✅ | -| `string`/`?string` | `*C.zend_string` | ❌ | frankenphp.GoString() | frankenphp.PHPString() | ✅ | -| `array` | `slice`/`map` | ❌ | _Pas encore implémenté_ | _Pas encore implémenté_ | ❌ | -| `object` | `struct` | ❌ | _Pas encore implémenté_ | _Pas encore implémenté_ | ❌ | +| Type PHP | Type Go | Conversion directe | Assistant C vers Go | Assistant Go vers C | Support des Méthodes de Classe | +|--------------------|---------------------|--------------------|-------------------------|-------------------------|--------------------------------| +| `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` | ❌ | _Pas encore implémenté_ | _Pas encore implémenté_ | ❌ | > [!NOTE] > Ce tableau n'est pas encore exhaustif et sera complété au fur et à mesure que l'API de types FrankenPHP deviendra plus complète. > -> Pour les méthodes de classe spécifiquement, seuls les types primitifs sont actuellement supportés. Les tableaux et objets ne peuvent pas encore être utilisés comme paramètres de méthode ou types de retour. +> Pour les méthodes de classe spécifiquement, les types primitifs et les tableaux sont supportés. Les objets ne peuvent pas encore être utilisés comme paramètres de méthode ou types de retour. Si vous vous référez à l'extrait de code de la section précédente, vous pouvez voir que des assistants sont utilisés pour convertir le premier paramètre et la valeur de retour. Les deuxième et troisième paramètres de notre fonction `repeat_this()` n'ont pas besoin d'être convertis car la représentation mémoire des types sous-jacents est la même pour C et Go. +#### Travailler avec les Tableaux + +FrankenPHP fournit un support natif pour les tableaux PHP à travers le type `frankenphp.Array`. Ce type représente à la fois les tableaux indexés PHP (listes) et les tableaux associatifs (hashmaps) avec des paires clé-valeur ordonnées. + +**Créer et manipuler des tableaux en Go :** + +```go +//export_php:function process_data(array $input): array +func process_data(arr *C.zval) unsafe.Pointer { + // Convertir le tableau PHP vers Go + goArray := frankenphp.GoArray(unsafe.Pointer(arr)) + + result := &frankenphp.Array{} + + result.SetInt(0, "first") + result.SetInt(1, "second") + result.Append("third") // Assigne automatiquement la prochaine clé entière + + result.SetString("name", "John") + result.SetString("age", int64(30)) + + for i := uint32(0); i < goArray.Len(); i++ { + key, value := goArray.At(i) + if key.Type == frankenphp.PHPStringKey { + result.SetString("processed_"+key.Str, value) + } else { + result.SetInt(key.Int+100, value) + } + } + + // Reconvertir vers un tableau PHP + return frankenphp.PHPArray(result) +} +``` + +**Fonctionnalités clés de `frankenphp.Array` :** + +* **Paires clé-valeur ordonnées** - Maintient l'ordre d'insertion comme les tableaux PHP +* **Types de clés mixtes** - Supporte les clés entières et chaînes dans le même tableau +* **Sécurité de type** - Le type `PHPKey` assure une gestion appropriée des clés +* **Détection automatique de liste** - Lors de la conversion vers PHP, détecte automatiquement si le tableau doit être une liste compacte ou un hashmap +* **Les objets ne sont pas supportés** - Actuellement, seuls les types scalaires et les tableaux sont supportés. Passer un objet en tant qu'élément du tableau résultera d'une valeur `null` dans le tableau PHP. + +**Méthodes disponibles :** + +* `SetInt(key int64, value interface{})` - Définir une valeur avec une clé entière +* `SetString(key string, value interface{})` - Définir une valeur avec une clé chaîne +* `Append(value interface{})` - Ajouter une valeur avec la prochaine clé entière disponible +* `Len() uint32` - Obtenir le nombre d'éléments +* `At(index uint32) (PHPKey, interface{})` - Obtenir la paire clé-valeur à l'index +* `frankenphp.PHPArray(arr *frankenphp.Array) unsafe.Pointer` - Convertir vers un tableau PHP + ### Déclarer une Classe PHP Native 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 : @@ -188,7 +240,7 @@ func (us *UserStruct) UpdateInfo(name *C.zend_string, age *int64, active *bool) * **PHP `null` devient Go `nil`** - quand PHP passe `null`, votre fonction Go reçoit un pointeur `nil` > [!WARNING] -> Actuellement, les méthodes de classe ont les limitations suivantes. **Les tableaux et objets ne sont pas supportés** comme types de paramètres ou types de retour. Seuls les types scalaires sont supportés : `string`, `int`, `float`, `bool` et `void` (pour le type de retour). **Les types de paramètres nullables sont entièrement supportés** pour tous les types scalaires (`?string`, `?int`, `?float`, `?bool`). +> Actuellement, les méthodes de classe ont les limitations suivantes. **Les objets ne sont pas supportés** comme types de paramètres ou types de retour. **Les tableaux sont entièrement supportés** pour les paramètres et types de retour. Types supportés : `string`, `int`, `float`, `bool`, `array`, et `void` (pour le type de retour). **Les types de paramètres nullables sont entièrement supportés** pour tous les types scalaires (`?string`, `?int`, `?float`, `?bool`). Après avoir généré l'extension, vous serez autorisé à utiliser la classe et ses méthodes en PHP. Notez que vous **ne pouvez pas accéder aux propriétés directement** : diff --git a/internal/extgen/cfile_test.go b/internal/extgen/cfile_test.go index cc9f4332a..63b696ff9 100644 --- a/internal/extgen/cfile_test.go +++ b/internal/extgen/cfile_test.go @@ -18,18 +18,18 @@ func TestCFileGenerator_Generate(t *testing.T) { Functions: []phpFunction{ { Name: "simpleFunction", - ReturnType: "string", + ReturnType: phpString, Params: []phpParameter{ - {Name: "input", PhpType: "string"}, + {Name: "input", PhpType: phpString}, }, }, { Name: "complexFunction", - ReturnType: "array", + ReturnType: phpArray, Params: []phpParameter{ - {Name: "data", PhpType: "string"}, - {Name: "count", PhpType: "int", IsNullable: true}, - {Name: "options", PhpType: "array", HasDefault: true, DefaultValue: "[]"}, + {Name: "data", PhpType: phpString}, + {Name: "count", PhpType: phpInt, IsNullable: true}, + {Name: "options", PhpType: phpArray, HasDefault: true, DefaultValue: "[]"}, }, }, }, @@ -38,8 +38,8 @@ func TestCFileGenerator_Generate(t *testing.T) { Name: "TestClass", GoStruct: "TestStruct", Properties: []phpClassProperty{ - {Name: "id", PhpType: "int"}, - {Name: "name", PhpType: "string"}, + {Name: "id", PhpType: phpInt}, + {Name: "name", PhpType: phpString}, }, }, }, @@ -84,7 +84,7 @@ func TestCFileGenerator_BuildContent(t *testing.T) { name: "extension with functions only", baseName: "func_only", functions: []phpFunction{ - {Name: "testFunc", ReturnType: "string"}, + {Name: "testFunc", ReturnType: phpString}, }, contains: []string{ "PHP_FUNCTION(testFunc)", @@ -110,7 +110,7 @@ func TestCFileGenerator_BuildContent(t *testing.T) { name: "extension with functions and classes", baseName: "full", functions: []phpFunction{ - {Name: "doSomething", ReturnType: "void"}, + {Name: "doSomething", ReturnType: phpVoid}, }, classes: []phpClass{ {Name: "FullClass", GoStruct: "FullStruct"}, @@ -209,20 +209,20 @@ func TestCFileIntegrationWithGenerators(t *testing.T) { functions := []phpFunction{ { Name: "processData", - ReturnType: "array", + ReturnType: phpArray, IsReturnNullable: true, Params: []phpParameter{ - {Name: "input", PhpType: "string"}, - {Name: "options", PhpType: "array", HasDefault: true, DefaultValue: "[]"}, - {Name: "callback", PhpType: "object", IsNullable: true}, + {Name: "input", PhpType: phpString}, + {Name: "options", PhpType: phpArray, HasDefault: true, DefaultValue: "[]"}, + {Name: "callback", PhpType: phpObject, IsNullable: true}, }, }, { Name: "validateInput", - ReturnType: "bool", + ReturnType: phpBool, Params: []phpParameter{ - {Name: "data", PhpType: "string", IsNullable: true}, - {Name: "strict", PhpType: "bool", HasDefault: true, DefaultValue: "false"}, + {Name: "data", PhpType: phpString, IsNullable: true}, + {Name: "strict", PhpType: phpBool, HasDefault: true, DefaultValue: "false"}, }, }, } @@ -232,18 +232,18 @@ func TestCFileIntegrationWithGenerators(t *testing.T) { Name: "DataProcessor", GoStruct: "DataProcessorStruct", Properties: []phpClassProperty{ - {Name: "mode", PhpType: "string"}, - {Name: "timeout", PhpType: "int", IsNullable: true}, - {Name: "options", PhpType: "array"}, + {Name: "mode", PhpType: phpString}, + {Name: "timeout", PhpType: phpInt, IsNullable: true}, + {Name: "options", PhpType: phpArray}, }, }, { Name: "Result", GoStruct: "ResultStruct", Properties: []phpClassProperty{ - {Name: "success", PhpType: "bool"}, - {Name: "data", PhpType: "mixed", IsNullable: true}, - {Name: "errors", PhpType: "array"}, + {Name: "success", PhpType: phpBool}, + {Name: "data", PhpType: phpMixed, IsNullable: true}, + {Name: "errors", PhpType: phpArray}, }, }, } @@ -281,7 +281,7 @@ func TestCFileErrorHandling(t *testing.T) { BaseName: "test", BuildDir: "/invalid/readonly/path", Functions: []phpFunction{ - {Name: "test", ReturnType: "void"}, + {Name: "test", ReturnType: phpVoid}, }, } @@ -305,7 +305,7 @@ func TestCFileSpecialCharacters(t *testing.T) { generator := &Generator{ BaseName: tt.baseName, Functions: []phpFunction{ - {Name: "test", ReturnType: "void"}, + {Name: "test", ReturnType: phpVoid}, }, } @@ -367,9 +367,9 @@ func TestCFileContentValidation(t *testing.T) { Functions: []phpFunction{ { Name: "testFunction", - ReturnType: "string", + ReturnType: phpString, Params: []phpParameter{ - {Name: "param", PhpType: "string"}, + {Name: "param", PhpType: phpString}, }, }, }, @@ -415,12 +415,12 @@ func TestCFileConstants(t *testing.T) { { Name: "GLOBAL_INT", Value: "42", - PhpType: "int", + PhpType: phpInt, }, { Name: "GLOBAL_STRING", Value: `"test"`, - PhpType: "string", + PhpType: phpString, }, }, contains: []string{ diff --git a/internal/extgen/classparser.go b/internal/extgen/classparser.go index 6ac39c755..b3bb60b39 100644 --- a/internal/extgen/classparser.go +++ b/internal/extgen/classparser.go @@ -181,15 +181,15 @@ func (cp *classParser) typeToString(expr ast.Expr) string { } } -func (cp *classParser) goTypeToPHPType(goType string) string { +func (cp *classParser) goTypeToPHPType(goType string) phpType { goType = strings.TrimPrefix(goType, "*") - typeMap := map[string]string{ - "string": "string", - "int": "int", "int64": "int", "int32": "int", "int16": "int", "int8": "int", - "uint": "int", "uint64": "int", "uint32": "int", "uint16": "int", "uint8": "int", - "float64": "float", "float32": "float", - "bool": "bool", + typeMap := map[string]phpType{ + "string": phpString, + "int": phpInt, "int64": phpInt, "int32": phpInt, "int16": phpInt, "int8": phpInt, + "uint": phpInt, "uint64": phpInt, "uint32": phpInt, "uint16": phpInt, "uint8": phpInt, + "float64": phpFloat, "float32": phpFloat, + "bool": phpBool, } if phpType, exists := typeMap[goType]; exists { @@ -197,10 +197,10 @@ func (cp *classParser) goTypeToPHPType(goType string) string { } if strings.HasPrefix(goType, "[]") || strings.HasPrefix(goType, "map[") { - return "array" + return phpArray } - return "mixed" + return phpMixed } func (cp *classParser) parseMethods(filename string) (methods []phpClassMethod, err error) { @@ -323,7 +323,7 @@ func (cp *classParser) parseMethodSignature(className, signature string) (*phpCl ClassName: className, Signature: signature, Params: params, - ReturnType: returnType, + ReturnType: phpType(returnType), isReturnNullable: isReturnNullable, }, nil } @@ -347,7 +347,7 @@ func (cp *classParser) parseMethodParameter(paramStr string) (phpParameter, erro typeStr := strings.TrimSpace(matches[1]) param.Name = strings.TrimSpace(matches[2]) param.IsNullable = strings.HasPrefix(typeStr, "?") - param.PhpType = strings.TrimPrefix(typeStr, "?") + param.PhpType = phpType(strings.TrimPrefix(typeStr, "?")) return param, nil } diff --git a/internal/extgen/classparser_test.go b/internal/extgen/classparser_test.go index 5acaff083..a05e5cbc0 100644 --- a/internal/extgen/classparser_test.go +++ b/internal/extgen/classparser_test.go @@ -157,29 +157,29 @@ func GetUserInfo(u UserStruct, prefix *C.zend_string) unsafe.Pointer { getName := class.Methods[0] assert.Equal(t, "getName", getName.Name, "Expected method name 'getName'") - assert.Equal(t, "string", getName.ReturnType, "Expected return type 'string'") + assert.Equal(t, phpString, getName.ReturnType, "Expected return type 'string'") assert.Empty(t, getName.Params, "Expected 0 params") assert.Equal(t, "User", getName.ClassName, "Expected class name 'User'") setAge := class.Methods[1] assert.Equal(t, "setAge", setAge.Name, "Expected method name 'setAge'") - assert.Equal(t, "void", setAge.ReturnType, "Expected return type 'void'") + assert.Equal(t, phpVoid, setAge.ReturnType, "Expected return type 'void'") require.Len(t, setAge.Params, 1, "Expected 1 param") param := setAge.Params[0] assert.Equal(t, "age", param.Name, "Expected param name 'age'") - assert.Equal(t, "int", param.PhpType, "Expected param type 'int'") + assert.Equal(t, phpInt, param.PhpType, "Expected param type 'int'") assert.False(t, param.IsNullable, "Expected param to not be nullable") assert.False(t, param.HasDefault, "Expected param to not have default value") getInfo := class.Methods[2] assert.Equal(t, "getInfo", getInfo.Name, "Expected method name 'getInfo'") - assert.Equal(t, "string", getInfo.ReturnType, "Expected return type 'string'") + assert.Equal(t, phpString, getInfo.ReturnType, "Expected return type 'string'") require.Len(t, getInfo.Params, 1, "Expected 1 param") param = getInfo.Params[0] assert.Equal(t, "prefix", param.Name, "Expected param name 'prefix'") - assert.Equal(t, "string", param.PhpType, "Expected param type 'string'") + assert.Equal(t, phpString, param.PhpType, "Expected param type 'string'") assert.True(t, param.HasDefault, "Expected param to have default value") assert.Equal(t, "User", param.DefaultValue, "Expected default value 'User'") } @@ -196,7 +196,7 @@ func TestMethodParameterParsing(t *testing.T) { paramStr: "int $age", expectedParam: phpParameter{ Name: "age", - PhpType: "int", + PhpType: phpInt, IsNullable: false, HasDefault: false, }, @@ -207,7 +207,7 @@ func TestMethodParameterParsing(t *testing.T) { paramStr: "?string $name", expectedParam: phpParameter{ Name: "name", - PhpType: "string", + PhpType: phpString, IsNullable: true, HasDefault: false, }, @@ -218,7 +218,7 @@ func TestMethodParameterParsing(t *testing.T) { paramStr: `string $prefix = "default"`, expectedParam: phpParameter{ Name: "prefix", - PhpType: "string", + PhpType: phpString, IsNullable: false, HasDefault: true, DefaultValue: "default", @@ -230,7 +230,7 @@ func TestMethodParameterParsing(t *testing.T) { paramStr: "?int $count = null", expectedParam: phpParameter{ Name: "count", - PhpType: "int", + PhpType: phpInt, IsNullable: true, HasDefault: true, DefaultValue: "null", @@ -268,22 +268,22 @@ func TestMethodParameterParsing(t *testing.T) { func TestGoTypeToPHPType(t *testing.T) { tests := []struct { goType string - expected string + expected phpType }{ - {"string", "string"}, - {"*string", "string"}, - {"int", "int"}, - {"int64", "int"}, - {"*int", "int"}, - {"float64", "float"}, - {"*float32", "float"}, - {"bool", "bool"}, - {"*bool", "bool"}, - {"[]string", "array"}, - {"map[string]int", "array"}, - {"*[]int", "array"}, - {"interface{}", "mixed"}, - {"CustomType", "mixed"}, + {"string", phpString}, + {"*string", phpString}, + {"int", phpInt}, + {"int64", phpInt}, + {"*int", phpInt}, + {"float64", phpFloat}, + {"*float32", phpFloat}, + {"bool", phpBool}, + {"*bool", phpBool}, + {"[]string", phpArray}, + {"map[string]int", phpArray}, + {"*[]int", phpArray}, + {"interface{}", phpMixed}, + {"CustomType", phpMixed}, } parser := classParser{} @@ -299,7 +299,7 @@ func TestTypeToString(t *testing.T) { tests := []struct { name string input string - expected []string + expected []phpType }{ { name: "basic types", @@ -312,7 +312,7 @@ type TestStruct struct { FloatField float64 BoolField bool }`, - expected: []string{"string", "int", "float", "bool"}, + expected: []phpType{phpString, phpInt, phpFloat, phpBool}, }, { name: "pointer types", @@ -325,7 +325,7 @@ type NullableStruct struct { NullableFloat *float64 NullableBool *bool }`, - expected: []string{"string", "int", "float", "bool"}, + expected: []phpType{phpString, phpInt, phpFloat, phpBool}, }, { name: "collection types", @@ -337,7 +337,7 @@ type CollectionStruct struct { IntMap map[string]int MixedSlice []interface{} }`, - expected: []string{"array", "array", "array"}, + expected: []phpType{phpArray, phpArray, phpArray}, }, } diff --git a/internal/extgen/constparser.go b/internal/extgen/constparser.go index b7bb3cdb5..4fe267de7 100644 --- a/internal/extgen/constparser.go +++ b/internal/extgen/constparser.go @@ -86,7 +86,7 @@ func (cp *ConstantParser) parse(filename string) (constants []phpConstant, err e if constant.IsIota { // affect a default value because user didn't give one constant.Value = fmt.Sprintf("%d", currentConstantValue) - constant.PhpType = "int" + constant.PhpType = phpInt currentConstantValue++ } @@ -108,26 +108,26 @@ func (cp *ConstantParser) parse(filename string) (constants []phpConstant, err e } // determineConstantType analyzes the value and determines its type -func determineConstantType(value string) string { +func determineConstantType(value string) phpType { value = strings.TrimSpace(value) if (strings.HasPrefix(value, "\"") && strings.HasSuffix(value, "\"")) || (strings.HasPrefix(value, "`") && strings.HasSuffix(value, "`")) { - return "string" + return phpString } if value == "true" || value == "false" { - return "bool" + return phpBool } // check for integer literals, including hex, octal, binary if _, err := strconv.ParseInt(value, 0, 64); err == nil { - return "int" + return phpInt } if _, err := strconv.ParseFloat(value, 64); err == nil { - return "float" + return phpFloat } - return "int" + return phpInt } diff --git a/internal/extgen/constparser_test.go b/internal/extgen/constparser_test.go index 34aa6d811..17ce1f5dc 100644 --- a/internal/extgen/constparser_test.go +++ b/internal/extgen/constparser_test.go @@ -144,7 +144,7 @@ const FalseConstant = false`, c := constants[0] assert.Equal(t, "MyConstant", c.Name, "Expected constant name 'MyConstant'") assert.Equal(t, `"test_value"`, c.Value, `Expected constant value '"test_value"'`) - assert.Equal(t, "string", c.PhpType, "Expected constant type 'string'") + assert.Equal(t, phpString, c.PhpType, "Expected constant type 'string'") assert.False(t, c.IsIota, "Expected isIota to be false for string constant") } @@ -158,7 +158,7 @@ const FalseConstant = false`, if tt.name == "multiple constants" && len(constants) == 3 { expectedNames := []string{"FirstConstant", "SecondConstant", "ThirdConstant"} expectedValues := []string{`"first"`, "42", "true"} - expectedTypes := []string{"string", "int", "bool"} + expectedTypes := []phpType{phpString, phpInt, phpBool} for i, c := range constants { assert.Equal(t, expectedNames[i], c.Name, "Expected constant name '%s'", expectedNames[i]) @@ -248,22 +248,22 @@ func TestConstantParserTypeDetection(t *testing.T) { tests := []struct { name string value string - expectedType string + expectedType phpType }{ - {"string with double quotes", "\"hello world\"", "string"}, - {"string with backticks", "`hello world`", "string"}, - {"boolean true", "true", "bool"}, - {"boolean false", "false", "bool"}, - {"integer", "42", "int"}, - {"negative integer", "-42", "int"}, - {"hex integer", "0xFF", "int"}, - {"octal integer", "0755", "int"}, - {"go octal integer", "0o755", "int"}, - {"binary integer", "0b1010", "int"}, - {"float", "3.14", "float"}, - {"negative float", "-3.14", "float"}, - {"scientific notation", "1e10", "float"}, - {"unknown type", "someFunction()", "int"}, + {"string with double quotes", "\"hello world\"", phpString}, + {"string with backticks", "`hello world`", phpString}, + {"boolean true", "true", phpBool}, + {"boolean false", "false", phpBool}, + {"integer", "42", phpInt}, + {"negative integer", "-42", phpInt}, + {"hex integer", "0xFF", phpInt}, + {"octal integer", "0755", phpInt}, + {"go octal integer", "0o755", phpInt}, + {"binary integer", "0b1010", phpInt}, + {"float", "3.14", phpFloat}, + {"negative float", "-3.14", phpFloat}, + {"scientific notation", "1e10", phpFloat}, + {"unknown type", "someFunction()", phpInt}, } for _, tt := range tests { @@ -354,7 +354,7 @@ const INVALID = "missing class name"`, assert.Equal(t, "STATUS_ACTIVE", c.Name, "Expected constant name 'STATUS_ACTIVE'") assert.Equal(t, "MyClass", c.ClassName, "Expected class name 'MyClass'") assert.Equal(t, "1", c.Value, "Expected constant value '1'") - assert.Equal(t, "int", c.PhpType, "Expected constant type 'int'") + assert.Equal(t, phpInt, c.PhpType, "Expected constant type 'int'") } if tt.name == "multiple class constants" && len(constants) == 3 { @@ -489,7 +489,7 @@ func TestPHPConstantCValue(t *testing.T) { constant: phpConstant{ Name: "OctalConst", Value: "0o35", - PhpType: "int", + PhpType: phpInt, }, expected: "29", // 0o35 = 29 in decimal }, @@ -498,7 +498,7 @@ func TestPHPConstantCValue(t *testing.T) { constant: phpConstant{ Name: "OctalPerm", Value: "0o755", - PhpType: "int", + PhpType: phpInt, }, expected: "493", // 0o755 = 493 in decimal }, @@ -507,7 +507,7 @@ func TestPHPConstantCValue(t *testing.T) { constant: phpConstant{ Name: "RegularInt", Value: "42", - PhpType: "int", + PhpType: phpInt, }, expected: "42", }, @@ -516,7 +516,7 @@ func TestPHPConstantCValue(t *testing.T) { constant: phpConstant{ Name: "HexInt", Value: "0xFF", - PhpType: "int", + PhpType: phpInt, }, expected: "0xFF", // hex should remain unchanged }, @@ -525,7 +525,7 @@ func TestPHPConstantCValue(t *testing.T) { constant: phpConstant{ Name: "StringConst", Value: "\"hello\"", - PhpType: "string", + PhpType: phpString, }, expected: "\"hello\"", // strings should remain unchanged }, @@ -534,7 +534,7 @@ func TestPHPConstantCValue(t *testing.T) { constant: phpConstant{ Name: "BoolConst", Value: "true", - PhpType: "bool", + PhpType: phpBool, }, expected: "true", // booleans should remain unchanged }, @@ -543,7 +543,7 @@ func TestPHPConstantCValue(t *testing.T) { constant: phpConstant{ Name: "FloatConst", Value: "3.14", - PhpType: "float", + PhpType: phpFloat, }, expected: "3.14", // floats should remain unchanged }, diff --git a/internal/extgen/docs_test.go b/internal/extgen/docs_test.go index 78a8abfd4..07ac0c22b 100644 --- a/internal/extgen/docs_test.go +++ b/internal/extgen/docs_test.go @@ -23,9 +23,9 @@ func TestDocumentationGenerator_Generate(t *testing.T) { Functions: []phpFunction{ { Name: "greet", - ReturnType: "string", + ReturnType: phpString, Params: []phpParameter{ - {Name: "name", PhpType: "string"}, + {Name: "name", PhpType: phpString}, }, Signature: "greet(string $name): string", }, @@ -44,8 +44,8 @@ func TestDocumentationGenerator_Generate(t *testing.T) { { Name: "TestClass", Properties: []phpClassProperty{ - {Name: "name", PhpType: "string"}, - {Name: "count", PhpType: "int", IsNullable: true}, + {Name: "name", PhpType: phpString}, + {Name: "count", PhpType: phpInt, IsNullable: true}, }, }, }, @@ -60,11 +60,11 @@ func TestDocumentationGenerator_Generate(t *testing.T) { Functions: []phpFunction{ { Name: "calculate", - ReturnType: "int", + ReturnType: phpInt, IsReturnNullable: true, Params: []phpParameter{ - {Name: "base", PhpType: "int"}, - {Name: "multiplier", PhpType: "int", HasDefault: true, DefaultValue: "2", IsNullable: true}, + {Name: "base", PhpType: phpInt}, + {Name: "multiplier", PhpType: phpInt, HasDefault: true, DefaultValue: "2", IsNullable: true}, }, Signature: "calculate(int $base, ?int $multiplier = 2): ?int", }, @@ -73,7 +73,7 @@ func TestDocumentationGenerator_Generate(t *testing.T) { { Name: "Calculator", Properties: []phpClassProperty{ - {Name: "precision", PhpType: "int"}, + {Name: "precision", PhpType: phpInt}, }, }, }, @@ -155,11 +155,11 @@ func TestDocumentationGenerator_GenerateMarkdown(t *testing.T) { Functions: []phpFunction{ { Name: "processData", - ReturnType: "array", + ReturnType: phpArray, Params: []phpParameter{ - {Name: "data", PhpType: "string"}, - {Name: "options", PhpType: "array", IsNullable: true}, - {Name: "count", PhpType: "int", HasDefault: true, DefaultValue: "10"}, + {Name: "data", PhpType: phpString}, + {Name: "options", PhpType: phpArray, IsNullable: true}, + {Name: "count", PhpType: phpInt, HasDefault: true, DefaultValue: "10"}, }, Signature: "processData(string $data, ?array $options, int $count = 10): array", }, @@ -184,7 +184,7 @@ func TestDocumentationGenerator_GenerateMarkdown(t *testing.T) { Functions: []phpFunction{ { Name: "maybeGetValue", - ReturnType: "string", + ReturnType: phpString, IsReturnNullable: true, Params: []phpParameter{}, Signature: "maybeGetValue(): ?string", @@ -205,9 +205,9 @@ func TestDocumentationGenerator_GenerateMarkdown(t *testing.T) { { Name: "DataProcessor", Properties: []phpClassProperty{ - {Name: "name", PhpType: "string"}, - {Name: "config", PhpType: "array", IsNullable: true}, - {Name: "enabled", PhpType: "bool"}, + {Name: "name", PhpType: phpString}, + {Name: "config", PhpType: phpArray, IsNullable: true}, + {Name: "enabled", PhpType: phpBool}, }, }, }, @@ -244,7 +244,7 @@ func TestDocumentationGenerator_GenerateMarkdown(t *testing.T) { Functions: []phpFunction{ { Name: "getCurrentTime", - ReturnType: "int", + ReturnType: phpInt, Params: []phpParameter{}, Signature: "getCurrentTime(): int", }, @@ -324,7 +324,7 @@ func TestDocumentationGenerator_TemplateError(t *testing.T) { Functions: []phpFunction{ { Name: "test", - ReturnType: "string", + ReturnType: phpString, Signature: "test(): string", }, }, @@ -346,19 +346,19 @@ func BenchmarkDocumentationGenerator_GenerateMarkdown(b *testing.B) { Functions: []phpFunction{ { Name: "function1", - ReturnType: "string", + ReturnType: phpString, Params: []phpParameter{ - {Name: "param1", PhpType: "string"}, - {Name: "param2", PhpType: "int", HasDefault: true, DefaultValue: "0"}, + {Name: "param1", PhpType: phpString}, + {Name: "param2", PhpType: phpInt, HasDefault: true, DefaultValue: "0"}, }, Signature: "function1(string $param1, int $param2 = 0): string", }, { Name: "function2", - ReturnType: "array", + ReturnType: phpArray, IsReturnNullable: true, Params: []phpParameter{ - {Name: "data", PhpType: "array", IsNullable: true}, + {Name: "data", PhpType: phpArray, IsNullable: true}, }, Signature: "function2(?array $data): ?array", }, @@ -367,8 +367,8 @@ func BenchmarkDocumentationGenerator_GenerateMarkdown(b *testing.B) { { Name: "TestClass", Properties: []phpClassProperty{ - {Name: "prop1", PhpType: "string"}, - {Name: "prop2", PhpType: "int", IsNullable: true}, + {Name: "prop1", PhpType: phpString}, + {Name: "prop2", PhpType: phpInt, IsNullable: true}, }, }, }, diff --git a/internal/extgen/funcparser.go b/internal/extgen/funcparser.go index eb9275d6e..037e27218 100644 --- a/internal/extgen/funcparser.go +++ b/internal/extgen/funcparser.go @@ -150,7 +150,7 @@ func (fp *FuncParser) parseSignature(signature string) (*phpFunction, error) { Name: name, Signature: signature, Params: params, - ReturnType: returnType, + ReturnType: phpType(returnType), IsReturnNullable: isReturnNullable, }, nil } @@ -174,7 +174,7 @@ func (fp *FuncParser) parseParameter(paramStr string) (phpParameter, error) { typeStr := strings.TrimSpace(matches[1]) param.Name = strings.TrimSpace(matches[2]) param.IsNullable = strings.HasPrefix(typeStr, "?") - param.PhpType = strings.TrimPrefix(typeStr, "?") + param.PhpType = phpType(strings.TrimPrefix(typeStr, "?")) return param, nil } diff --git a/internal/extgen/funcparser_test.go b/internal/extgen/funcparser_test.go index 3af5088f3..186cdb088 100644 --- a/internal/extgen/funcparser_test.go +++ b/internal/extgen/funcparser_test.go @@ -109,7 +109,7 @@ func someOtherGoName(num int64) int64 { if tt.name == "single function" && len(functions) > 0 { fn := functions[0] assert.Equal(t, "testFunc", fn.Name, "Expected function name 'testFunc'") - assert.Equal(t, "string", fn.ReturnType, "Expected return type 'string'") + assert.Equal(t, phpString, fn.ReturnType, "Expected return type 'string'") assert.Len(t, fn.Params, 1, "Expected 1 parameter") if len(fn.Params) > 0 { assert.Equal(t, "name", fn.Params[0].Name, "Expected parameter name 'name'") @@ -133,7 +133,7 @@ func TestSignatureParsing(t *testing.T) { expectError bool funcName string paramCount int - returnType string + returnType phpType nullable bool }{ { @@ -141,7 +141,7 @@ func TestSignatureParsing(t *testing.T) { signature: "test(name string): string", funcName: "test", paramCount: 1, - returnType: "string", + returnType: phpString, nullable: false, }, { @@ -149,7 +149,7 @@ func TestSignatureParsing(t *testing.T) { signature: "test(id int): ?string", funcName: "test", paramCount: 1, - returnType: "string", + returnType: phpString, nullable: true, }, { @@ -157,7 +157,7 @@ func TestSignatureParsing(t *testing.T) { signature: "calculate(a int, b float, name string): float", funcName: "calculate", paramCount: 3, - returnType: "float", + returnType: phpFloat, nullable: false, }, { @@ -165,7 +165,7 @@ func TestSignatureParsing(t *testing.T) { signature: "getValue(): int", funcName: "getValue", paramCount: 0, - returnType: "int", + returnType: phpInt, nullable: false, }, { @@ -173,7 +173,7 @@ func TestSignatureParsing(t *testing.T) { signature: "process(?string data, ?int count): bool", funcName: "process", paramCount: 2, - returnType: "bool", + returnType: phpBool, nullable: false, }, { @@ -219,7 +219,7 @@ func TestParameterParsing(t *testing.T) { name string paramStr string expectedName string - expectedType string + expectedType phpType expectedNullable bool expectedDefault string hasDefault bool @@ -229,20 +229,20 @@ func TestParameterParsing(t *testing.T) { name: "simple string param", paramStr: "string name", expectedName: "name", - expectedType: "string", + expectedType: phpString, }, { name: "nullable int param", paramStr: "?int count", expectedName: "count", - expectedType: "int", + expectedType: phpInt, expectedNullable: true, }, { name: "param with default", paramStr: "string message = 'hello'", expectedName: "message", - expectedType: "string", + expectedType: phpString, expectedDefault: "hello", hasDefault: true, }, @@ -250,7 +250,7 @@ func TestParameterParsing(t *testing.T) { name: "int with default", paramStr: "int limit = 10", expectedName: "limit", - expectedType: "int", + expectedType: phpInt, expectedDefault: "10", hasDefault: true, }, @@ -258,7 +258,7 @@ func TestParameterParsing(t *testing.T) { name: "nullable with default", paramStr: "?string data = null", expectedName: "data", - expectedType: "string", + expectedType: phpString, expectedNullable: true, expectedDefault: "null", hasDefault: true, diff --git a/internal/extgen/gofile.go b/internal/extgen/gofile.go index b436315e4..d90883bff 100644 --- a/internal/extgen/gofile.go +++ b/internal/extgen/gofile.go @@ -5,7 +5,6 @@ import ( _ "embed" "fmt" "path/filepath" - "strings" "text/template" "github.com/Masterminds/sprig/v3" @@ -54,11 +53,6 @@ func (gg *GoFileGenerator) buildContent() (string, error) { classes := make([]phpClass, len(gg.generator.Classes)) copy(classes, gg.generator.Classes) - for i, class := range classes { - for j, method := range class.Methods { - classes[i].Methods[j].Wrapper = gg.generateMethodWrapper(method, class) - } - } templateContent, err := gg.getTemplateContent(goTemplateData{ PackageName: SanitizePackageName(gg.generator.BaseName), @@ -78,7 +72,16 @@ func (gg *GoFileGenerator) buildContent() (string, error) { } func (gg *GoFileGenerator) getTemplateContent(data goTemplateData) (string, error) { - tmpl := template.Must(template.New("gofile").Funcs(sprig.FuncMap()).Parse(goFileContent)) + funcMap := sprig.FuncMap() + funcMap["phpTypeToGoType"] = gg.phpTypeToGoType + funcMap["isStringOrArray"] = func(t phpType) bool { + return t == phpString || t == phpArray + } + funcMap["isVoid"] = func(t phpType) bool { + return t == phpVoid + } + + tmpl := template.Must(template.New("gofile").Funcs(funcMap).Parse(goFileContent)) var buf bytes.Buffer if err := tmpl.Execute(&buf, data); err != nil { @@ -88,72 +91,6 @@ func (gg *GoFileGenerator) getTemplateContent(data goTemplateData) (string, erro return buf.String(), nil } -func (gg *GoFileGenerator) generateMethodWrapper(method phpClassMethod, class phpClass) string { - var builder strings.Builder - - builder.WriteString(fmt.Sprintf("func %s_wrapper(handle C.uintptr_t", method.Name)) - - for _, param := range method.Params { - if param.PhpType == "string" { - builder.WriteString(fmt.Sprintf(", %s *C.zend_string", param.Name)) - - continue - } - - goType := gg.phpTypeToGoType(param.PhpType) - if param.IsNullable { - goType = "*" + goType - } - builder.WriteString(fmt.Sprintf(", %s %s", param.Name, goType)) - } - - if method.ReturnType != "void" { - if method.ReturnType == "string" { - builder.WriteString(") unsafe.Pointer {\n") - } else { - goReturnType := gg.phpTypeToGoType(method.ReturnType) - builder.WriteString(fmt.Sprintf(") %s {\n", goReturnType)) - } - } else { - builder.WriteString(") {\n") - } - - builder.WriteString(" obj := getGoObject(handle)\n") - builder.WriteString(" if obj == nil {\n") - if method.ReturnType != "void" { - if method.ReturnType == "string" { - builder.WriteString(" return nil\n") - } else { - builder.WriteString(fmt.Sprintf(" var zero %s\n", gg.phpTypeToGoType(method.ReturnType))) - builder.WriteString(" return zero\n") - } - } else { - builder.WriteString(" return\n") - } - builder.WriteString(" }\n") - builder.WriteString(fmt.Sprintf(" structObj := obj.(*%s)\n", class.GoStruct)) - - builder.WriteString(" ") - if method.ReturnType != "void" { - builder.WriteString("return ") - } - - builder.WriteString(fmt.Sprintf("structObj.%s(", gg.goMethodName(method.Name))) - - for i, param := range method.Params { - if i > 0 { - builder.WriteString(", ") - } - - builder.WriteString(param.Name) - } - - builder.WriteString(")\n") - builder.WriteString("}") - - return builder.String() -} - type GoMethodSignature struct { MethodName string Params []GoParameter @@ -165,28 +102,20 @@ type GoParameter struct { Type string } -func (gg *GoFileGenerator) phpTypeToGoType(phpType string) string { - typeMap := map[string]string{ - "string": "string", - "int": "int64", - "float": "float64", - "bool": "bool", - "array": "[]interface{}", - "mixed": "interface{}", - "void": "", +func (gg *GoFileGenerator) phpTypeToGoType(phpT phpType) string { + typeMap := map[phpType]string{ + phpString: "string", + phpInt: "int64", + phpFloat: "float64", + phpBool: "bool", + phpArray: "*frankenphp.Array", + phpMixed: "interface{}", + phpVoid: "", } - if goType, exists := typeMap[phpType]; exists { + if goType, exists := typeMap[phpT]; exists { return goType } return "interface{}" } - -func (gg *GoFileGenerator) goMethodName(phpMethodName string) string { - if len(phpMethodName) == 0 { - return phpMethodName - } - - return strings.ToUpper(phpMethodName[:1]) + phpMethodName[1:] -} diff --git a/internal/extgen/gofile_test.go b/internal/extgen/gofile_test.go index 7a2faffbf..11e962f3d 100644 --- a/internal/extgen/gofile_test.go +++ b/internal/extgen/gofile_test.go @@ -50,14 +50,14 @@ func anotherHelper() { Functions: []phpFunction{ { Name: "greet", - ReturnType: "string", + ReturnType: phpString, GoFunction: `func greet(name *go_string) *go_value { return types.String("Hello " + CStringToGoString(name)) }`, }, { Name: "calculate", - ReturnType: "int", + ReturnType: phpInt, GoFunction: `func calculate(a long, b long) *go_value { result := a + b return types.Int(result) @@ -102,7 +102,7 @@ func test() { functions: []phpFunction{ { Name: "test", - ReturnType: "void", + ReturnType: phpVoid, GoFunction: "func test() {\n\t// simple function\n}", }, }, @@ -135,7 +135,7 @@ func process(data *go_string) *go_value { functions: []phpFunction{ { Name: "process", - ReturnType: "string", + ReturnType: phpString, GoFunction: `func process(data *go_string) *go_value { return String(fmt.Sprintf("processed: %s", CStringToGoString(data))) }`, @@ -168,7 +168,7 @@ func internalFunc2(data string) { functions: []phpFunction{ { Name: "publicFunc", - ReturnType: "void", + ReturnType: phpVoid, GoFunction: "func publicFunc() {}", }, }, @@ -219,7 +219,7 @@ func TestGoFileGenerator_PackageNameSanitization(t *testing.T) { BaseName: tt.baseName, SourceFile: sourceFile, Functions: []phpFunction{ - {Name: "test", ReturnType: "void", GoFunction: "func test() {}"}, + {Name: "test", ReturnType: phpVoid, GoFunction: "func test() {}"}, }, } @@ -296,7 +296,7 @@ func test() {}` BaseName: "importtest", SourceFile: sourceFile, Functions: []phpFunction{ - {Name: "test", ReturnType: "void", GoFunction: "func test() {}"}, + {Name: "test", ReturnType: phpVoid, GoFunction: "func test() {}"}, }, } @@ -371,7 +371,7 @@ func debugPrint(msg string) { functions := []phpFunction{ { Name: "processData", - ReturnType: "array", + ReturnType: phpArray, GoFunction: `func processData(input *go_string, options *go_nullable) *go_value { data := CStringToGoString(input) processed := internalProcess(data) @@ -380,7 +380,7 @@ func debugPrint(msg string) { }, { Name: "validateInput", - ReturnType: "bool", + ReturnType: phpBool, GoFunction: `func validateInput(data *go_string) *go_value { input := CStringToGoString(data) isValid := len(input) > 0 && validateFormat(input) @@ -453,11 +453,11 @@ func (ts *TestStruct) ProcessData(name string, count *int64, enabled *bool) stri PhpName: "processData", ClassName: "TestClass", Signature: "processData(string $name, ?int $count, ?bool $enabled): string", - ReturnType: "string", + ReturnType: phpString, Params: []phpParameter{ - {Name: "name", PhpType: "string", IsNullable: false}, - {Name: "count", PhpType: "int", IsNullable: true}, - {Name: "enabled", PhpType: "bool", IsNullable: true}, + {Name: "name", PhpType: phpString, IsNullable: false}, + {Name: "count", PhpType: phpInt, IsNullable: true}, + {Name: "enabled", PhpType: phpBool, IsNullable: true}, }, GoFunction: `func (ts *TestStruct) ProcessData(name string, count *int64, enabled *bool) string { result := fmt.Sprintf("name=%s", name) @@ -501,6 +501,176 @@ func (ts *TestStruct) ProcessData(name string, count *int64, enabled *bool) stri assert.Contains(t, content, exportDirective, "Generated content should contain export directive: %s", exportDirective) } +func TestGoFileGenerator_MethodWrapperWithArrayParams(t *testing.T) { + tmpDir := t.TempDir() + + sourceContent := `package main + +import "fmt" + +//export_php:class ArrayClass +type ArrayStruct struct { + data []interface{} +} + +//export_php:method ArrayClass::processArray(array $items): array +func (as *ArrayStruct) ProcessArray(items *frankenphp.Array) *frankenphp.Array { + result := &frankenphp.Array{} + for i := uint32(0); i < items.Len(); i++ { + key, value := items.At(i) + result.SetString(fmt.Sprintf("processed_%d", i), value) + } + return result +} + +//export_php:method ArrayClass::filterData(array $data, string $filter): array +func (as *ArrayStruct) FilterData(data *frankenphp.Array, filter string) *frankenphp.Array { + result := &frankenphp.Array{} + // Filter logic here + return result +}` + + sourceFile := filepath.Join(tmpDir, "test.go") + require.NoError(t, os.WriteFile(sourceFile, []byte(sourceContent), 0644)) + + methods := []phpClassMethod{ + { + Name: "ProcessArray", + PhpName: "processArray", + ClassName: "ArrayClass", + Signature: "processArray(array $items): array", + ReturnType: phpArray, + Params: []phpParameter{ + {Name: "items", PhpType: phpArray, IsNullable: false}, + }, + GoFunction: `func (as *ArrayStruct) ProcessArray(items *frankenphp.Array) *frankenphp.Array { + result := &frankenphp.Array{} + for i := uint32(0); i < items.Len(); i++ { + key, value := items.At(i) + result.SetString(fmt.Sprintf("processed_%d", i), value) + } + return result +}`, + }, + { + Name: "FilterData", + PhpName: "filterData", + ClassName: "ArrayClass", + Signature: "filterData(array $data, string $filter): array", + ReturnType: phpArray, + Params: []phpParameter{ + {Name: "data", PhpType: phpArray, IsNullable: false}, + {Name: "filter", PhpType: phpString, IsNullable: false}, + }, + GoFunction: `func (as *ArrayStruct) FilterData(data *frankenphp.Array, filter string) *frankenphp.Array { + result := &frankenphp.Array{} + return result +}`, + }, + } + + classes := []phpClass{ + { + Name: "ArrayClass", + GoStruct: "ArrayStruct", + Methods: methods, + }, + } + + generator := &Generator{ + BaseName: "array_test", + SourceFile: sourceFile, + Classes: classes, + BuildDir: tmpDir, + } + + goGen := GoFileGenerator{generator} + content, err := goGen.buildContent() + require.NoError(t, err) + + expectedArrayWrapperSignature := "func ProcessArray_wrapper(handle C.uintptr_t, items *C.zval) unsafe.Pointer" + assert.Contains(t, content, expectedArrayWrapperSignature, "Generated content should contain array wrapper signature: %s", expectedArrayWrapperSignature) + + expectedMixedWrapperSignature := "func FilterData_wrapper(handle C.uintptr_t, data *C.zval, filter *C.zend_string) unsafe.Pointer" + assert.Contains(t, content, expectedMixedWrapperSignature, "Generated content should contain mixed wrapper signature: %s", expectedMixedWrapperSignature) + + expectedArrayCall := "structObj.ProcessArray(items)" + assert.Contains(t, content, expectedArrayCall, "Generated content should contain array method call: %s", expectedArrayCall) + + expectedMixedCall := "structObj.FilterData(data, filter)" + assert.Contains(t, content, expectedMixedCall, "Generated content should contain mixed method call: %s", expectedMixedCall) + + assert.Contains(t, content, "//export ProcessArray_wrapper", "Generated content should contain ProcessArray export directive") + assert.Contains(t, content, "//export FilterData_wrapper", "Generated content should contain FilterData export directive") +} + +func TestGoFileGenerator_MethodWrapperWithNullableArrayParams(t *testing.T) { + tmpDir := t.TempDir() + + sourceContent := `package main + +//export_php:class NullableArrayClass +type NullableArrayStruct struct{} + +//export_php:method NullableArrayClass::processOptionalArray(?array $items, string $name): string +func (nas *NullableArrayStruct) ProcessOptionalArray(items *frankenphp.Array, name string) string { + if items == nil { + return "No items: " + name + } + return fmt.Sprintf("Processing %d items for %s", items.Len(), name) +}` + + sourceFile := filepath.Join(tmpDir, "test.go") + require.NoError(t, os.WriteFile(sourceFile, []byte(sourceContent), 0644)) + + methods := []phpClassMethod{ + { + Name: "ProcessOptionalArray", + PhpName: "processOptionalArray", + ClassName: "NullableArrayClass", + Signature: "processOptionalArray(?array $items, string $name): string", + ReturnType: phpString, + Params: []phpParameter{ + {Name: "items", PhpType: phpArray, IsNullable: true}, + {Name: "name", PhpType: phpString, IsNullable: false}, + }, + GoFunction: `func (nas *NullableArrayStruct) ProcessOptionalArray(items *frankenphp.Array, name string) string { + if items == nil { + return "No items: " + name + } + return fmt.Sprintf("Processing %d items for %s", items.Len(), name) +}`, + }, + } + + classes := []phpClass{ + { + Name: "NullableArrayClass", + GoStruct: "NullableArrayStruct", + Methods: methods, + }, + } + + generator := &Generator{ + BaseName: "nullable_array_test", + SourceFile: sourceFile, + Classes: classes, + BuildDir: tmpDir, + } + + goGen := GoFileGenerator{generator} + content, err := goGen.buildContent() + require.NoError(t, err) + + expectedWrapperSignature := "func ProcessOptionalArray_wrapper(handle C.uintptr_t, items *C.zval, name *C.zend_string) unsafe.Pointer" + assert.Contains(t, content, expectedWrapperSignature, "Generated content should contain nullable array wrapper signature: %s", expectedWrapperSignature) + + expectedCall := "structObj.ProcessOptionalArray(items, name)" + assert.Contains(t, content, expectedCall, "Generated content should contain method call: %s", expectedCall) + + assert.Contains(t, content, "//export ProcessOptionalArray_wrapper", "Generated content should contain export directive") +} + func createTempSourceFile(t *testing.T, content string) string { tmpDir := t.TempDir() tmpFile := filepath.Join(tmpDir, "source.go") diff --git a/internal/extgen/nodes.go b/internal/extgen/nodes.go index b585089df..c57e595e5 100644 --- a/internal/extgen/nodes.go +++ b/internal/extgen/nodes.go @@ -5,19 +5,36 @@ import ( "strings" ) +// phpType represents a PHP type +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" +) + type phpFunction struct { Name string Signature string GoFunction string Params []phpParameter - ReturnType string + ReturnType phpType IsReturnNullable bool lineNumber int } type phpParameter struct { Name string - PhpType string + PhpType phpType IsNullable bool DefaultValue string HasDefault bool @@ -37,7 +54,7 @@ type phpClassMethod struct { GoFunction string Wrapper string Params []phpParameter - ReturnType string + ReturnType phpType isReturnNullable bool lineNumber int ClassName string // used by the "//export_php:method" directive @@ -45,7 +62,7 @@ type phpClassMethod struct { type phpClassProperty struct { Name string - PhpType string + PhpType phpType GoType string IsNullable bool } @@ -53,7 +70,7 @@ type phpClassProperty struct { type phpConstant struct { Name string Value string - PhpType string // "int", "string", "bool", "float" + PhpType phpType IsIota bool lineNumber int ClassName string // empty for global constants, set for class constants @@ -61,7 +78,7 @@ type phpConstant struct { // CValue returns the constant value in C-compatible format func (c phpConstant) CValue() string { - if c.PhpType != "int" { + if c.PhpType != phpInt { return c.Value } diff --git a/internal/extgen/paramparser.go b/internal/extgen/paramparser.go index 9fa42119d..7c203a359 100644 --- a/internal/extgen/paramparser.go +++ b/internal/extgen/paramparser.go @@ -42,24 +42,24 @@ func (pp *ParameterParser) generateSingleParamDeclaration(param phpParameter) [] var decls []string switch param.PhpType { - case "string": + case phpString: decls = append(decls, fmt.Sprintf("zend_string *%s = NULL;", param.Name)) if param.IsNullable { decls = append(decls, fmt.Sprintf("zend_bool %s_is_null = 0;", param.Name)) } - case "int": + case phpInt: defaultVal := pp.getDefaultValue(param, "0") decls = append(decls, fmt.Sprintf("zend_long %s = %s;", param.Name, defaultVal)) if param.IsNullable { decls = append(decls, fmt.Sprintf("zend_bool %s_is_null = 0;", param.Name)) } - case "float": + case phpFloat: defaultVal := pp.getDefaultValue(param, "0.0") decls = append(decls, fmt.Sprintf("double %s = %s;", param.Name, defaultVal)) if param.IsNullable { decls = append(decls, fmt.Sprintf("zend_bool %s_is_null = 0;", param.Name)) } - case "bool": + case phpBool: defaultVal := pp.getDefaultValue(param, "0") if param.HasDefault && param.DefaultValue == "true" { defaultVal = "1" @@ -68,6 +68,8 @@ func (pp *ParameterParser) generateSingleParamDeclaration(param phpParameter) [] if param.IsNullable { decls = append(decls, fmt.Sprintf("zend_bool %s_is_null = 0;", param.Name)) } + case phpArray: + decls = append(decls, fmt.Sprintf("zval *%s = NULL;", param.Name)) } return decls @@ -107,27 +109,31 @@ func (pp *ParameterParser) generateParamParsing(params []phpParameter, requiredC func (pp *ParameterParser) generateParamParsingMacro(param phpParameter) string { if param.IsNullable { switch param.PhpType { - case "string": + case phpString: return fmt.Sprintf("\n Z_PARAM_STR_OR_NULL(%s, %s_is_null)", param.Name, param.Name) - case "int": + case phpInt: return fmt.Sprintf("\n Z_PARAM_LONG_OR_NULL(%s, %s_is_null)", param.Name, param.Name) - case "float": + case phpFloat: return fmt.Sprintf("\n Z_PARAM_DOUBLE_OR_NULL(%s, %s_is_null)", param.Name, param.Name) - case "bool": + 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) default: return "" } } else { switch param.PhpType { - case "string": + case phpString: return fmt.Sprintf("\n Z_PARAM_STR(%s)", param.Name) - case "int": + case phpInt: return fmt.Sprintf("\n Z_PARAM_LONG(%s)", param.Name) - case "float": + case phpFloat: return fmt.Sprintf("\n Z_PARAM_DOUBLE(%s)", param.Name) - case "bool": + case phpBool: return fmt.Sprintf("\n Z_PARAM_BOOL(%s)", param.Name) + case phpArray: + return fmt.Sprintf("\n Z_PARAM_ARRAY(%s)", param.Name) default: return "" } @@ -150,27 +156,31 @@ func (pp *ParameterParser) generateGoCallParams(params []phpParameter) string { func (pp *ParameterParser) generateSingleGoCallParam(param phpParameter) string { if param.IsNullable { switch param.PhpType { - case "string": + case phpString: return fmt.Sprintf("%s_is_null ? NULL : %s", param.Name, param.Name) - case "int": + case phpInt: return fmt.Sprintf("%s_is_null ? NULL : &%s", param.Name, param.Name) - case "float": + case phpFloat: return fmt.Sprintf("%s_is_null ? NULL : &%s", param.Name, param.Name) - case "bool": + case phpBool: return fmt.Sprintf("%s_is_null ? NULL : &%s", param.Name, param.Name) + case phpArray: + return param.Name default: return param.Name } } else { switch param.PhpType { - case "string": + case phpString: return param.Name - case "int": + case phpInt: return fmt.Sprintf("(long) %s", param.Name) - case "float": + case phpFloat: return fmt.Sprintf("(double) %s", param.Name) - case "bool": + case phpBool: return fmt.Sprintf("(int) %s", param.Name) + case phpArray: + return param.Name default: return param.Name } diff --git a/internal/extgen/paramparser_test.go b/internal/extgen/paramparser_test.go index 254b96467..251719dc5 100644 --- a/internal/extgen/paramparser_test.go +++ b/internal/extgen/paramparser_test.go @@ -25,8 +25,8 @@ func TestParameterParser_AnalyzeParameters(t *testing.T) { { name: "all required parameters", params: []phpParameter{ - {Name: "name", PhpType: "string", HasDefault: false}, - {Name: "count", PhpType: "int", HasDefault: false}, + {Name: "name", PhpType: phpString, HasDefault: false}, + {Name: "count", PhpType: phpInt, HasDefault: false}, }, expected: ParameterInfo{ RequiredCount: 2, @@ -36,9 +36,9 @@ func TestParameterParser_AnalyzeParameters(t *testing.T) { { name: "mixed required and optional parameters", params: []phpParameter{ - {Name: "name", PhpType: "string", HasDefault: false}, - {Name: "count", PhpType: "int", HasDefault: true, DefaultValue: "10"}, - {Name: "enabled", PhpType: "bool", HasDefault: true, DefaultValue: "true"}, + {Name: "name", PhpType: phpString, HasDefault: false}, + {Name: "count", PhpType: phpInt, HasDefault: true, DefaultValue: "10"}, + {Name: "enabled", PhpType: phpBool, HasDefault: true, DefaultValue: "true"}, }, expected: ParameterInfo{ RequiredCount: 1, @@ -71,75 +71,98 @@ func TestParameterParser_GenerateParamDeclarations(t *testing.T) { { name: "string parameter", params: []phpParameter{ - {Name: "message", PhpType: "string", HasDefault: false}, + {Name: "message", PhpType: phpString, HasDefault: false}, }, expected: " zend_string *message = NULL;", }, { name: "nullable string parameter", params: []phpParameter{ - {Name: "message", PhpType: "string", HasDefault: false, IsNullable: true}, + {Name: "message", PhpType: phpString, HasDefault: false, IsNullable: true}, }, expected: " zend_string *message = NULL;\n zend_bool message_is_null = 0;", }, { name: "int parameter with default", params: []phpParameter{ - {Name: "count", PhpType: "int", HasDefault: true, DefaultValue: "42"}, + {Name: "count", PhpType: phpInt, HasDefault: true, DefaultValue: "42"}, }, expected: " zend_long count = 42;", }, { name: "nullable int parameter", params: []phpParameter{ - {Name: "count", PhpType: "int", HasDefault: false, IsNullable: true}, + {Name: "count", PhpType: phpInt, HasDefault: false, IsNullable: true}, }, expected: " zend_long count = 0;\n zend_bool count_is_null = 0;", }, { name: "bool parameter with true default", params: []phpParameter{ - {Name: "enabled", PhpType: "bool", HasDefault: true, DefaultValue: "true"}, + {Name: "enabled", PhpType: phpBool, HasDefault: true, DefaultValue: "true"}, }, expected: " zend_bool enabled = 1;", }, { name: "nullable bool parameter", params: []phpParameter{ - {Name: "enabled", PhpType: "bool", HasDefault: false, IsNullable: true}, + {Name: "enabled", PhpType: phpBool, HasDefault: false, IsNullable: true}, }, expected: " zend_bool enabled = 0;\n zend_bool enabled_is_null = 0;", }, { name: "float parameter", params: []phpParameter{ - {Name: "ratio", PhpType: "float", HasDefault: false}, + {Name: "ratio", PhpType: phpFloat, HasDefault: false}, }, expected: " double ratio = 0.0;", }, { name: "nullable float parameter", params: []phpParameter{ - {Name: "ratio", PhpType: "float", HasDefault: false, IsNullable: true}, + {Name: "ratio", PhpType: phpFloat, HasDefault: false, IsNullable: true}, }, expected: " double ratio = 0.0;\n zend_bool ratio_is_null = 0;", }, { name: "multiple parameters", params: []phpParameter{ - {Name: "name", PhpType: "string", HasDefault: false}, - {Name: "count", PhpType: "int", HasDefault: true, DefaultValue: "10"}, + {Name: "name", PhpType: phpString, HasDefault: false}, + {Name: "count", PhpType: phpInt, HasDefault: true, DefaultValue: "10"}, }, expected: " zend_string *name = NULL;\n zend_long count = 10;", }, { name: "mixed nullable and non-nullable parameters", params: []phpParameter{ - {Name: "name", PhpType: "string", HasDefault: false, IsNullable: false}, - {Name: "count", PhpType: "int", HasDefault: false, IsNullable: true}, + {Name: "name", PhpType: phpString, HasDefault: false, IsNullable: false}, + {Name: "count", PhpType: phpInt, HasDefault: false, IsNullable: true}, }, expected: " zend_string *name = NULL;\n zend_long count = 0;\n zend_bool count_is_null = 0;", }, + { + name: "array parameter", + params: []phpParameter{ + {Name: "items", PhpType: phpArray, HasDefault: false}, + }, + expected: " zval *items = NULL;", + }, + { + name: "nullable array parameter", + params: []phpParameter{ + {Name: "items", PhpType: phpArray, HasDefault: false, IsNullable: true}, + }, + expected: " zval *items = NULL;", + }, + { + name: "mixed types with array", + params: []phpParameter{ + {Name: "name", PhpType: phpString, HasDefault: false}, + {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;", + }, } for _, tt := range tests { @@ -170,7 +193,7 @@ func TestParameterParser_GenerateParamParsing(t *testing.T) { { name: "single required string parameter", params: []phpParameter{ - {Name: "message", PhpType: "string", HasDefault: false}, + {Name: "message", PhpType: phpString, HasDefault: false}, }, requiredCount: 1, expected: ` ZEND_PARSE_PARAMETERS_START(1, 1) @@ -180,9 +203,9 @@ func TestParameterParser_GenerateParamParsing(t *testing.T) { { name: "mixed required and optional parameters", params: []phpParameter{ - {Name: "name", PhpType: "string", HasDefault: false}, - {Name: "count", PhpType: "int", HasDefault: true, DefaultValue: "10"}, - {Name: "enabled", PhpType: "bool", HasDefault: true, DefaultValue: "true"}, + {Name: "name", PhpType: phpString, HasDefault: false}, + {Name: "count", PhpType: phpInt, HasDefault: true, DefaultValue: "10"}, + {Name: "enabled", PhpType: phpBool, HasDefault: true, DefaultValue: "true"}, }, requiredCount: 1, expected: ` ZEND_PARSE_PARAMETERS_START(1, 3) @@ -218,20 +241,43 @@ func TestParameterParser_GenerateGoCallParams(t *testing.T) { { name: "single string parameter", params: []phpParameter{ - {Name: "message", PhpType: "string"}, + {Name: "message", PhpType: phpString}, }, expected: "message", }, { name: "multiple parameters of different types", params: []phpParameter{ - {Name: "name", PhpType: "string"}, - {Name: "count", PhpType: "int"}, - {Name: "ratio", PhpType: "float"}, - {Name: "enabled", PhpType: "bool"}, + {Name: "name", PhpType: phpString}, + {Name: "count", PhpType: phpInt}, + {Name: "ratio", PhpType: phpFloat}, + {Name: "enabled", PhpType: phpBool}, }, expected: "name, (long) count, (double) ratio, (int) enabled", }, + { + name: "array parameter", + params: []phpParameter{ + {Name: "items", PhpType: phpArray}, + }, + expected: "items", + }, + { + name: "nullable array parameter", + params: []phpParameter{ + {Name: "items", PhpType: phpArray, IsNullable: true}, + }, + expected: "items", + }, + { + name: "mixed parameters with array", + params: []phpParameter{ + {Name: "name", PhpType: phpString}, + {Name: "items", PhpType: phpArray}, + {Name: "count", PhpType: phpInt}, + }, + expected: "name, items, (long) count", + }, } for _, tt := range tests { @@ -252,47 +298,57 @@ func TestParameterParser_GenerateParamParsingMacro(t *testing.T) { }{ { name: "string parameter", - param: phpParameter{Name: "message", PhpType: "string"}, + param: phpParameter{Name: "message", PhpType: phpString}, expected: "\n Z_PARAM_STR(message)", }, { name: "nullable string parameter", - param: phpParameter{Name: "message", PhpType: "string", IsNullable: true}, + param: phpParameter{Name: "message", PhpType: phpString, IsNullable: true}, expected: "\n Z_PARAM_STR_OR_NULL(message, message_is_null)", }, { name: "int parameter", - param: phpParameter{Name: "count", PhpType: "int"}, + param: phpParameter{Name: "count", PhpType: phpInt}, expected: "\n Z_PARAM_LONG(count)", }, { name: "nullable int parameter", - param: phpParameter{Name: "count", PhpType: "int", IsNullable: true}, + param: phpParameter{Name: "count", PhpType: phpInt, IsNullable: true}, expected: "\n Z_PARAM_LONG_OR_NULL(count, count_is_null)", }, { name: "float parameter", - param: phpParameter{Name: "ratio", PhpType: "float"}, + param: phpParameter{Name: "ratio", PhpType: phpFloat}, expected: "\n Z_PARAM_DOUBLE(ratio)", }, { name: "nullable float parameter", - param: phpParameter{Name: "ratio", PhpType: "float", IsNullable: true}, + param: phpParameter{Name: "ratio", PhpType: phpFloat, IsNullable: true}, expected: "\n Z_PARAM_DOUBLE_OR_NULL(ratio, ratio_is_null)", }, { name: "bool parameter", - param: phpParameter{Name: "enabled", PhpType: "bool"}, + param: phpParameter{Name: "enabled", PhpType: phpBool}, expected: "\n Z_PARAM_BOOL(enabled)", }, { name: "nullable bool parameter", - param: phpParameter{Name: "enabled", PhpType: "bool", IsNullable: true}, + param: phpParameter{Name: "enabled", PhpType: phpBool, IsNullable: true}, expected: "\n Z_PARAM_BOOL_OR_NULL(enabled, enabled_is_null)", }, + { + name: "array parameter", + param: phpParameter{Name: "items", PhpType: phpArray}, + expected: "\n Z_PARAM_ARRAY(items)", + }, + { + name: "nullable array parameter", + param: phpParameter{Name: "items", PhpType: phpArray, IsNullable: true}, + expected: "\n Z_PARAM_ARRAY_OR_NULL(items)", + }, { name: "unknown type", - param: phpParameter{Name: "unknown", PhpType: "unknown"}, + param: phpParameter{Name: "unknown", PhpType: phpType("unknown")}, expected: "", }, } @@ -316,19 +372,19 @@ func TestParameterParser_GetDefaultValue(t *testing.T) { }{ { name: "parameter without default", - param: phpParameter{Name: "count", PhpType: "int", HasDefault: false}, + param: phpParameter{Name: "count", PhpType: phpInt, HasDefault: false}, fallback: "0", expected: "0", }, { name: "parameter with default value", - param: phpParameter{Name: "count", PhpType: "int", HasDefault: true, DefaultValue: "42"}, + param: phpParameter{Name: "count", PhpType: phpInt, HasDefault: true, DefaultValue: "42"}, fallback: "0", expected: "42", }, { name: "parameter with empty default value", - param: phpParameter{Name: "count", PhpType: "int", HasDefault: true, DefaultValue: ""}, + param: phpParameter{Name: "count", PhpType: phpInt, HasDefault: true, DefaultValue: ""}, fallback: "0", expected: "0", }, @@ -352,47 +408,57 @@ func TestParameterParser_GenerateSingleGoCallParam(t *testing.T) { }{ { name: "string parameter", - param: phpParameter{Name: "message", PhpType: "string"}, + param: phpParameter{Name: "message", PhpType: phpString}, expected: "message", }, { name: "nullable string parameter", - param: phpParameter{Name: "message", PhpType: "string", IsNullable: true}, + param: phpParameter{Name: "message", PhpType: phpString, IsNullable: true}, expected: "message_is_null ? NULL : message", }, { name: "int parameter", - param: phpParameter{Name: "count", PhpType: "int"}, + param: phpParameter{Name: "count", PhpType: phpInt}, expected: "(long) count", }, { name: "nullable int parameter", - param: phpParameter{Name: "count", PhpType: "int", IsNullable: true}, + param: phpParameter{Name: "count", PhpType: phpInt, IsNullable: true}, expected: "count_is_null ? NULL : &count", }, { name: "float parameter", - param: phpParameter{Name: "ratio", PhpType: "float"}, + param: phpParameter{Name: "ratio", PhpType: phpFloat}, expected: "(double) ratio", }, { name: "nullable float parameter", - param: phpParameter{Name: "ratio", PhpType: "float", IsNullable: true}, + param: phpParameter{Name: "ratio", PhpType: phpFloat, IsNullable: true}, expected: "ratio_is_null ? NULL : &ratio", }, { name: "bool parameter", - param: phpParameter{Name: "enabled", PhpType: "bool"}, + param: phpParameter{Name: "enabled", PhpType: phpBool}, expected: "(int) enabled", }, { name: "nullable bool parameter", - param: phpParameter{Name: "enabled", PhpType: "bool", IsNullable: true}, + param: phpParameter{Name: "enabled", PhpType: phpBool, IsNullable: true}, expected: "enabled_is_null ? NULL : &enabled", }, + { + name: "array parameter", + param: phpParameter{Name: "items", PhpType: phpArray}, + expected: "items", + }, + { + name: "nullable array parameter", + param: phpParameter{Name: "items", PhpType: phpArray, IsNullable: true}, + expected: "items", + }, { name: "unknown type", - param: phpParameter{Name: "unknown", PhpType: "unknown"}, + param: phpParameter{Name: "unknown", PhpType: phpType("unknown")}, expected: "unknown", }, } @@ -415,49 +481,59 @@ func TestParameterParser_GenerateSingleParamDeclaration(t *testing.T) { }{ { name: "string parameter", - param: phpParameter{Name: "message", PhpType: "string", HasDefault: false}, + param: phpParameter{Name: "message", PhpType: phpString, HasDefault: false}, expected: []string{"zend_string *message = NULL;"}, }, { name: "nullable string parameter", - param: phpParameter{Name: "message", PhpType: "string", HasDefault: false, IsNullable: true}, + param: phpParameter{Name: "message", PhpType: phpString, HasDefault: false, IsNullable: true}, expected: []string{"zend_string *message = NULL;", "zend_bool message_is_null = 0;"}, }, { name: "int parameter with default", - param: phpParameter{Name: "count", PhpType: "int", HasDefault: true, DefaultValue: "42"}, + param: phpParameter{Name: "count", PhpType: phpInt, HasDefault: true, DefaultValue: "42"}, expected: []string{"zend_long count = 42;"}, }, { name: "nullable int parameter", - param: phpParameter{Name: "count", PhpType: "int", HasDefault: false, IsNullable: true}, + param: phpParameter{Name: "count", PhpType: phpInt, HasDefault: false, IsNullable: true}, expected: []string{"zend_long count = 0;", "zend_bool count_is_null = 0;"}, }, { name: "bool parameter with true default", - param: phpParameter{Name: "enabled", PhpType: "bool", HasDefault: true, DefaultValue: "true"}, + param: phpParameter{Name: "enabled", PhpType: phpBool, HasDefault: true, DefaultValue: "true"}, expected: []string{"zend_bool enabled = 1;"}, }, { name: "nullable bool parameter", - param: phpParameter{Name: "enabled", PhpType: "bool", HasDefault: false, IsNullable: true}, + param: phpParameter{Name: "enabled", PhpType: phpBool, HasDefault: false, IsNullable: true}, expected: []string{"zend_bool enabled = 0;", "zend_bool enabled_is_null = 0;"}, }, { name: "bool parameter with false default", - param: phpParameter{Name: "disabled", PhpType: "bool", HasDefault: true, DefaultValue: "false"}, + param: phpParameter{Name: "disabled", PhpType: phpBool, HasDefault: true, DefaultValue: "false"}, expected: []string{"zend_bool disabled = false;"}, }, { name: "float parameter", - param: phpParameter{Name: "ratio", PhpType: "float", HasDefault: false}, + param: phpParameter{Name: "ratio", PhpType: phpFloat, HasDefault: false}, expected: []string{"double ratio = 0.0;"}, }, { name: "nullable float parameter", - param: phpParameter{Name: "ratio", PhpType: "float", HasDefault: false, IsNullable: true}, + param: phpParameter{Name: "ratio", PhpType: phpFloat, HasDefault: false, IsNullable: true}, expected: []string{"double ratio = 0.0;", "zend_bool ratio_is_null = 0;"}, }, + { + name: "array parameter", + param: phpParameter{Name: "items", PhpType: phpArray, HasDefault: false}, + expected: []string{"zval *items = NULL;"}, + }, + { + name: "nullable array parameter", + param: phpParameter{Name: "items", PhpType: phpArray, HasDefault: false, IsNullable: true}, + expected: []string{"zval *items = NULL;"}, + }, } for _, tt := range tests { @@ -472,9 +548,9 @@ func TestParameterParser_Integration(t *testing.T) { pp := &ParameterParser{} params := []phpParameter{ - {Name: "name", PhpType: "string", HasDefault: false}, - {Name: "count", PhpType: "int", HasDefault: true, DefaultValue: "10"}, - {Name: "enabled", PhpType: "bool", HasDefault: true, DefaultValue: "true"}, + {Name: "name", PhpType: phpString, HasDefault: false}, + {Name: "count", PhpType: phpInt, HasDefault: true, DefaultValue: "10"}, + {Name: "enabled", PhpType: phpBool, HasDefault: true, DefaultValue: "true"}, } info := pp.analyzeParameters(params) diff --git a/internal/extgen/phpfunc.go b/internal/extgen/phpfunc.go index 2fdf519fe..8298bcb59 100644 --- a/internal/extgen/phpfunc.go +++ b/internal/extgen/phpfunc.go @@ -38,46 +38,58 @@ func (pfg *PHPFuncGenerator) generate(fn phpFunction) string { func (pfg *PHPFuncGenerator) generateGoCall(fn phpFunction) string { callParams := pfg.paramParser.generateGoCallParams(fn.Params) - if fn.ReturnType == "void" { + if fn.ReturnType == phpVoid { return fmt.Sprintf(" %s(%s);", fn.Name, callParams) } - if fn.ReturnType == "string" { + if fn.ReturnType == phpString { return fmt.Sprintf(" zend_string *result = %s(%s);", fn.Name, callParams) } + if fn.ReturnType == phpArray { + return fmt.Sprintf(" zend_array *result = %s(%s);", fn.Name, callParams) + } + return fmt.Sprintf(" %s result = %s(%s);", pfg.getCReturnType(fn.ReturnType), fn.Name, callParams) } -func (pfg *PHPFuncGenerator) getCReturnType(returnType string) string { +func (pfg *PHPFuncGenerator) getCReturnType(returnType phpType) string { switch returnType { - case "string": + case phpString: return "zend_string*" - case "int": + case phpInt: return "long" - case "float": + case phpFloat: return "double" - case "bool": + case phpBool: return "int" + case phpArray: + return "zend_array*" default: return "void" } } -func (pfg *PHPFuncGenerator) generateReturnCode(returnType string) string { +func (pfg *PHPFuncGenerator) generateReturnCode(returnType phpType) string { switch returnType { - case "string": + case phpString: return ` if (result) { RETURN_STR(result); - } else { - RETURN_EMPTY_STRING(); - }` - case "int": + } + + RETURN_EMPTY_STRING();` + case phpInt: return ` RETURN_LONG(result);` - case "float": + case phpFloat: return ` RETURN_DOUBLE(result);` - case "bool": + case phpBool: return ` RETURN_BOOL(result);` + case phpArray: + return ` if (result) { + RETURN_ARR(result); + } + + RETURN_EMPTY_ARRAY();` default: return "" } diff --git a/internal/extgen/phpfunc_test.go b/internal/extgen/phpfunc_test.go index 03281eee2..36a532f50 100644 --- a/internal/extgen/phpfunc_test.go +++ b/internal/extgen/phpfunc_test.go @@ -17,9 +17,9 @@ func TestPHPFunctionGenerator_Generate(t *testing.T) { name: "simple string function", function: phpFunction{ Name: "greet", - ReturnType: "string", + ReturnType: phpString, Params: []phpParameter{ - {Name: "name", PhpType: "string"}, + {Name: "name", PhpType: phpString}, }, }, contains: []string{ @@ -34,10 +34,10 @@ func TestPHPFunctionGenerator_Generate(t *testing.T) { name: "function with default parameter", function: phpFunction{ Name: "calculate", - ReturnType: "int", + ReturnType: phpInt, Params: []phpParameter{ - {Name: "base", PhpType: "int"}, - {Name: "multiplier", PhpType: "int", HasDefault: true, DefaultValue: "2"}, + {Name: "base", PhpType: phpInt}, + {Name: "multiplier", PhpType: phpInt, HasDefault: true, DefaultValue: "2"}, }, }, contains: []string{ @@ -54,9 +54,9 @@ func TestPHPFunctionGenerator_Generate(t *testing.T) { name: "void function", function: phpFunction{ Name: "doSomething", - ReturnType: "void", + ReturnType: phpVoid, Params: []phpParameter{ - {Name: "action", PhpType: "string"}, + {Name: "action", PhpType: phpString}, }, }, contains: []string{ @@ -68,9 +68,9 @@ func TestPHPFunctionGenerator_Generate(t *testing.T) { name: "bool function with default", function: phpFunction{ Name: "isEnabled", - ReturnType: "bool", + ReturnType: phpBool, Params: []phpParameter{ - {Name: "flag", PhpType: "bool", HasDefault: true, DefaultValue: "true"}, + {Name: "flag", PhpType: phpBool, HasDefault: true, DefaultValue: "true"}, }, }, contains: []string{ @@ -84,9 +84,9 @@ func TestPHPFunctionGenerator_Generate(t *testing.T) { name: "float function", function: phpFunction{ Name: "calculate", - ReturnType: "float", + ReturnType: phpFloat, Params: []phpParameter{ - {Name: "value", PhpType: "float"}, + {Name: "value", PhpType: phpFloat}, }, }, contains: []string{ @@ -96,6 +96,46 @@ func TestPHPFunctionGenerator_Generate(t *testing.T) { "RETURN_DOUBLE(result)", }, }, + { + name: "array function with array parameter", + function: phpFunction{ + Name: "process_array", + ReturnType: phpArray, + Params: []phpParameter{ + {Name: "input", PhpType: phpArray}, + }, + }, + contains: []string{ + "PHP_FUNCTION(process_array)", + "zval *input = NULL;", + "Z_PARAM_ARRAY(input)", + "zend_array *result = process_array(input);", + "RETURN_ARR(result)", + }, + }, + { + name: "array function with mixed parameters", + function: phpFunction{ + Name: "filter_array", + ReturnType: phpArray, + Params: []phpParameter{ + {Name: "data", PhpType: phpArray}, + {Name: "key", PhpType: phpString}, + {Name: "limit", PhpType: phpInt, HasDefault: true, DefaultValue: "10"}, + }, + }, + contains: []string{ + "PHP_FUNCTION(filter_array)", + "zval *data = NULL;", + "zend_string *key = NULL;", + "zend_long limit = 10;", + "Z_PARAM_ARRAY(data)", + "Z_PARAM_STR(key)", + "Z_PARAM_LONG(limit)", + "ZEND_PARSE_PARAMETERS_START(2, 3)", + "Z_PARAM_OPTIONAL", + }, + }, } generator := PHPFuncGenerator{} @@ -122,7 +162,7 @@ func TestPHPFunctionGenerator_GenerateParamDeclarations(t *testing.T) { { name: "string parameter", params: []phpParameter{ - {Name: "message", PhpType: "string"}, + {Name: "message", PhpType: phpString}, }, contains: []string{ "zend_string *message = NULL;", @@ -131,7 +171,7 @@ func TestPHPFunctionGenerator_GenerateParamDeclarations(t *testing.T) { { name: "int parameter", params: []phpParameter{ - {Name: "count", PhpType: "int"}, + {Name: "count", PhpType: phpInt}, }, contains: []string{ "zend_long count = 0;", @@ -140,7 +180,7 @@ func TestPHPFunctionGenerator_GenerateParamDeclarations(t *testing.T) { { name: "bool with default", params: []phpParameter{ - {Name: "enabled", PhpType: "bool", HasDefault: true, DefaultValue: "true"}, + {Name: "enabled", PhpType: phpBool, HasDefault: true, DefaultValue: "true"}, }, contains: []string{ "zend_bool enabled = 1;", @@ -149,12 +189,34 @@ func TestPHPFunctionGenerator_GenerateParamDeclarations(t *testing.T) { { name: "float parameter with default", params: []phpParameter{ - {Name: "rate", PhpType: "float", HasDefault: true, DefaultValue: "1.5"}, + {Name: "rate", PhpType: phpFloat, HasDefault: true, DefaultValue: "1.5"}, }, contains: []string{ "double rate = 1.5;", }, }, + { + name: "array parameter", + params: []phpParameter{ + {Name: "items", PhpType: phpArray}, + }, + contains: []string{ + "zval *items = NULL;", + }, + }, + { + name: "mixed types with array", + params: []phpParameter{ + {Name: "name", PhpType: phpString}, + {Name: "data", PhpType: phpArray}, + {Name: "count", PhpType: phpInt}, + }, + contains: []string{ + "zend_string *name = NULL;", + "zval *data = NULL;", + "zend_long count = 0;", + }, + }, } parser := ParameterParser{} @@ -172,12 +234,12 @@ func TestPHPFunctionGenerator_GenerateParamDeclarations(t *testing.T) { func TestPHPFunctionGenerator_GenerateReturnCode(t *testing.T) { tests := []struct { name string - returnType string + returnType phpType contains []string }{ { name: "string return", - returnType: "string", + returnType: phpString, contains: []string{ "RETURN_STR(result)", "RETURN_EMPTY_STRING()", @@ -185,28 +247,36 @@ func TestPHPFunctionGenerator_GenerateReturnCode(t *testing.T) { }, { name: "int return", - returnType: "int", + returnType: phpInt, contains: []string{ "RETURN_LONG(result)", }, }, { name: "bool return", - returnType: "bool", + returnType: phpBool, contains: []string{ "RETURN_BOOL(result)", }, }, { name: "float return", - returnType: "float", + returnType: phpFloat, contains: []string{ "RETURN_DOUBLE(result)", }, }, + { + name: "array return", + returnType: phpArray, + contains: []string{ + "RETURN_ARR(result)", + "RETURN_EMPTY_ARRAY()", + }, + }, { name: "void return", - returnType: "void", + returnType: phpVoid, contains: []string{}, }, } @@ -214,7 +284,7 @@ func TestPHPFunctionGenerator_GenerateReturnCode(t *testing.T) { generator := PHPFuncGenerator{} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result := generator.generateReturnCode(tt.returnType) + result := generator.generateReturnCode(phpType(tt.returnType)) if len(tt.contains) == 0 { assert.Empty(t, result, "Return code should be empty for void") @@ -242,33 +312,49 @@ func TestPHPFunctionGenerator_GenerateGoCallParams(t *testing.T) { { name: "simple string parameter", params: []phpParameter{ - {Name: "message", PhpType: "string"}, + {Name: "message", PhpType: phpString}, }, expected: "message", }, { name: "int parameter", params: []phpParameter{ - {Name: "count", PhpType: "int"}, + {Name: "count", PhpType: phpInt}, }, expected: "(long) count", }, { name: "multiple parameters", params: []phpParameter{ - {Name: "name", PhpType: "string"}, - {Name: "age", PhpType: "int"}, + {Name: "name", PhpType: phpString}, + {Name: "age", PhpType: phpInt}, }, expected: "name, (long) age", }, { name: "bool and float parameters", params: []phpParameter{ - {Name: "enabled", PhpType: "bool"}, - {Name: "rate", PhpType: "float"}, + {Name: "enabled", PhpType: phpBool}, + {Name: "rate", PhpType: phpFloat}, }, expected: "(int) enabled, (double) rate", }, + { + name: "array parameter", + params: []phpParameter{ + {Name: "data", PhpType: phpArray}, + }, + expected: "data", + }, + { + name: "mixed parameters with array", + params: []phpParameter{ + {Name: "name", PhpType: phpString}, + {Name: "items", PhpType: phpArray}, + {Name: "count", PhpType: phpInt}, + }, + expected: "name, items, (long) count", + }, } parser := ParameterParser{} @@ -297,8 +383,8 @@ func TestPHPFunctionGenerator_AnalyzeParameters(t *testing.T) { { name: "all required", params: []phpParameter{ - {Name: "a", PhpType: "string"}, - {Name: "b", PhpType: "int"}, + {Name: "a", PhpType: phpString}, + {Name: "b", PhpType: phpInt}, }, expectedReq: 2, expectedTotal: 2, @@ -306,8 +392,8 @@ func TestPHPFunctionGenerator_AnalyzeParameters(t *testing.T) { { name: "mixed required and optional", params: []phpParameter{ - {Name: "required", PhpType: "string"}, - {Name: "optional", PhpType: "int", HasDefault: true, DefaultValue: "10"}, + {Name: "required", PhpType: phpString}, + {Name: "optional", PhpType: phpInt, HasDefault: true, DefaultValue: "10"}, }, expectedReq: 1, expectedTotal: 2, @@ -315,8 +401,8 @@ func TestPHPFunctionGenerator_AnalyzeParameters(t *testing.T) { { name: "all optional", params: []phpParameter{ - {Name: "opt1", PhpType: "string", HasDefault: true, DefaultValue: "hello"}, - {Name: "opt2", PhpType: "int", HasDefault: true, DefaultValue: "0"}, + {Name: "opt1", PhpType: phpString, HasDefault: true, DefaultValue: "hello"}, + {Name: "opt2", PhpType: phpInt, HasDefault: true, DefaultValue: "0"}, }, expectedReq: 0, expectedTotal: 2, diff --git a/internal/extgen/stub.go b/internal/extgen/stub.go index 17881d622..eac929c44 100644 --- a/internal/extgen/stub.go +++ b/internal/extgen/stub.go @@ -40,11 +40,19 @@ func (sg *StubGenerator) buildContent() (string, error) { return buf.String(), nil } -// getPhpTypeAnnotation converts Go constant type to PHP type annotation -func getPhpTypeAnnotation(goType string) string { - switch goType { - case "string", "bool", "float", "int": - return goType +// getPhpTypeAnnotation converts phpType to PHP type annotation +func getPhpTypeAnnotation(t phpType) string { + switch t { + case phpString: + return "string" + case phpBool: + return "bool" + case phpFloat: + return "float" + case phpInt: + return "int" + case phpArray: + return "array" default: return "int" } diff --git a/internal/extgen/stub_test.go b/internal/extgen/stub_test.go index a63359887..418e9587d 100644 --- a/internal/extgen/stub_test.go +++ b/internal/extgen/stub_test.go @@ -19,18 +19,18 @@ func TestStubGenerator_Generate(t *testing.T) { Name: "greet", Signature: "greet(string $name): string", Params: []phpParameter{ - {Name: "name", PhpType: "string"}, + {Name: "name", PhpType: phpString}, }, - ReturnType: "string", + ReturnType: phpString, }, { Name: "calculate", Signature: "calculate(int $a, int $b): int", Params: []phpParameter{ - {Name: "a", PhpType: "int"}, - {Name: "b", PhpType: "int"}, + {Name: "a", PhpType: phpInt}, + {Name: "b", PhpType: phpInt}, }, - ReturnType: "int", + ReturnType: phpInt, }, }, Classes: []phpClass{ @@ -43,12 +43,12 @@ func TestStubGenerator_Generate(t *testing.T) { { Name: "GLOBAL_CONST", Value: "42", - PhpType: "int", + PhpType: phpInt, }, { Name: "USER_STATUS_ACTIVE", Value: "1", - PhpType: "int", + PhpType: phpInt, ClassName: "User", }, }, @@ -128,7 +128,7 @@ func TestStubGenerator_BuildContent(t *testing.T) { { Name: "GLOBAL_CONST", Value: `"test"`, - PhpType: "string", + PhpType: phpString, }, }, contains: []string{ @@ -430,13 +430,13 @@ func TestStubGenerator_ClassConstants(t *testing.T) { { Name: "STATUS_ACTIVE", Value: "1", - PhpType: "int", + PhpType: phpInt, ClassName: "MyClass", }, { Name: "STATUS_INACTIVE", Value: "0", - PhpType: "int", + PhpType: phpInt, ClassName: "MyClass", }, }, @@ -456,14 +456,14 @@ func TestStubGenerator_ClassConstants(t *testing.T) { { Name: "FIRST", Value: "0", - PhpType: "int", + PhpType: phpInt, IsIota: true, ClassName: "StatusClass", }, { Name: "SECOND", Value: "1", - PhpType: "int", + PhpType: phpInt, IsIota: true, ClassName: "StatusClass", }, @@ -485,12 +485,12 @@ func TestStubGenerator_ClassConstants(t *testing.T) { { Name: "GLOBAL_CONST", Value: `"global"`, - PhpType: "string", + PhpType: phpString, }, { Name: "CLASS_CONST", Value: "42", - PhpType: "int", + PhpType: phpInt, ClassName: "TestClass", }, }, diff --git a/internal/extgen/templates/extension.c.tpl b/internal/extgen/templates/extension.c.tpl index 35cf7f468..6ae0b3474 100644 --- a/internal/extgen/templates/extension.c.tpl +++ b/internal/extgen/templates/extension.c.tpl @@ -1,5 +1,7 @@ #include #include +#include +#include #include #include "{{.BaseName}}.h" @@ -92,6 +94,8 @@ PHP_METHOD({{namespacedClassName $.Namespace .ClassName}}, {{.PhpName}}) { {{- else if eq $param.PhpType "bool"}} 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; {{- end}} {{- end}} @@ -100,7 +104,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){{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}}){{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}} {{end -}} ZEND_PARSE_PARAMETERS_END(); {{else}} @@ -109,20 +113,28 @@ 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}}{{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}}{{end}}{{else}}{{.Name}}{{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}}{{end}}{{else}}(long){{.Name}}{{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}}{{end}}{{else}}{{if eq .PhpType "array"}}{{.Name}}{{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}}{{end}}{{else}}(double){{.Name}}{{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}}{{end}}{{else}}{{if eq .PhpType "array"}}{{.Name}}{{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}}{{end}}{{else}}(int){{.Name}}{{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}}{{end}}{{else}}{{if eq .PhpType "array"}}{{.Name}}{{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}}); + if (result != NULL) { + HashTable *ht = (HashTable*)result; + RETURN_ARR(ht); + } else { + RETURN_NULL(); + } {{- 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}}{{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}}{{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}}{{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}}); {{- end}} } {{end}}{{end}} diff --git a/internal/extgen/templates/extension.go.tpl b/internal/extgen/templates/extension.go.tpl index ab42c3624..39e7ae031 100644 --- a/internal/extgen/templates/extension.go.tpl +++ b/internal/extgen/templates/extension.go.tpl @@ -55,7 +55,7 @@ func removeGoObject(handle C.uintptr_t) { {{- end}} -{{- range .Classes}} +{{- range $class := .Classes}} //export create_{{.GoStruct}}_object func create_{{.GoStruct}}_object() C.uintptr_t { obj := &{{.GoStruct}}{} @@ -70,6 +70,22 @@ func create_{{.GoStruct}}_object() C.uintptr_t { {{- range .Methods}} //export {{.Name}}_wrapper -{{.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}} { + obj := getGoObject(handle) + if obj == nil { +{{- if not (isVoid .ReturnType)}} +{{- if isStringOrArray .ReturnType}} + return nil +{{- else}} + var zero {{phpTypeToGoType .ReturnType}} + return zero +{{- end}} +{{- else}} + return +{{- end}} + } + structObj := obj.(*{{$class.GoStruct}}) + {{if not (isVoid .ReturnType)}}return {{end}}structObj.{{.Name | title}}({{range $i, $param := .Params}}{{if $i}}, {{end}}{{$param.Name}}{{end}}) +} {{end}} {{- end}} diff --git a/internal/extgen/validator.go b/internal/extgen/validator.go index b4e897275..04f97086a 100644 --- a/internal/extgen/validator.go +++ b/internal/extgen/validator.go @@ -9,6 +9,22 @@ import ( "strings" ) +func scalarTypes() []phpType { + return []phpType{phpString, phpInt, phpFloat, phpBool, phpArray} +} + +func paramTypes() []phpType { + return []phpType{phpString, phpInt, phpFloat, phpBool, phpArray, phpObject, phpMixed} +} + +func returnTypes() []phpType { + return []phpType{phpVoid, phpString, phpInt, phpFloat, phpBool, phpArray, phpObject, phpMixed, phpNull, phpTrue, phpFalse} +} + +func propTypes() []phpType { + return []phpType{phpString, phpInt, phpFloat, phpBool, phpArray, phpObject, phpMixed} +} + var functionNameRegex = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`) var parameterNameRegex = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`) var classNameRegex = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`) @@ -47,17 +63,17 @@ 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"} - if !v.isValidType(param.PhpType, validTypes) { + validTypes := paramTypes() + if !v.isValidPHPType(param.PhpType, validTypes) { return fmt.Errorf("invalid parameter type: %s", param.PhpType) } return nil } -func (v *Validator) validateReturnType(returnType string) error { - validReturnTypes := []string{"void", "string", "int", "float", "bool", "array", "object", "mixed", "null", "true", "false"} - if !v.isValidType(returnType, validReturnTypes) { +func (v *Validator) validateReturnType(returnType phpType) error { + validReturnTypes := returnTypes() + if !v.isValidPHPType(returnType, validReturnTypes) { return fmt.Errorf("invalid return type: %s", returnType) } return nil @@ -90,17 +106,17 @@ 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"} - if !v.isValidType(prop.PhpType, validTypes) { + validTypes := propTypes() + if !v.isValidPHPType(prop.PhpType, validTypes) { return fmt.Errorf("invalid property type: %s", prop.PhpType) } return nil } -func (v *Validator) isValidType(typeStr string, validTypes []string) bool { +func (v *Validator) isValidPHPType(phpType phpType, validTypes []phpType) bool { for _, valid := range validTypes { - if typeStr == valid { + if phpType == valid { return true } } @@ -109,22 +125,22 @@ 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"} + supportedTypes := scalarTypes() 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) and their nullable variants are supported", i+1, param.Name, param.PhpType) + if !v.isScalarPHPType(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) } } - 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), void, and their nullable variants are supported", fn.ReturnType) + if fn.ReturnType != phpVoid && !v.isScalarPHPType(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 nil } -func (v *Validator) isScalarType(phpType string, supportedTypes []string) bool { +func (v *Validator) isScalarPHPType(phpType phpType, supportedTypes []phpType) bool { for _, supported := range supportedTypes { if phpType == supported { return true @@ -197,7 +213,7 @@ func (v *Validator) validateGoFunctionSignatureWithOptions(phpFunc phpFunction, } } - expectedGoReturnType := v.phpReturnTypeToGoType(phpFunc.ReturnType, phpFunc.IsReturnNullable) + expectedGoReturnType := v.phpReturnTypeToGoType(phpFunc.ReturnType) actualGoReturnType := v.goReturnTypeToString(goFunc.Type.Results) if !v.isCompatibleGoType(expectedGoReturnType, actualGoReturnType) { @@ -207,22 +223,24 @@ func (v *Validator) validateGoFunctionSignatureWithOptions(phpFunc phpFunction, return nil } -func (v *Validator) phpTypeToGoType(phpType string, isNullable bool) string { +func (v *Validator) phpTypeToGoType(t phpType, isNullable bool) string { var baseType string - switch phpType { - case "string": + switch t { + case phpString: baseType = "*C.zend_string" - case "int": + case phpInt: baseType = "int64" - case "float": + case phpFloat: baseType = "float64" - case "bool": + case phpBool: baseType = "bool" + case phpArray: + baseType = "*C.zval" default: baseType = "interface{}" } - if isNullable && phpType != "string" { + if isNullable && t != phpString && t != phpArray { return "*" + baseType } @@ -247,18 +265,20 @@ func (v *Validator) isCompatibleGoType(expectedType, actualType string) bool { return false } -func (v *Validator) phpReturnTypeToGoType(phpReturnType string, isNullable bool) string { +func (v *Validator) phpReturnTypeToGoType(phpReturnType phpType) string { switch phpReturnType { - case "void": + case phpVoid: return "" - case "string": + case phpString: return "unsafe.Pointer" - case "int": + case phpInt: return "int64" - case "float": + case phpFloat: return "float64" - case "bool": + case phpBool: return "bool" + case phpArray: + return "unsafe.Pointer" default: return "interface{}" } diff --git a/internal/extgen/validator_test.go b/internal/extgen/validator_test.go index 3e1b54c00..53d941c1f 100644 --- a/internal/extgen/validator_test.go +++ b/internal/extgen/validator_test.go @@ -16,10 +16,10 @@ func TestValidateFunction(t *testing.T) { name: "valid function", function: phpFunction{ Name: "validFunction", - ReturnType: "string", + ReturnType: phpString, Params: []phpParameter{ - {Name: "param1", PhpType: "string"}, - {Name: "param2", PhpType: "int"}, + {Name: "param1", PhpType: phpString}, + {Name: "param2", PhpType: phpInt}, }, }, expectError: false, @@ -28,10 +28,34 @@ func TestValidateFunction(t *testing.T) { name: "valid function with nullable return", function: phpFunction{ Name: "nullableReturn", - ReturnType: "string", + ReturnType: phpString, IsReturnNullable: true, Params: []phpParameter{ - {Name: "data", PhpType: "array"}, + {Name: "data", PhpType: phpArray}, + }, + }, + expectError: false, + }, + { + name: "valid function with array parameter", + function: phpFunction{ + Name: "arrayFunction", + ReturnType: phpArray, + Params: []phpParameter{ + {Name: "items", PhpType: phpArray}, + {Name: "filter", PhpType: phpString}, + }, + }, + expectError: false, + }, + { + name: "valid function with nullable array parameter", + function: phpFunction{ + Name: "nullableArrayFunction", + ReturnType: phpString, + Params: []phpParameter{ + {Name: "items", PhpType: phpArray, IsNullable: true}, + {Name: "name", PhpType: phpString}, }, }, expectError: false, @@ -40,7 +64,7 @@ func TestValidateFunction(t *testing.T) { name: "empty function name", function: phpFunction{ Name: "", - ReturnType: "string", + ReturnType: phpString, }, expectError: true, }, @@ -48,7 +72,7 @@ func TestValidateFunction(t *testing.T) { name: "invalid function name - starts with number", function: phpFunction{ Name: "123invalid", - ReturnType: "string", + ReturnType: phpString, }, expectError: true, }, @@ -56,7 +80,7 @@ func TestValidateFunction(t *testing.T) { name: "invalid function name - contains special chars", function: phpFunction{ Name: "invalid-name", - ReturnType: "string", + ReturnType: phpString, }, expectError: true, }, @@ -64,9 +88,9 @@ func TestValidateFunction(t *testing.T) { name: "invalid parameter name", function: phpFunction{ Name: "validName", - ReturnType: "string", + ReturnType: phpString, Params: []phpParameter{ - {Name: "123invalid", PhpType: "string"}, + {Name: "123invalid", PhpType: phpString}, }, }, expectError: true, @@ -75,9 +99,9 @@ func TestValidateFunction(t *testing.T) { name: "empty parameter name", function: phpFunction{ Name: "validName", - ReturnType: "string", + ReturnType: phpString, Params: []phpParameter{ - {Name: "", PhpType: "string"}, + {Name: "", PhpType: phpString}, }, }, expectError: true, @@ -154,7 +178,7 @@ func TestValidateReturnType(t *testing.T) { validator := Validator{} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := validator.validateReturnType(tt.returnType) + err := validator.validateReturnType(phpType(tt.returnType)) if tt.expectError { assert.Error(t, err, "validateReturnType(%s) should return an error", tt.returnType) @@ -175,7 +199,7 @@ func TestValidateClassProperty(t *testing.T) { name: "valid property", prop: phpClassProperty{ Name: "validProperty", - PhpType: "string", + PhpType: phpString, GoType: "string", }, expectError: false, @@ -184,7 +208,7 @@ func TestValidateClassProperty(t *testing.T) { name: "valid nullable property", prop: phpClassProperty{ Name: "nullableProperty", - PhpType: "int", + PhpType: phpInt, GoType: "*int", IsNullable: true, }, @@ -194,7 +218,7 @@ func TestValidateClassProperty(t *testing.T) { name: "empty property name", prop: phpClassProperty{ Name: "", - PhpType: "string", + PhpType: phpString, }, expectError: true, }, @@ -202,7 +226,7 @@ func TestValidateClassProperty(t *testing.T) { name: "invalid property name", prop: phpClassProperty{ Name: "123invalid", - PhpType: "string", + PhpType: phpString, }, expectError: true, }, @@ -210,7 +234,7 @@ func TestValidateClassProperty(t *testing.T) { name: "invalid property type", prop: phpClassProperty{ Name: "validName", - PhpType: "invalidType", + PhpType: phpType("invalidType"), }, expectError: true, }, @@ -240,7 +264,7 @@ func TestValidateParameter(t *testing.T) { name: "valid string parameter", param: phpParameter{ Name: "validParam", - PhpType: "string", + PhpType: phpString, }, expectError: false, }, @@ -248,7 +272,7 @@ func TestValidateParameter(t *testing.T) { name: "valid nullable parameter", param: phpParameter{ Name: "nullableParam", - PhpType: "int", + PhpType: phpInt, IsNullable: true, }, expectError: false, @@ -257,17 +281,34 @@ func TestValidateParameter(t *testing.T) { name: "valid parameter with default", param: phpParameter{ Name: "defaultParam", - PhpType: "string", + PhpType: phpString, HasDefault: true, DefaultValue: "hello", }, expectError: false, }, + { + name: "valid array parameter", + param: phpParameter{ + Name: "arrayParam", + PhpType: phpArray, + }, + expectError: false, + }, + { + name: "valid nullable array parameter", + param: phpParameter{ + Name: "nullableArrayParam", + PhpType: phpArray, + IsNullable: true, + }, + expectError: false, + }, { name: "empty parameter name", param: phpParameter{ Name: "", - PhpType: "string", + PhpType: phpString, }, expectError: true, }, @@ -275,7 +316,7 @@ func TestValidateParameter(t *testing.T) { name: "invalid parameter name", param: phpParameter{ Name: "123invalid", - PhpType: "string", + PhpType: phpString, }, expectError: true, }, @@ -283,7 +324,7 @@ func TestValidateParameter(t *testing.T) { name: "invalid parameter type", param: phpParameter{ Name: "validName", - PhpType: "invalidType", + PhpType: phpType("invalidType"), }, expectError: true, }, @@ -315,8 +356,8 @@ func TestValidateClass(t *testing.T) { Name: "ValidClass", GoStruct: "ValidStruct", Properties: []phpClassProperty{ - {Name: "name", PhpType: "string"}, - {Name: "age", PhpType: "int"}, + {Name: "name", PhpType: phpString}, + {Name: "age", PhpType: phpInt}, }, }, expectError: false, @@ -327,8 +368,8 @@ func TestValidateClass(t *testing.T) { Name: "NullableClass", GoStruct: "NullableStruct", Properties: []phpClassProperty{ - {Name: "required", PhpType: "string", IsNullable: false}, - {Name: "optional", PhpType: "string", IsNullable: true}, + {Name: "required", PhpType: phpString, IsNullable: false}, + {Name: "optional", PhpType: phpString, IsNullable: true}, }, }, expectError: false, @@ -355,7 +396,7 @@ func TestValidateClass(t *testing.T) { Name: "ValidClass", GoStruct: "ValidStruct", Properties: []phpClassProperty{ - {Name: "123invalid", PhpType: "string"}, + {Name: "123invalid", PhpType: phpString}, }, }, expectError: true, @@ -387,12 +428,12 @@ func TestValidateScalarTypes(t *testing.T) { name: "valid scalar parameters only", function: phpFunction{ Name: "validFunction", - ReturnType: "string", + ReturnType: phpString, Params: []phpParameter{ - {Name: "stringParam", PhpType: "string"}, - {Name: "intParam", PhpType: "int"}, - {Name: "floatParam", PhpType: "float"}, - {Name: "boolParam", PhpType: "bool"}, + {Name: "stringParam", PhpType: phpString}, + {Name: "intParam", PhpType: phpInt}, + {Name: "floatParam", PhpType: phpFloat}, + {Name: "boolParam", PhpType: phpBool}, }, }, expectError: false, @@ -401,10 +442,10 @@ func TestValidateScalarTypes(t *testing.T) { name: "valid nullable scalar parameters", function: phpFunction{ Name: "nullableFunction", - ReturnType: "string", + ReturnType: phpString, Params: []phpParameter{ - {Name: "stringParam", PhpType: "string", IsNullable: true}, - {Name: "intParam", PhpType: "int", IsNullable: true}, + {Name: "stringParam", PhpType: phpString, IsNullable: true}, + {Name: "intParam", PhpType: phpInt, IsNullable: true}, }, }, expectError: false, @@ -413,68 +454,67 @@ func TestValidateScalarTypes(t *testing.T) { name: "valid void return type", function: phpFunction{ Name: "voidFunction", - ReturnType: "void", + ReturnType: phpVoid, Params: []phpParameter{ - {Name: "stringParam", PhpType: "string"}, + {Name: "stringParam", PhpType: phpString}, }, }, expectError: false, }, { - name: "invalid array parameter", + name: "valid array parameter and return", function: phpFunction{ Name: "arrayFunction", - ReturnType: "string", + ReturnType: phpArray, Params: []phpParameter{ - {Name: "arrayParam", PhpType: "array"}, + {Name: "arrayParam", PhpType: phpArray}, + {Name: "stringParam", PhpType: phpString}, }, }, - expectError: true, - errorMsg: "parameter 1 (arrayParam) has unsupported type 'array'", + expectError: false, }, { - name: "invalid object parameter", + name: "valid nullable array parameter", function: phpFunction{ - Name: "objectFunction", - ReturnType: "string", + Name: "nullableArrayFunction", + ReturnType: phpString, Params: []phpParameter{ - {Name: "objectParam", PhpType: "object"}, + {Name: "arrayParam", PhpType: phpArray, IsNullable: true}, }, }, - expectError: true, - errorMsg: "parameter 1 (objectParam) has unsupported type 'object'", + expectError: false, }, { - name: "invalid mixed parameter", + name: "invalid object parameter", function: phpFunction{ - Name: "mixedFunction", - ReturnType: "string", + Name: "objectFunction", + ReturnType: phpString, Params: []phpParameter{ - {Name: "mixedParam", PhpType: "mixed"}, + {Name: "objectParam", PhpType: phpObject}, }, }, expectError: true, - errorMsg: "parameter 1 (mixedParam) has unsupported type 'mixed'", + errorMsg: "parameter 1 (objectParam) has unsupported type 'object'", }, { - name: "invalid array return type", + name: "invalid mixed parameter", function: phpFunction{ - Name: "arrayReturnFunction", - ReturnType: "array", + Name: "mixedFunction", + ReturnType: phpString, Params: []phpParameter{ - {Name: "stringParam", PhpType: "string"}, + {Name: "mixedParam", PhpType: phpMixed}, }, }, expectError: true, - errorMsg: "return type 'array' is not supported", + errorMsg: "parameter 1 (mixedParam) has unsupported type 'mixed'", }, { name: "invalid object return type", function: phpFunction{ Name: "objectReturnFunction", - ReturnType: "object", + ReturnType: phpObject, Params: []phpParameter{ - {Name: "stringParam", PhpType: "string"}, + {Name: "stringParam", PhpType: phpString}, }, }, expectError: true, @@ -484,15 +524,15 @@ func TestValidateScalarTypes(t *testing.T) { name: "mixed scalar and invalid parameters", function: phpFunction{ Name: "mixedFunction", - ReturnType: "string", + ReturnType: phpString, Params: []phpParameter{ - {Name: "validParam", PhpType: "string"}, - {Name: "invalidParam", PhpType: "array"}, - {Name: "anotherValidParam", PhpType: "int"}, + {Name: "validParam", PhpType: phpString}, + {Name: "invalidParam", PhpType: phpObject}, + {Name: "anotherValidParam", PhpType: phpInt}, }, }, expectError: true, - errorMsg: "parameter 2 (invalidParam) has unsupported type 'array'", + errorMsg: "parameter 2 (invalidParam) has unsupported type 'object'", }, } @@ -522,10 +562,10 @@ func TestValidateGoFunctionSignature(t *testing.T) { name: "valid Go function signature", phpFunc: phpFunction{ Name: "testFunc", - ReturnType: "string", + ReturnType: phpString, Params: []phpParameter{ - {Name: "name", PhpType: "string"}, - {Name: "count", PhpType: "int"}, + {Name: "name", PhpType: phpString}, + {Name: "count", PhpType: phpInt}, }, GoFunction: `func testFunc(name *C.zend_string, count int64) unsafe.Pointer { return nil @@ -537,9 +577,9 @@ func TestValidateGoFunctionSignature(t *testing.T) { name: "valid void return type", phpFunc: phpFunction{ Name: "voidFunc", - ReturnType: "void", + ReturnType: phpVoid, Params: []phpParameter{ - {Name: "message", PhpType: "string"}, + {Name: "message", PhpType: phpString}, }, GoFunction: `func voidFunc(message *C.zend_string) { // Do something @@ -551,7 +591,7 @@ func TestValidateGoFunctionSignature(t *testing.T) { name: "no Go function provided", phpFunc: phpFunction{ Name: "noGoFunc", - ReturnType: "string", + ReturnType: phpString, Params: []phpParameter{}, GoFunction: "", }, @@ -562,10 +602,10 @@ func TestValidateGoFunctionSignature(t *testing.T) { name: "parameter count mismatch", phpFunc: phpFunction{ Name: "countMismatch", - ReturnType: "string", + ReturnType: phpString, Params: []phpParameter{ - {Name: "param1", PhpType: "string"}, - {Name: "param2", PhpType: "int"}, + {Name: "param1", PhpType: phpString}, + {Name: "param2", PhpType: phpInt}, }, GoFunction: `func countMismatch(param1 *C.zend_string) unsafe.Pointer { return nil @@ -578,10 +618,10 @@ func TestValidateGoFunctionSignature(t *testing.T) { name: "parameter type mismatch", phpFunc: phpFunction{ Name: "typeMismatch", - ReturnType: "string", + ReturnType: phpString, Params: []phpParameter{ - {Name: "name", PhpType: "string"}, - {Name: "count", PhpType: "int"}, + {Name: "name", PhpType: phpString}, + {Name: "count", PhpType: phpInt}, }, GoFunction: `func typeMismatch(name *C.zend_string, count string) unsafe.Pointer { return nil @@ -594,9 +634,9 @@ func TestValidateGoFunctionSignature(t *testing.T) { name: "return type mismatch", phpFunc: phpFunction{ Name: "returnMismatch", - ReturnType: "int", + ReturnType: phpInt, Params: []phpParameter{ - {Name: "value", PhpType: "string"}, + {Name: "value", PhpType: phpString}, }, GoFunction: `func returnMismatch(value *C.zend_string) string { return "" @@ -609,9 +649,9 @@ func TestValidateGoFunctionSignature(t *testing.T) { name: "valid bool parameter and return", phpFunc: phpFunction{ Name: "boolFunc", - ReturnType: "bool", + ReturnType: phpBool, Params: []phpParameter{ - {Name: "flag", PhpType: "bool"}, + {Name: "flag", PhpType: phpBool}, }, GoFunction: `func boolFunc(flag bool) bool { return flag @@ -623,12 +663,57 @@ func TestValidateGoFunctionSignature(t *testing.T) { name: "valid float parameter and return", phpFunc: phpFunction{ Name: "floatFunc", - ReturnType: "float", + ReturnType: phpFloat, Params: []phpParameter{ - {Name: "value", PhpType: "float"}, + {Name: "value", PhpType: phpFloat}, }, GoFunction: `func floatFunc(value float64) float64 { return value * 2.0 +}`, + }, + expectError: false, + }, + { + name: "valid array parameter and return", + phpFunc: phpFunction{ + Name: "arrayFunc", + ReturnType: phpArray, + Params: []phpParameter{ + {Name: "items", PhpType: phpArray}, + }, + GoFunction: `func arrayFunc(items *C.zval) unsafe.Pointer { + return nil +}`, + }, + expectError: false, + }, + { + name: "valid nullable array parameter", + phpFunc: phpFunction{ + Name: "nullableArrayFunc", + ReturnType: phpString, + Params: []phpParameter{ + {Name: "items", PhpType: phpArray, IsNullable: true}, + {Name: "name", PhpType: phpString}, + }, + GoFunction: `func nullableArrayFunc(items *C.zval, name *C.zend_string) unsafe.Pointer { + return nil +}`, + }, + expectError: false, + }, + { + name: "mixed array and scalar parameters", + phpFunc: phpFunction{ + Name: "mixedFunc", + ReturnType: phpArray, + Params: []phpParameter{ + {Name: "data", PhpType: phpArray}, + {Name: "filter", PhpType: phpString}, + {Name: "limit", PhpType: phpInt}, + }, + GoFunction: `func mixedFunc(data *C.zval, filter *C.zend_string, limit int64) unsafe.Pointer { + return nil }`, }, expectError: false, @@ -657,20 +742,22 @@ func TestPhpTypeToGoType(t *testing.T) { expected string }{ {"string", false, "*C.zend_string"}, - {"string", true, "*C.zend_string"}, // String is already a pointer, no change for nullable + {"string", true, "*C.zend_string"}, {"int", false, "int64"}, - {"int", true, "*int64"}, // Nullable int becomes pointer to int64 + {"int", true, "*int64"}, {"float", false, "float64"}, - {"float", true, "*float64"}, // Nullable float becomes pointer to float64 + {"float", true, "*float64"}, {"bool", false, "bool"}, - {"bool", true, "*bool"}, // Nullable bool becomes pointer to bool + {"bool", true, "*bool"}, + {"array", false, "*C.zval"}, + {"array", true, "*C.zval"}, {"unknown", false, "interface{}"}, } validator := Validator{} for _, tt := range tests { t.Run(tt.phpType, func(t *testing.T) { - result := validator.phpTypeToGoType(tt.phpType, tt.isNullable) + result := validator.phpTypeToGoType(phpType(tt.phpType), tt.isNullable) assert.Equal(t, tt.expected, result, "phpTypeToGoType(%s, %v) should return %s", tt.phpType, tt.isNullable, tt.expected) }) } @@ -679,27 +766,28 @@ func TestPhpTypeToGoType(t *testing.T) { func TestPhpReturnTypeToGoType(t *testing.T) { tests := []struct { phpReturnType string - isNullable bool expected string }{ - {"void", false, ""}, - {"void", true, ""}, - {"string", false, "unsafe.Pointer"}, - {"string", true, "unsafe.Pointer"}, - {"int", false, "int64"}, - {"int", true, "int64"}, - {"float", false, "float64"}, - {"float", true, "float64"}, - {"bool", false, "bool"}, - {"bool", true, "bool"}, - {"unknown", false, "interface{}"}, + {"void", ""}, + {"void", ""}, + {"string", "unsafe.Pointer"}, + {"string", "unsafe.Pointer"}, + {"int", "int64"}, + {"int", "int64"}, + {"float", "float64"}, + {"float", "float64"}, + {"bool", "bool"}, + {"bool", "bool"}, + {"array", "unsafe.Pointer"}, + {"array", "unsafe.Pointer"}, + {"unknown", "interface{}"}, } validator := Validator{} for _, tt := range tests { t.Run(tt.phpReturnType, func(t *testing.T) { - result := validator.phpReturnTypeToGoType(tt.phpReturnType, tt.isNullable) - assert.Equal(t, tt.expected, result, "phpReturnTypeToGoType(%s, %v) should return %s", tt.phpReturnType, tt.isNullable, tt.expected) + result := validator.phpReturnTypeToGoType(phpType(tt.phpReturnType)) + assert.Equal(t, tt.expected, result, "phpReturnTypeToGoType(%s) should return %s", tt.phpReturnType, tt.expected) }) } } diff --git a/types.c b/types.c new file mode 100644 index 000000000..b03dc2abd --- /dev/null +++ b/types.c @@ -0,0 +1,22 @@ +#include "types.h" + +zval *get_ht_packed_data(HashTable *ht, uint32_t index) { + if (ht->u.flags & HASH_FLAG_PACKED) { + return &ht->arPacked[index]; + } + return NULL; +} + +Bucket *get_ht_bucket_data(HashTable *ht, uint32_t index) { + if (!(ht->u.flags & HASH_FLAG_PACKED)) { + return &ht->arData[index]; + } + return NULL; +} + +void *__emalloc__(size_t size) { return emalloc(size); } + +void __zend_hash_init__(HashTable *ht, uint32_t nSize, dtor_func_t pDestructor, + bool persistent) { + zend_hash_init(ht, nSize, null, pDestructor, persistent); +} \ No newline at end of file diff --git a/types.go b/types.go index 446ec9cbc..8683c92e0 100644 --- a/types.go +++ b/types.go @@ -1,6 +1,8 @@ package frankenphp -//#include +/* +#include "types.h" +*/ import "C" import "unsafe" @@ -31,3 +33,270 @@ func PHPString(s string, persistent bool) unsafe.Pointer { return unsafe.Pointer(zendStr) } + +// PHPKeyType represents the type of PHP hashmap key +type PHPKeyType int + +const ( + PHPIntKey PHPKeyType = iota + PHPStringKey +) + +type PHPKey struct { + Type PHPKeyType + Str string + Int int64 +} + +// Array represents a PHP array with ordered key-value pairs +type Array struct { + keys []PHPKey + values []interface{} +} + +// SetInt sets a value with an integer key +func (arr *Array) SetInt(key int64, value interface{}) { + arr.keys = append(arr.keys, PHPKey{Type: PHPIntKey, Int: key}) + arr.values = append(arr.values, value) +} + +// SetString sets a value with a string key +func (arr *Array) SetString(key string, value interface{}) { + arr.keys = append(arr.keys, PHPKey{Type: PHPStringKey, Str: key}) + arr.values = append(arr.values, value) +} + +// Append adds a value to the end of the array with the next available integer key +func (arr *Array) Append(value interface{}) { + nextKey := arr.getNextIntKey() + arr.SetInt(nextKey, value) +} + +// getNextIntKey finds the next available integer key +func (arr *Array) getNextIntKey() int64 { + maxKey := int64(-1) + for _, key := range arr.keys { + if key.Type == PHPIntKey && key.Int > maxKey { + maxKey = key.Int + } + } + + return maxKey + 1 +} + +// Len returns the number of elements in the array +func (arr *Array) Len() uint32 { + return uint32(len(arr.keys)) +} + +// At returns the key and value at the given index +func (arr *Array) At(index uint32) (PHPKey, interface{}) { + if index >= uint32(len(arr.keys)) { + return PHPKey{}, nil + } + return arr.keys[index], arr.values[index] +} + +// EXPERIMENTAL: GoArray converts a zend_array to a Go Array +func GoArray(arr unsafe.Pointer) *Array { + result := &Array{ + keys: make([]PHPKey, 0), + values: make([]interface{}, 0), + } + + if arr == nil { + return result + } + + zval := (*C.zval)(arr) + hashTable := (*C.HashTable)(castZval(zval, C.IS_ARRAY)) + + if hashTable == nil { + return result + } + + used := hashTable.nNumUsed + if htIsPacked(hashTable) { + for i := C.uint32_t(0); i < used; i++ { + v := C.get_ht_packed_data(hashTable, i) + if v != nil && C.zval_get_type(v) != C.IS_UNDEF { + value := convertZvalToGo(v) + result.SetInt(int64(i), value) + } + } + + return result + } + + for i := C.uint32_t(0); i < used; i++ { + bucket := C.get_ht_bucket_data(hashTable, i) + if bucket == nil || C.zval_get_type(&bucket.val) == C.IS_UNDEF { + continue + } + + v := convertZvalToGo(&bucket.val) + + if bucket.key != nil { + keyStr := GoString(unsafe.Pointer(bucket.key)) + result.SetString(keyStr, v) + + continue + } + + result.SetInt(int64(bucket.h), v) + } + + return result +} + +// PHPArray converts a Go Array to a PHP zend_array. +func PHPArray(arr *Array) unsafe.Pointer { + if arr == nil || arr.Len() == 0 { + return unsafe.Pointer(createNewArray(0)) + } + + isList := true + for i, k := range arr.keys { + if k.Type != PHPIntKey || k.Int != int64(i) { + isList = false + break + } + } + + var zendArray *C.HashTable + if isList { + zendArray = createNewArray(arr.Len()) + for _, v := range arr.values { + zval := convertGoToZval(v) + C.zend_hash_next_index_insert(zendArray, zval) + } + + return unsafe.Pointer(zendArray) + } + + zendArray = createNewArray(arr.Len()) + for i, k := range arr.keys { + zval := convertGoToZval(arr.values[i]) + + if k.Type == PHPStringKey { + keyStr := PHPString(k.Str, false) + C.zend_hash_update(zendArray, (*C.zend_string)(keyStr), zval) + + continue + } + + C.zend_hash_index_update(zendArray, C.zend_ulong(k.Int), zval) + } + + return unsafe.Pointer(zendArray) +} + +// convertZvalToGo converts a PHP zval to a Go interface{} +func convertZvalToGo(zval *C.zval) interface{} { + t := C.zval_get_type(zval) + switch t { + case C.IS_NULL: + return nil + case C.IS_FALSE: + return false + case C.IS_TRUE: + return true + case C.IS_LONG: + longPtr := (*C.zend_long)(castZval(zval, C.IS_LONG)) + if longPtr != nil { + return int64(*longPtr) + } + return int64(0) + case C.IS_DOUBLE: + doublePtr := (*C.double)(castZval(zval, C.IS_DOUBLE)) + if doublePtr != nil { + return float64(*doublePtr) + } + return float64(0) + case C.IS_STRING: + str := (*C.zend_string)(castZval(zval, C.IS_STRING)) + if str == nil { + return "" + } + + return GoString(unsafe.Pointer(str)) + case C.IS_ARRAY: + return GoArray(unsafe.Pointer(zval)) + default: + return nil + } +} + +// 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]) + + switch v := value.(type) { + case nil: + *u1 = C.IS_NULL + case bool: + if v { + *u1 = C.IS_TRUE + } else { + *u1 = C.IS_FALSE + } + case int: + *u1 = C.IS_LONG + *(*C.zend_long)(v0) = C.zend_long(v) + case int64: + *u1 = C.IS_LONG + *(*C.zend_long)(v0) = C.zend_long(v) + case float64: + *u1 = C.IS_DOUBLE + *(*C.double)(v0) = C.double(v) + case string: + *u1 = C.IS_STRING + *(**C.zend_string)(v0) = (*C.zend_string)(PHPString(v, false)) + case *Array: + *u1 = C.IS_ARRAY + *(**C.zend_array)(v0) = (*C.zend_array)(PHPArray(v)) + default: + *u1 = C.IS_NULL + } + + 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)) + + return (*C.HashTable)(ht) +} + +// htIsPacked checks if a HashTable is a list (packed) or hashmap (not packed). +func htIsPacked(ht *C.HashTable) bool { + flags := *(*C.uint32_t)(unsafe.Pointer(&ht.u[0])) + + return (flags & C.HASH_FLAG_PACKED) != 0 +} + +// castZval casts a zval to the expected type and returns a pointer to the value +func castZval(zval *C.zval, expectedType C.uint8_t) unsafe.Pointer { + if zval == nil || C.zval_get_type(zval) != expectedType { + return nil + } + + v := unsafe.Pointer(&zval.value[0]) + + switch expectedType { + case C.IS_LONG: + return v + case C.IS_DOUBLE: + return v + case C.IS_STRING: + return unsafe.Pointer(*(**C.zend_string)(v)) + case C.IS_ARRAY: + return unsafe.Pointer(*(**C.zend_array)(v)) + default: + return nil + } +} diff --git a/types.h b/types.h new file mode 100644 index 000000000..8df37e04a --- /dev/null +++ b/types.h @@ -0,0 +1,17 @@ +#ifndef TYPES_H +#define TYPES_H + +#include +#include +#include +#include +#include + +zval *get_ht_packed_data(HashTable *, uint32_t index); +Bucket *get_ht_bucket_data(HashTable *, uint32_t index); + +void *__emalloc__(size_t size); +void __zend_hash_init__(HashTable *ht, uint32_t nSize, dtor_func_t pDestructor, + bool persistent); + +#endif From 94672b7e02f35cc1531ea704f0f84357ec92d0fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Wed, 16 Jul 2025 12:05:03 +0200 Subject: [PATCH 2/2] cs --- types.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types.c b/types.c index b03dc2abd..3f8620566 100644 --- a/types.c +++ b/types.c @@ -19,4 +19,4 @@ void *__emalloc__(size_t size) { return emalloc(size); } void __zend_hash_init__(HashTable *ht, uint32_t nSize, dtor_func_t pDestructor, bool persistent) { zend_hash_init(ht, nSize, null, pDestructor, persistent); -} \ No newline at end of file +}