diff --git a/docs/extensions.md b/docs/extensions.md index 0efc34f6d..c87b627b9 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -81,17 +81,19 @@ While the first point speaks for itself, the second may be harder to apprehend. While some variable types have the same memory representation between C/PHP and Go, some types require more logic to be directly used. This is maybe the hardest part when it comes to writing extensions because it requires understanding internals of the Zend Engine and how variables are stored internally in PHP. This table summarizes what you need to know: -| PHP type | Go type | Direct conversion | C to Go helper | Go to C helper | Class Methods Support | -|--------------------|---------------------|-------------------|-----------------------|------------------------|-----------------------| -| `int` | `int64` | ✅ | - | - | ✅ | -| `?int` | `*int64` | ✅ | - | - | ✅ | -| `float` | `float64` | ✅ | - | - | ✅ | -| `?float` | `*float64` | ✅ | - | - | ✅ | -| `bool` | `bool` | ✅ | - | - | ✅ | -| `?bool` | `*bool` | ✅ | - | - | ✅ | -| `string`/`?string` | `*C.zend_string` | ❌ | frankenphp.GoString() | frankenphp.PHPString() | ✅ | -| `array` | `*frankenphp.Array` | ❌ | frankenphp.GoArray() | frankenphp.PHPArray() | ✅ | -| `object` | `struct` | ❌ | _Not yet implemented_ | _Not yet implemented_ | ❌ | +| PHP type | Go type | Direct conversion | C to Go helper | Go to C helper | Class Methods Support | +|--------------------|-------------------------------|-------------------|---------------------------------|----------------------------------|-----------------------| +| `int` | `int64` | ✅ | - | - | ✅ | +| `?int` | `*int64` | ✅ | - | - | ✅ | +| `float` | `float64` | ✅ | - | - | ✅ | +| `?float` | `*float64` | ✅ | - | - | ✅ | +| `bool` | `bool` | ✅ | - | - | ✅ | +| `?bool` | `*bool` | ✅ | - | - | ✅ | +| `string`/`?string` | `*C.zend_string` | ❌ | frankenphp.GoString() | frankenphp.PHPString() | ✅ | +| `array` | `frankenphp.AssociativeArray` | ❌ | frankenphp.GoAssociativeArray() | frankenphp.PHPAssociativeArray() | ✅ | +| `array` | `map[string]any` | ❌ | frankenphp.GoMap() | frankenphp.PHPMap() | ✅ | +| `array` | `[]any` | ❌ | frankenphp.GoPackedArray() | frankenphp.PHPPackedArray() | ✅ | +| `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. @@ -102,55 +104,86 @@ If you refer to the code snippet of the previous section, you can see that helpe #### 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. +FrankenPHP provides native support for PHP arrays through `frankenphp.AssociativeArray` or direct conversion to a map or slice. + +`AssociativeArray` represents a [hash map](https://en.wikipedia.org/wiki/Hash_table) composed of a `Map: map[string]any`field and an optional `Order: []string` field (unlike PHP "associative arrays", Go maps aren't ordered). + +If order or association are not needed, it's also possible to directly convert to a slice `[]any` or unordered map `map[string]any`. **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) +// export_php:function process_data_ordered(array $input): array +func process_data_ordered_map(arr *C.zval) unsafe.Pointer { + // Convert PHP associative array to Go while keeping the order + associativeArray := frankenphp.GoAssociativeArray(unsafe.Pointer(arr)) + + // loop over the entries in order + for _, key := range associativeArray.Order { + value, _ = associativeArray.Map[key] + // do something with key and value + } + + // return an ordered array + // if 'Order' is not empty, only the key-value paris in 'Order' will be respected + return frankenphp.PHPAssociativeArray(AssociativeArray{ + Map: map[string]any{ + "key1": "value1", + "key2": "value2", + }, + Order: []string{"key1", "key2"}, + }) +} + +// export_php:function process_data_unordered(array $input): array +func process_data_unordered_map(arr *C.zval) unsafe.Pointer { + // Convert PHP associative array to a Go map without keeping the order + // ignoring the order will be more performant + goMap := frankenphp.GoMap(unsafe.Pointer(arr)) + + // loop over the entries in no specific order + for key, value := range goMap { + // do something with key and value + } + + // return an unordered array + return frankenphp.PHPMap(map[string]any{ + "key1": "value1", + "key2": "value2", + }) +} + +// export_php:function process_data_packed(array $input): array +func process_data_packed(arr *C.zval) unsafe.Pointer { + // Convert PHP packed array to Go + goSlice := frankenphp.GoPackedArray(unsafe.Pointer(arr), false) + + // loop over the slice in order + for index, value := range goSlice { + // do something with index and value + } + + // return a packed array + return frankenphp.PHPackedArray([]any{"value1", "value2", "value3"}) } ``` -**Key features of `frankenphp.Array`:** +**Key features of array conversion:** -* **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 +* **Ordered key-value pairs** - Option to keep the order of the associative array +* **Optimized for multiple cases** - Option to ditch the order for better performance or convert straight to a slice * **Automatic list detection** - When converting to PHP, automatically detects if array should be a packed list or hashmap +* **Nested Arrays** - Arrays can be nested and will convert all support types automatically (`int64`,`float64`,`string`,`bool`,`nil`,`AssociativeArray`,`map[string]any`,`[]any`) * **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:** +##### Available methods: Packed and Associative -* `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 +* `frankenphp.PHPAssociativeArray(arr frankenphp.AssociativeArray) unsafe.Pointer` - Convert to an ordered PHP array with key-value pairs +* `frankenphp.PHPMap(arr map[string]any) unsafe.Pointer` - Convert a map to an unordered PHP array with key-value pairs +* `frankenphp.PHPPackedArray(slice []any) unsafe.Pointer` - Convert a slice to a PHP packed array with indexed values only +* `frankenphp.GoAssociativeArray(arr unsafe.Pointer, ordered bool) frankenphp.AssociativeArray` - Convert a PHP array to an ordered Go AssociativeArray (map with order) +* `frankenphp.GoMap(arr unsafe.Pointer) map[string]any` - Convert a PHP array to an unordered go map +* `frankenphp.GoPackedArray(arr unsafe.Pointer) []any` - Convert a PHP array to a go slice ### Declaring a Native PHP Class diff --git a/internal/extgen/gofile_test.go b/internal/extgen/gofile_test.go index 11e962f3d..ce7fe2c52 100644 --- a/internal/extgen/gofile_test.go +++ b/internal/extgen/gofile_test.go @@ -514,18 +514,17 @@ type ArrayStruct struct { } //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) +func (as *ArrayStruct) ProcessArray(items frankenphp.AssociativeArray) frankenphp.AssociativeArray { + result := frankenphp.AssociativeArray{} + for key, value := range items.Map { + result.Set("processed_"+key, 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{} +func (as *ArrayStruct) FilterData(data frankenphp.AssociativeArray, filter string) frankenphp.AssociativeArray { + result := frankenphp.AssociativeArray{} // Filter logic here return result }` @@ -543,11 +542,10 @@ func (as *ArrayStruct) FilterData(data *frankenphp.Array, filter string) *franke 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) + GoFunction: `func (as *ArrayStruct) ProcessArray(items frankenphp.AssociativeArray) frankenphp.AssociativeArray { + result := frankenphp.AssociativeArray{} + for key, value := range items.Entries() { + result.Set("processed_"+key, value) } return result }`, @@ -562,8 +560,8 @@ func (as *ArrayStruct) FilterData(data *frankenphp.Array, filter string) *franke {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{} + GoFunction: `func (as *ArrayStruct) FilterData(data frankenphp.AssociativeArray, filter string) frankenphp.AssociativeArray { + result := frankenphp.AssociativeArray{} return result }`, }, @@ -613,11 +611,8 @@ func TestGoFileGenerator_MethodWrapperWithNullableArrayParams(t *testing.T) { 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) +func (nas *NullableArrayStruct) ProcessOptionalArray(items frankenphp.AssociativeArray, name string) string { + return fmt.Sprintf("Processing %d items for %s", len(items.Map), name) }` sourceFile := filepath.Join(tmpDir, "test.go") @@ -634,11 +629,8 @@ func (nas *NullableArrayStruct) ProcessOptionalArray(items *frankenphp.Array, na {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) + GoFunction: `func (nas *NullableArrayStruct) ProcessOptionalArray(items frankenphp.AssociativeArray, name string) string { + return fmt.Sprintf("Processing %d items for %s", len(items.Map), name) }`, }, } diff --git a/threadtasks_test.go b/threadtasks_test.go new file mode 100644 index 000000000..d81c55535 --- /dev/null +++ b/threadtasks_test.go @@ -0,0 +1,90 @@ +package frankenphp + +import ( + "sync" +) + +// representation of a thread that handles tasks directly assigned by go +// implements the threadHandler interface +type taskThread struct { + thread *phpThread + execChan chan *task +} + +// task callbacks will be executed directly on the PHP thread +// therefore having full access to the PHP runtime +type task struct { + callback func() + done sync.Mutex +} + +func newTask(cb func()) *task { + t := &task{callback: cb} + t.done.Lock() + + return t +} + +func (t *task) waitForCompletion() { + t.done.Lock() +} + +func convertToTaskThread(thread *phpThread) *taskThread { + handler := &taskThread{ + thread: thread, + execChan: make(chan *task), + } + thread.setHandler(handler) + return handler +} + +func (handler *taskThread) beforeScriptExecution() string { + thread := handler.thread + + switch thread.state.get() { + case stateTransitionRequested: + return thread.transitionToNewHandler() + case stateBooting, stateTransitionComplete: + thread.state.set(stateReady) + handler.waitForTasks() + + return handler.beforeScriptExecution() + case stateReady: + handler.waitForTasks() + + return handler.beforeScriptExecution() + case stateShuttingDown: + // signal to stop + return "" + } + panic("unexpected state: " + thread.state.name()) +} + +func (handler *taskThread) afterScriptExecution(int) { + panic("task threads should not execute scripts") +} + +func (handler *taskThread) getRequestContext() *frankenPHPContext { + return nil +} + +func (handler *taskThread) name() string { + return "Task PHP Thread" +} + +func (handler *taskThread) waitForTasks() { + for { + select { + case task := <-handler.execChan: + task.callback() + task.done.Unlock() // unlock the task to signal completion + case <-handler.thread.drainChan: + // thread is shutting down, do not execute the function + return + } + } +} + +func (handler *taskThread) execute(t *task) { + handler.execChan <- t +} diff --git a/types.go b/types.go index 02044bc88..4fe2352a4 100644 --- a/types.go +++ b/types.go @@ -4,7 +4,10 @@ package frankenphp #include "types.h" */ import "C" -import "unsafe" +import ( + "strconv" + "unsafe" +) // EXPERIMENTAL: GoString copies a zend_string to a Go string. func GoString(s unsafe.Pointer) string { @@ -34,101 +37,62 @@ 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 +// AssociativeArray represents a PHP array with ordered key-value pairs +type AssociativeArray struct { + Map map[string]any + Order []string } -// Len returns the number of elements in the array -func (arr *Array) Len() uint32 { - return uint32(len(arr.keys)) +// EXPERIMENTAL: GoAssociativeArray converts a zend_array to a Go AssociativeArray +func GoAssociativeArray(arr unsafe.Pointer) AssociativeArray { + entries, order := goArray(arr, true) + return AssociativeArray{entries, order} } -// 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: GoMap converts a zend_array to an unordered Go map +func GoMap(arr unsafe.Pointer) map[string]any { + entries, _ := goArray(arr, false) + return entries } -// 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), - } - +func goArray(arr unsafe.Pointer, ordered bool) (map[string]any, []string) { if arr == nil { - return result + panic("received a nil pointer on array conversion") } zval := (*C.zval)(arr) hashTable := (*C.HashTable)(castZval(zval, C.IS_ARRAY)) if hashTable == nil { - return result + panic("received a *zval that wasn't a HashTable on array conversion") + } + + nNumUsed := hashTable.nNumUsed + entries := make(map[string]any) + var order []string + if ordered { + order = make([]string, 0, nNumUsed) } - used := hashTable.nNumUsed if htIsPacked(hashTable) { - for i := C.uint32_t(0); i < used; i++ { + // if the HashTable is packed, convert all integer keys to strings + // this is probably a bug by the dev using this function + // still, we'll (inefficiently) convert to an associative array + for i := C.uint32_t(0); i < nNumUsed; 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) + strIndex := strconv.Itoa(int(i)) + entries[strIndex] = convertZvalToGo(v) + if ordered { + order = append(order, strIndex) + } } } - return result + return entries, order } - for i := C.uint32_t(0); i < used; i++ { + for i := C.uint32_t(0); i < nNumUsed; i++ { bucket := C.get_ht_bucket_data(hashTable, i) if bucket == nil || C.zval_get_type(&bucket.val) == C.IS_UNDEF { continue @@ -138,62 +102,113 @@ func GoArray(arr unsafe.Pointer) *Array { if bucket.key != nil { keyStr := GoString(unsafe.Pointer(bucket.key)) - result.SetString(keyStr, v) + entries[keyStr] = v + if ordered { + order = append(order, keyStr) + } continue } - result.SetInt(int64(bucket.h), v) + // as fallback convert the bucket index to a string key + strIndex := strconv.Itoa(int(bucket.h)) + entries[strIndex] = v + if ordered { + order = append(order, strIndex) + } } - return result + return entries, order } -// 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)) +// EXPERIMENTAL: GoPackedArray converts a zend_array to a Go slice +func GoPackedArray(arr unsafe.Pointer) []any { + if arr == nil { + panic("GoPackedArray received a nil pointer") } - isList := true - for i, k := range arr.keys { - if k.Type != PHPIntKey || k.Int != int64(i) { - isList = false - break - } + zval := (*C.zval)(arr) + hashTable := (*C.HashTable)(castZval(zval, C.IS_ARRAY)) + + if hashTable == nil { + panic("GoPackedArray received *zval that wasn't a HashTable") } - 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) + nNumUsed := hashTable.nNumUsed + result := make([]any, 0, nNumUsed) + + if htIsPacked(hashTable) { + for i := C.uint32_t(0); i < nNumUsed; i++ { + v := C.get_ht_packed_data(hashTable, i) + if v != nil && C.zval_get_type(v) != C.IS_UNDEF { + result = append(result, convertZvalToGo(v)) + } } - return unsafe.Pointer(zendArray) + return result } - zendArray = createNewArray(arr.Len()) - for i, k := range arr.keys { - zval := convertGoToZval(arr.values[i]) + // fallback if ht isn't packed - equivalent to array_values() + for i := C.uint32_t(0); i < nNumUsed; i++ { + bucket := C.get_ht_bucket_data(hashTable, i) + if bucket != nil && C.zval_get_type(&bucket.val) != C.IS_UNDEF { + result = append(result, convertZvalToGo(&bucket.val)) + } + } - if k.Type == PHPStringKey { - keyStr := k.Str - keyData := (*C.char)(unsafe.Pointer(unsafe.StringData(keyStr))) - C.zend_hash_str_add(zendArray, keyData, C.size_t(len(keyStr)), zval) + return result +} - continue +// EXPERIMENTAL: PHPMap converts an unordered Go map to a PHP zend_array. +func PHPMap(arr map[string]any) unsafe.Pointer { + return phpArray(arr, nil) +} + +// EXPERIMENTAL: PHPAssociativeArray converts a Go AssociativeArray to a PHP zend_array. +func PHPAssociativeArray(arr AssociativeArray) unsafe.Pointer { + return phpArray(arr.Map, arr.Order) +} + +func phpArray(entries map[string]any, order []string) unsafe.Pointer { + var zendArray *C.HashTable + + if len(order) != 0 { + zendArray = createNewArray((uint32)(len(order))) + for _, key := range order { + val := entries[key] + zval := convertGoToZval(val) + C.zend_hash_str_update(zendArray, toUnsafeChar(key), C.size_t(len(key)), zval) + } + } else { + zendArray = createNewArray((uint32)(len(entries))) + for key, val := range entries { + zval := convertGoToZval(val) + C.zend_hash_str_update(zendArray, toUnsafeChar(key), C.size_t(len(key)), zval) } + } - C.zend_hash_index_update(zendArray, C.zend_ulong(k.Int), zval) + var zval C.zval + C.__zval_arr__(&zval, zendArray) + + return unsafe.Pointer(&zval) +} + +// EXPERIMENTAL: PHPPackedArray converts a Go slice to a PHP zend_array. +func PHPPackedArray(slice []any) unsafe.Pointer { + zendArray := createNewArray((uint32)(len(slice))) + for _, val := range slice { + zval := convertGoToZval(val) + C.zend_hash_next_index_insert(zendArray, zval) } - return unsafe.Pointer(zendArray) + var zval C.zval + C.__zval_arr__(&zval, zendArray) + + return unsafe.Pointer(&zval) } -// convertZvalToGo converts a PHP zval to a Go interface{} -func convertZvalToGo(zval *C.zval) interface{} { +// convertZvalToGo converts a PHP zval to a Go any +func convertZvalToGo(zval *C.zval) any { t := C.zval_get_type(zval) switch t { case C.IS_NULL: @@ -222,14 +237,19 @@ func convertZvalToGo(zval *C.zval) interface{} { return GoString(unsafe.Pointer(str)) case C.IS_ARRAY: - return GoArray(unsafe.Pointer(zval)) + hashTable := (*C.HashTable)(castZval(zval, C.IS_ARRAY)) + if hashTable != nil && htIsPacked(hashTable) { + return GoPackedArray(unsafe.Pointer(zval)) + } + + return GoAssociativeArray(unsafe.Pointer(zval)) default: return nil } } -// convertGoToZval converts a Go interface{} to a PHP zval -func convertGoToZval(value interface{}) *C.zval { +// convertGoToZval converts a Go any to a PHP zval +func convertGoToZval(value any) *C.zval { var zval C.zval switch v := value.(type) { @@ -246,9 +266,12 @@ func convertGoToZval(value interface{}) *C.zval { case string: str := (*C.zend_string)(PHPString(v, false)) C.__zval_string__(&zval, str) - case *Array: - arr := (*C.zend_array)(PHPArray(v)) - C.__zval_arr__(&zval, arr) + case AssociativeArray: + return (*C.zval)(PHPAssociativeArray(v)) + case map[string]any: + return (*C.zval)(PHPAssociativeArray(AssociativeArray{Map: v})) + case []any: + return (*C.zval)(PHPPackedArray(v)) default: C.__zval_null__(&zval) } diff --git a/types_test.go b/types_test.go index be4559a4f..9499301f6 100644 --- a/types_test.go +++ b/types_test.go @@ -1,7 +1,127 @@ package frankenphp -import "testing" +import ( + "io" + "log/slog" + "testing" + + "github.com/stretchr/testify/assert" +) + +// execute the function on a PHP thread directly +// this is necessary if tests make use of PHP's internal allocation +func testOnDummyPHPThread(t *testing.T, test func()) { + t.Helper() + logger = slog.New(slog.NewTextHandler(io.Discard, nil)) + _, err := initPHPThreads(1, 1, nil) // boot 1 thread + assert.NoError(t, err) + handler := convertToTaskThread(phpThreads[0]) + + task := newTask(test) + handler.execute(task) + task.waitForCompletion() + + drainPHPThreads() +} func TestGoString(t *testing.T) { - testGoString(t) + testOnDummyPHPThread(t, func() { + originalString := "Hello, World!" + + convertedString := GoString(PHPString(originalString, false)) + + assert.Equal(t, originalString, convertedString, "string -> zend_string -> string should yield an equal string") + }) +} + +func TestPHPMap(t *testing.T) { + testOnDummyPHPThread(t, func() { + originalMap := map[string]any{ + "foo1": "bar1", + "foo2": "bar2", + } + + convertedMap := GoMap(PHPMap(originalMap)) + + assert.Equal(t, originalMap, convertedMap, "associative array should be equal after conversion") + }) +} + +func TestOrderedPHPAssociativeArray(t *testing.T) { + testOnDummyPHPThread(t, func() { + originalArray := AssociativeArray{ + Map: map[string]any{ + "foo1": "bar1", + "foo2": "bar2", + }, + Order: []string{"foo2", "foo1"}, + } + + convertedArray := GoAssociativeArray(PHPAssociativeArray(originalArray)) + + assert.Equal(t, originalArray, convertedArray, "associative array should be equal after conversion") + }) +} + +func TestPHPPackedArray(t *testing.T) { + testOnDummyPHPThread(t, func() { + originalSlice := []any{"bar1", "bar2"} + + convertedSlice := GoPackedArray(PHPPackedArray(originalSlice)) + + assert.Equal(t, originalSlice, convertedSlice, "slice should be equal after conversion") + }) +} + +func TestPHPPackedArrayToGoMap(t *testing.T) { + testOnDummyPHPThread(t, func() { + originalSlice := []any{"bar1", "bar2"} + expectedMap := map[string]any{ + "0": "bar1", + "1": "bar2", + } + + convertedMap := GoMap(PHPPackedArray(originalSlice)) + + assert.Equal(t, expectedMap, convertedMap, "convert a packed to an associative array") + }) +} + +func TestPHPAssociativeArrayToPacked(t *testing.T) { + testOnDummyPHPThread(t, func() { + originalArray := AssociativeArray{ + Map: map[string]any{ + "foo1": "bar1", + "foo2": "bar2", + }, + Order: []string{"foo1", "foo2"}, + } + expectedSlice := []any{"bar1", "bar2"} + + convertedSlice := GoPackedArray(PHPAssociativeArray(originalArray)) + + assert.Equal(t, expectedSlice, convertedSlice, "convert an associative array to a slice") + }) +} + +func TestNestedMixedArray(t *testing.T) { + testOnDummyPHPThread(t, func() { + originalArray := map[string]any{ + "string": "value", + "int": int64(123), + "float": float64(1.2), + "true": true, + "false": false, + "nil": nil, + "packedArray": []any{"bar1", "bar2"}, + "associativeArray": AssociativeArray{ + Map: map[string]any{"foo1": "bar1", "foo2": "bar2"}, + Order: []string{"foo2", "foo1"}, + }, + } + + convertedArray := GoMap(PHPMap(originalArray)) + + assert.Equal(t, originalArray, convertedArray, "nested mixed array should be equal after conversion") + }) } diff --git a/typestest.go b/typestest.go deleted file mode 100644 index 178dae220..000000000 --- a/typestest.go +++ /dev/null @@ -1,18 +0,0 @@ -package frankenphp - -//#include -// -//zend_string *hello_string() { -// return zend_string_init("Hello", 5, 1); -//} -import "C" -import ( - "github.com/stretchr/testify/assert" - "testing" - "unsafe" -) - -func testGoString(t *testing.T) { - assert.Equal(t, "", GoString(nil)) - assert.Equal(t, "Hello", GoString(unsafe.Pointer(C.hello_string()))) -}