Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions docs/extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ If order or association are not needed, it's also possible to directly convert t

```go
// export_php:function process_data_ordered(array $input): array
func process_data_ordered_map(arr *C.zval) unsafe.Pointer {
func process_data_ordered_map(arr *C.HashTable) unsafe.Pointer {
// Convert PHP associative array to Go while keeping the order
associativeArray := frankenphp.GoAssociativeArray(unsafe.Pointer(arr))

Expand All @@ -126,7 +126,7 @@ func process_data_ordered_map(arr *C.zval) unsafe.Pointer {
// do something with key and value
}

// return an ordered array
// return an ordered HashTable
// if 'Order' is not empty, only the key-value pairs in 'Order' will be respected
return frankenphp.PHPAssociativeArray(AssociativeArray{
Map: map[string]any{
Expand All @@ -138,7 +138,7 @@ func process_data_ordered_map(arr *C.zval) unsafe.Pointer {
}

// export_php:function process_data_unordered(array $input): array
func process_data_unordered_map(arr *C.zval) unsafe.Pointer {
func process_data_unordered_map(arr *C.HashTable) 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))
Expand All @@ -148,15 +148,15 @@ func process_data_unordered_map(arr *C.zval) unsafe.Pointer {
// do something with key and value
}

// return an unordered array
// return an unordered HashTable
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 {
func process_data_packed(arr *C.HashTable) unsafe.Pointer {
// Convert PHP packed array to Go
goSlice := frankenphp.GoPackedArray(unsafe.Pointer(arr), false)

Expand All @@ -165,7 +165,7 @@ func process_data_packed(arr *C.zval) unsafe.Pointer {
// do something with index and value
}

// return a packed array
// return a packed HashTable
return frankenphp.PHPackedArray([]any{"value1", "value2", "value3"})
}
```
Expand All @@ -180,9 +180,9 @@ func process_data_packed(arr *C.zval) unsafe.Pointer {

##### Available methods: Packed and Associative

- `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.PHPAssociativeArray(arr frankenphp.AssociativeArray) unsafe.Pointer` - Convert to an ordered PHP HashTable with key-value pairs
- `frankenphp.PHPMap(arr map[string]any) unsafe.Pointer` - Convert a map to an unordered PHP HashTable with key-value pairs
- `frankenphp.PHPPackedArray(slice []any) unsafe.Pointer` - Convert a slice to a PHP packed HashTable 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
Expand Down
12 changes: 6 additions & 6 deletions internal/extgen/templates/extension.c.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -113,19 +113,19 @@ PHP_METHOD({{namespacedClassName $.Namespace .ClassName}}, {{.PhpName}}) {

{{- if ne .ReturnType "void"}}
{{- if eq .ReturnType "string"}}
zend_string* result = {{.Name}}_wrapper(intern->go_handle{{if .Params}}{{range .Params}}, {{if .IsNullable}}{{if eq .PhpType "string"}}{{.Name}}_is_null ? NULL : {{.Name}}{{else if eq .PhpType "int"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "float"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "bool"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "array"}}{{.Name}}{{end}}{{else}}{{.Name}}{{end}}{{end}}{{end}});
zend_string* result = {{.Name}}_wrapper(intern->go_handle{{if .Params}}{{range .Params}}, {{if .IsNullable}}{{if eq .PhpType "string"}}{{.Name}}_is_null ? NULL : {{.Name}}{{else if eq .PhpType "int"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "float"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "bool"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "array"}}{{.Name}} ? Z_ARRVAL_P({{.Name}}) : NULL{{end}}{{else}}{{if eq .PhpType "array"}}Z_ARRVAL_P({{.Name}}){{else}}{{.Name}}{{end}}{{end}}{{end}}{{end}});
RETURN_STR(result);
{{- else if eq .ReturnType "int"}}
zend_long result = {{.Name}}_wrapper(intern->go_handle{{if .Params}}{{range .Params}}, {{if .IsNullable}}{{if eq .PhpType "string"}}{{.Name}}_is_null ? NULL : {{.Name}}{{else if eq .PhpType "int"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "float"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "bool"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "array"}}{{.Name}}{{end}}{{else}}{{if eq .PhpType "array"}}{{.Name}}{{else}}(long){{.Name}}{{end}}{{end}}{{end}}{{end}});
zend_long result = {{.Name}}_wrapper(intern->go_handle{{if .Params}}{{range .Params}}, {{if .IsNullable}}{{if eq .PhpType "string"}}{{.Name}}_is_null ? NULL : {{.Name}}{{else if eq .PhpType "int"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "float"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "bool"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "array"}}{{.Name}} ? Z_ARRVAL_P({{.Name}}) : NULL{{end}}{{else}}{{if eq .PhpType "array"}}Z_ARRVAL_P({{.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}}{{else if eq .PhpType "array"}}{{.Name}}{{end}}{{else}}{{if eq .PhpType "array"}}{{.Name}}{{else}}(double){{.Name}}{{end}}{{end}}{{end}}{{end}});
double result = {{.Name}}_wrapper(intern->go_handle{{if .Params}}{{range .Params}}, {{if .IsNullable}}{{if eq .PhpType "string"}}{{.Name}}_is_null ? NULL : {{.Name}}{{else if eq .PhpType "int"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "float"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "bool"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "array"}}{{.Name}} ? Z_ARRVAL_P({{.Name}}) : NULL{{end}}{{else}}{{if eq .PhpType "array"}}Z_ARRVAL_P({{.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}}{{else if eq .PhpType "array"}}{{.Name}}{{end}}{{else}}{{if eq .PhpType "array"}}{{.Name}}{{else}}(int){{.Name}}{{end}}{{end}}{{end}}{{end}});
int result = {{.Name}}_wrapper(intern->go_handle{{if .Params}}{{range .Params}}, {{if .IsNullable}}{{if eq .PhpType "string"}}{{.Name}}_is_null ? NULL : {{.Name}}{{else if eq .PhpType "int"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "float"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "bool"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "array"}}{{.Name}} ? Z_ARRVAL_P({{.Name}}) : NULL{{end}}{{else}}{{if eq .PhpType "array"}}Z_ARRVAL_P({{.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}});
void* result = {{.Name}}_wrapper(intern->go_handle{{if .Params}}{{range .Params}}, {{if .IsNullable}}{{if eq .PhpType "string"}}{{.Name}}_is_null ? NULL : {{.Name}}{{else if eq .PhpType "int"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "float"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "bool"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "array"}}{{.Name}} ? Z_ARRVAL_P({{.Name}}) : NULL{{end}}{{else}}{{if eq .PhpType "array"}}Z_ARRVAL_P({{.Name}}){{else}}{{.Name}}{{end}}{{end}}{{end}}{{end}});
if (result != NULL) {
HashTable *ht = (HashTable*)result;
RETURN_ARR(ht);
Expand All @@ -134,7 +134,7 @@ PHP_METHOD({{namespacedClassName $.Namespace .ClassName}}, {{.PhpName}}) {
}
{{- end}}
{{- else}}
{{.Name}}_wrapper(intern->go_handle{{if .Params}}{{range .Params}}, {{if .IsNullable}}{{if eq .PhpType "string"}}{{.Name}}_is_null ? NULL : {{.Name}}{{else if eq .PhpType "int"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "float"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "bool"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "array"}}{{.Name}}{{end}}{{else}}{{if eq .PhpType "string"}}{{.Name}}{{else if eq .PhpType "int"}}(long){{.Name}}{{else if eq .PhpType "float"}}(double){{.Name}}{{else if eq .PhpType "bool"}}(int){{.Name}}{{else if eq .PhpType "array"}}{{.Name}}{{end}}{{end}}{{end}}{{end}});
{{.Name}}_wrapper(intern->go_handle{{if .Params}}{{range .Params}}, {{if .IsNullable}}{{if eq .PhpType "string"}}{{.Name}}_is_null ? NULL : {{.Name}}{{else if eq .PhpType "int"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "float"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "bool"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "array"}}{{.Name}} ? Z_ARRVAL_P({{.Name}}) : NULL{{end}}{{else}}{{if eq .PhpType "string"}}{{.Name}}{{else if eq .PhpType "int"}}(long){{.Name}}{{else if eq .PhpType "float"}}(double){{.Name}}{{else if eq .PhpType "bool"}}(int){{.Name}}{{else if eq .PhpType "array"}}Z_ARRVAL_P({{.Name}}){{end}}{{end}}{{end}}{{end}});
{{- end}}
}
{{end}}{{end}}
Expand Down
64 changes: 40 additions & 24 deletions types.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
package frankenphp

/*
#cgo nocallback __zend_new_array__
#cgo nocallback __zval_null__
#cgo nocallback __zval_bool__
#cgo nocallback __zval_long__
#cgo nocallback __zval_double__
#cgo nocallback __zval_string__
#cgo nocallback __zval_arr__
#cgo noescape __zend_new_array__
#cgo noescape __zval_null__
#cgo noescape __zval_bool__
#cgo noescape __zval_long__
#cgo noescape __zval_double__
#cgo noescape __zval_string__
#cgo noescape __zval_arr__
#include "types.h"
*/
import "C"
Expand Down Expand Up @@ -43,13 +57,13 @@ type AssociativeArray struct {
Order []string
}

// EXPERIMENTAL: GoAssociativeArray converts a zend_array to a Go AssociativeArray
// EXPERIMENTAL: GoAssociativeArray converts a PHP HashTable to a Go AssociativeArray
func GoAssociativeArray(arr unsafe.Pointer) AssociativeArray {
entries, order := goArray(arr, true)
return AssociativeArray{entries, order}
}

// EXPERIMENTAL: GoMap converts a zval having a zend_array value to an unordered Go map
// EXPERIMENTAL: GoMap converts a PHP HashTable to an unordered Go map
func GoMap(arr unsafe.Pointer) map[string]any {
entries, _ := goArray(arr, false)
return entries
Expand All @@ -60,11 +74,10 @@ func goArray(arr unsafe.Pointer, ordered bool) (map[string]any, []string) {
panic("received a nil pointer on array conversion")
}

zval := (*C.zval)(arr)
hashTable := (*C.HashTable)(extractZvalValue(zval, C.IS_ARRAY))
hashTable := (*C.HashTable)(arr)

if hashTable == nil {
panic("received a *zval that wasn't a HashTable on array conversion")
panic("received a pointer that wasn't a HashTable on array conversion")
}

nNumUsed := hashTable.nNumUsed
Expand Down Expand Up @@ -121,17 +134,16 @@ func goArray(arr unsafe.Pointer, ordered bool) (map[string]any, []string) {
return entries, order
}

// EXPERIMENTAL: GoPackedArray converts a zval with a zend_array value to a Go slice
// EXPERIMENTAL: GoPackedArray converts a PHP HashTable to a Go slice
func GoPackedArray(arr unsafe.Pointer) []any {
if arr == nil {
panic("GoPackedArray received a nil pointer")
}

zval := (*C.zval)(arr)
hashTable := (*C.HashTable)(extractZvalValue(zval, C.IS_ARRAY))
hashTable := (*C.HashTable)(arr)

if hashTable == nil {
panic("GoPackedArray received *zval that wasn't a HashTable")
panic("GoPackedArray received a pointer that wasn't a HashTable")
}

nNumUsed := hashTable.nNumUsed
Expand Down Expand Up @@ -164,7 +176,7 @@ func PHPMap(arr map[string]any) unsafe.Pointer {
return phpArray(arr, nil)
}

// EXPERIMENTAL: PHPAssociativeArray converts a Go AssociativeArray to a PHP zval with a zend_array value
// EXPERIMENTAL: PHPAssociativeArray converts a Go AssociativeArray to a PHP zend_array
func PHPAssociativeArray(arr AssociativeArray) unsafe.Pointer {
return phpArray(arr.Map, arr.Order)
}
Expand All @@ -187,24 +199,18 @@ func phpArray(entries map[string]any, order []string) unsafe.Pointer {
}
}

var zval C.zval
C.__zval_arr__(&zval, zendArray)

return unsafe.Pointer(&zval)
return unsafe.Pointer(zendArray)
}

// EXPERIMENTAL: PHPPackedArray converts a Go slice to a PHP zval with a zend_array value.
// 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 := phpValue(val)
C.zend_hash_next_index_insert(zendArray, zval)
}

var zval C.zval
C.__zval_arr__(&zval, zendArray)

return unsafe.Pointer(&zval)
return unsafe.Pointer(zendArray)
}

// EXPERIMENTAL: GoValue converts a PHP zval to a Go value
Expand Down Expand Up @@ -246,10 +252,10 @@ func goValue(zval *C.zval) any {
case C.IS_ARRAY:
hashTable := (*C.HashTable)(extractZvalValue(zval, C.IS_ARRAY))
if hashTable != nil && htIsPacked(hashTable) {
return GoPackedArray(unsafe.Pointer(zval))
return GoPackedArray(unsafe.Pointer(hashTable))
}

return GoAssociativeArray(unsafe.Pointer(zval))
return GoAssociativeArray(unsafe.Pointer(hashTable))
default:
return nil
}
Expand Down Expand Up @@ -278,11 +284,11 @@ func phpValue(value any) *C.zval {
str := (*C.zend_string)(PHPString(v, false))
C.__zval_string__(&zval, str)
case AssociativeArray:
return (*C.zval)(PHPAssociativeArray(v))
C.__zval_arr__(&zval, (*C.HashTable)(PHPAssociativeArray(v)))
case map[string]any:
return (*C.zval)(PHPAssociativeArray(AssociativeArray{Map: v}))
C.__zval_arr__(&zval, (*C.HashTable)(PHPMap(v)))
case []any:
return (*C.zval)(PHPPackedArray(v))
C.__zval_arr__(&zval, (*C.HashTable)(PHPPackedArray(v)))
default:
C.__zval_null__(&zval)
}
Expand Down Expand Up @@ -324,3 +330,13 @@ func extractZvalValue(zval *C.zval, expectedType C.uint8_t) unsafe.Pointer {
return nil
}
}

func zendStringRelease(p unsafe.Pointer) {
zs := (*C.zend_string)(p)
C.zend_string_release(zs)
}

func zendHashDestroy(p unsafe.Pointer) {
ht := (*C.HashTable)(p)
C.zend_hash_destroy(ht)
}
35 changes: 21 additions & 14 deletions types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@ func TestGoString(t *testing.T) {
testOnDummyPHPThread(t, func() {
originalString := "Hello, World!"

convertedString := GoString(PHPString(originalString, false))
phpString := PHPString(originalString, false)
defer zendStringRelease(phpString)

assert.Equal(t, originalString, convertedString, "string -> zend_string -> string should yield an equal string")
assert.Equal(t, originalString, GoString(phpString), "string -> zend_string -> string should yield an equal string")
})
}

Expand All @@ -41,9 +42,10 @@ func TestPHPMap(t *testing.T) {
"foo2": "bar2",
}

convertedMap := GoMap(PHPMap(originalMap))
phpArray := PHPMap(originalMap)
defer zendHashDestroy(phpArray)

assert.Equal(t, originalMap, convertedMap, "associative array should be equal after conversion")
assert.Equal(t, originalMap, GoMap(phpArray), "associative array should be equal after conversion")
})
}

Expand All @@ -57,19 +59,21 @@ func TestOrderedPHPAssociativeArray(t *testing.T) {
Order: []string{"foo2", "foo1"},
}

convertedArray := GoAssociativeArray(PHPAssociativeArray(originalArray))
phpArray := PHPAssociativeArray(originalArray)
defer zendHashDestroy(phpArray)

assert.Equal(t, originalArray, convertedArray, "associative array should be equal after conversion")
assert.Equal(t, originalArray, GoAssociativeArray(phpArray), "associative array should be equal after conversion")
})
}

func TestPHPPackedArray(t *testing.T) {
testOnDummyPHPThread(t, func() {
originalSlice := []any{"bar1", "bar2"}

convertedSlice := GoPackedArray(PHPPackedArray(originalSlice))
phpArray := PHPPackedArray(originalSlice)
defer zendHashDestroy(phpArray)

assert.Equal(t, originalSlice, convertedSlice, "slice should be equal after conversion")
assert.Equal(t, originalSlice, GoPackedArray(phpArray), "slice should be equal after conversion")
})
}

Expand All @@ -81,9 +85,10 @@ func TestPHPPackedArrayToGoMap(t *testing.T) {
"1": "bar2",
}

convertedMap := GoMap(PHPPackedArray(originalSlice))
phpArray := PHPPackedArray(originalSlice)
defer zendHashDestroy(phpArray)

assert.Equal(t, expectedMap, convertedMap, "convert a packed to an associative array")
assert.Equal(t, expectedMap, GoMap(phpArray), "convert a packed to an associative array")
})
}

Expand All @@ -98,9 +103,10 @@ func TestPHPAssociativeArrayToPacked(t *testing.T) {
}
expectedSlice := []any{"bar1", "bar2"}

convertedSlice := GoPackedArray(PHPAssociativeArray(originalArray))
phpArray := PHPAssociativeArray(originalArray)
defer zendHashDestroy(phpArray)

assert.Equal(t, expectedSlice, convertedSlice, "convert an associative array to a slice")
assert.Equal(t, expectedSlice, GoPackedArray(phpArray), "convert an associative array to a slice")
})
}

Expand All @@ -120,8 +126,9 @@ func TestNestedMixedArray(t *testing.T) {
},
}

convertedArray := GoMap(PHPMap(originalArray))
phpArray := PHPMap(originalArray)
defer zendHashDestroy(phpArray)

assert.Equal(t, originalArray, convertedArray, "nested mixed array should be equal after conversion")
assert.Equal(t, originalArray, GoMap(phpArray), "nested mixed array should be equal after conversion")
})
}