Files
frankenphp/types.go

526 lines
13 KiB
Go

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"
import (
"errors"
"fmt"
"reflect"
"strconv"
"unsafe"
)
type toZval interface {
toZval(*C.zval)
}
// EXPERIMENTAL: GoString copies a zend_string to a Go string.
func GoString(s unsafe.Pointer) string {
if s == nil {
return ""
}
zendStr := (*C.zend_string)(s)
return C.GoStringN((*C.char)(unsafe.Pointer(&zendStr.val)), C.int(zendStr.len))
}
// EXPERIMENTAL: PHPString converts a Go string to a zend_string with copy. The string can be
// non-persistent (automatically freed after the request by the ZMM) or persistent. If you choose
// the second mode, it is your repsonsability to free the allocated memory.
func PHPString(s string, persistent bool) unsafe.Pointer {
if s == "" {
return nil
}
zendStr := C.zend_string_init(
(*C.char)(unsafe.Pointer(unsafe.StringData(s))),
C.size_t(len(s)),
C._Bool(persistent),
)
return unsafe.Pointer(zendStr)
}
// AssociativeArray represents a PHP array with ordered key-value pairs
type AssociativeArray[T any] struct {
Map map[string]T
Order []string
}
func (a AssociativeArray[T]) toZval(zval *C.zval) {
C.__zval_arr__(zval, (*C.zend_array)(PHPAssociativeArray[T](a)))
}
// EXPERIMENTAL: GoAssociativeArray converts a zend_array to a Go AssociativeArray
func GoAssociativeArray[T any](arr unsafe.Pointer) (AssociativeArray[T], error) {
entries, order, err := goArray[T](arr, true)
return AssociativeArray[T]{entries, order}, err
}
// EXPERIMENTAL: GoMap converts a zend_array to an unordered Go map
func GoMap[T any](arr unsafe.Pointer) (map[string]T, error) {
entries, _, err := goArray[T](arr, false)
return entries, err
}
func goArray[T any](arr unsafe.Pointer, ordered bool) (map[string]T, []string, error) {
if arr == nil {
return nil, nil, errors.New("received a nil pointer on array conversion")
}
array := (*C.zend_array)(arr)
if array == nil {
return nil, nil, fmt.Errorf("received a *zval that wasn't a HashTable on array conversion")
}
nNumUsed := array.nNumUsed
entries := make(map[string]T, nNumUsed)
var order []string
if ordered {
order = make([]string, 0, nNumUsed)
}
if htIsPacked(array) {
// if the array 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(array, i)
if v != nil && C.zval_get_type(v) != C.IS_UNDEF {
strIndex := strconv.Itoa(int(i))
e, err := goValue[T](v)
if err != nil {
return nil, nil, err
}
entries[strIndex] = e
if ordered {
order = append(order, strIndex)
}
}
}
return entries, order, nil
}
var zeroVal T
for i := C.uint32_t(0); i < nNumUsed; i++ {
bucket := C.get_ht_bucket_data(array, i)
if bucket == nil || C.zval_get_type(&bucket.val) == C.IS_UNDEF {
continue
}
v, err := goValue[any](&bucket.val)
if err != nil {
return nil, nil, err
}
if bucket.key != nil {
keyStr := GoString(unsafe.Pointer(bucket.key))
if v == nil {
entries[keyStr] = zeroVal
} else {
entries[keyStr] = v.(T)
}
if ordered {
order = append(order, keyStr)
}
continue
}
// as fallback convert the bucket index to a string key
strIndex := strconv.Itoa(int(bucket.h))
entries[strIndex] = v.(T)
if ordered {
order = append(order, strIndex)
}
}
return entries, order, nil
}
// EXPERIMENTAL: GoPackedArray converts a zend_array to a Go slice
func GoPackedArray[T any](arr unsafe.Pointer) ([]T, error) {
if arr == nil {
return nil, errors.New("GoPackedArray received a nil value")
}
array := (*C.zend_array)(arr)
if array == nil {
return nil, fmt.Errorf("GoPackedArray received *zval that wasn't a HashTable")
}
nNumUsed := array.nNumUsed
result := make([]T, 0, nNumUsed)
if htIsPacked(array) {
for i := C.uint32_t(0); i < nNumUsed; i++ {
v := C.get_ht_packed_data(array, i)
if v != nil && C.zval_get_type(v) != C.IS_UNDEF {
v, err := goValue[T](v)
if err != nil {
return nil, err
}
result = append(result, v)
}
}
return result, nil
}
// 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(array, i)
if bucket != nil && C.zval_get_type(&bucket.val) != C.IS_UNDEF {
v, err := goValue[T](&bucket.val)
if err != nil {
return nil, err
}
result = append(result, v)
}
}
return result, nil
}
// EXPERIMENTAL: PHPMap converts an unordered Go map to a zend_array
func PHPMap[T any](arr map[string]T) unsafe.Pointer {
return phpArray[T](arr, nil)
}
// EXPERIMENTAL: PHPAssociativeArray converts a Go AssociativeArray to a zend_array
func PHPAssociativeArray[T any](arr AssociativeArray[T]) unsafe.Pointer {
return phpArray[T](arr.Map, arr.Order)
}
func phpArray[T any](entries map[string]T, order []string) unsafe.Pointer {
var zendArray *C.zend_array
if len(order) != 0 {
zendArray = createNewArray((uint32)(len(order)))
for _, key := range order {
val := entries[key]
zval := phpValue(val)
C.zend_hash_str_update(zendArray, toUnsafeChar(key), C.size_t(len(key)), zval)
C.__efree__(unsafe.Pointer(zval))
}
} else {
zendArray = createNewArray((uint32)(len(entries)))
for key, val := range entries {
zval := phpValue(val)
C.zend_hash_str_update(zendArray, toUnsafeChar(key), C.size_t(len(key)), zval)
C.__efree__(unsafe.Pointer(zval))
}
}
return unsafe.Pointer(zendArray)
}
// EXPERIMENTAL: PHPPackedArray converts a Go slice to a PHP zval with a zend_array value.
func PHPPackedArray[T any](slice []T) unsafe.Pointer {
zendArray := createNewArray((uint32)(len(slice)))
for _, val := range slice {
zval := phpValue(val)
C.zend_hash_next_index_insert(zendArray, zval)
C.__efree__(unsafe.Pointer(zval))
}
return unsafe.Pointer(zendArray)
}
// EXPERIMENTAL: GoValue converts a PHP zval to a Go value
//
// Zval having the null, bool, long, double, string and array types are currently supported.
// Arrays can curently only be converted to any[] and AssociativeArray[any].
// Any other type will cause an error.
// More types may be supported in the future.
func GoValue[T any](zval unsafe.Pointer) (T, error) {
return goValue[T]((*C.zval)(zval))
}
func goValue[T any](zval *C.zval) (res T, err error) {
var (
resAny any
resZero T
)
t := C.zval_get_type(zval)
switch t {
case C.IS_NULL:
resAny = any(nil)
case C.IS_FALSE:
resAny = any(false)
case C.IS_TRUE:
resAny = any(true)
case C.IS_LONG:
v, err := extractZvalValue(zval, C.IS_LONG)
if err != nil {
return resZero, err
}
if v != nil {
resAny = any(int64(*(*C.zend_long)(v)))
break
}
resAny = any(int64(0))
case C.IS_DOUBLE:
v, err := extractZvalValue(zval, C.IS_DOUBLE)
if err != nil {
return resZero, err
}
if v != nil {
resAny = any(float64(*(*C.double)(v)))
break
}
resAny = any(float64(0))
case C.IS_STRING:
v, err := extractZvalValue(zval, C.IS_STRING)
if err != nil {
return resZero, err
}
if v == nil {
resAny = any("")
break
}
resAny = any(GoString(v))
case C.IS_ARRAY:
v, err := extractZvalValue(zval, C.IS_ARRAY)
if err != nil {
return resZero, err
}
array := (*C.zend_array)(v)
if array != nil && htIsPacked(array) {
typ := reflect.TypeOf(res)
if typ == nil || typ.Kind() == reflect.Interface && typ.NumMethod() == 0 {
r, e := GoPackedArray[any](unsafe.Pointer(array))
if e != nil {
return resZero, e
}
resAny = any(r)
break
}
return resZero, fmt.Errorf("cannot convert packed array to non-any Go type %s", typ.String())
}
a, err := GoAssociativeArray[T](unsafe.Pointer(array))
if err != nil {
return resZero, err
}
resAny = any(a)
default:
return resZero, fmt.Errorf("unsupported zval type %d", t)
}
if resAny == nil {
return resZero, nil
}
if castRes, ok := resAny.(T); ok {
return castRes, nil
}
return resZero, fmt.Errorf("cannot cast value of type %T to type %T", resAny, res)
}
// EXPERIMENTAL: PHPValue converts a Go any to a PHP zval
//
// nil, bool, int, int64, float64, string, []any, and map[string]any are currently supported.
// Any other type will cause a panic.
// More types may be supported in the future.
func PHPValue(value any) unsafe.Pointer {
return unsafe.Pointer(phpValue(value))
}
func phpValue(value any) *C.zval {
zval := (*C.zval)(C.__emalloc__(C.size_t(unsafe.Sizeof(C.zval{}))))
if toZvalObj, ok := value.(toZval); ok {
toZvalObj.toZval(zval)
return zval
}
switch v := value.(type) {
case nil:
C.__zval_null__(zval)
case bool:
C.__zval_bool__(zval, C._Bool(v))
case int:
C.__zval_long__(zval, C.zend_long(v))
case int64:
C.__zval_long__(zval, C.zend_long(v))
case float64:
C.__zval_double__(zval, C.double(v))
case string:
if v == "" {
C.__zval_empty_string__(zval)
break
}
str := (*C.zend_string)(PHPString(v, false))
C.__zval_string__(zval, str)
case AssociativeArray[any]:
C.__zval_arr__(zval, (*C.zend_array)(PHPAssociativeArray[any](v)))
case map[string]any:
C.__zval_arr__(zval, (*C.zend_array)(PHPMap[any](v)))
case []any:
C.__zval_arr__(zval, (*C.zend_array)(PHPPackedArray[any](v)))
default:
C.__efree__(unsafe.Pointer(zval))
panic(fmt.Sprintf("unsupported Go type %T", v))
}
return zval
}
// createNewArray creates a new zend_array with the specified size.
func createNewArray(size uint32) *C.zend_array {
arr := C.__zend_new_array__(C.uint32_t(size))
return (*C.zend_array)(unsafe.Pointer(arr))
}
// IsPacked determines if the given zend_array is a packed array (list).
// Returns false if the array is nil or not packed.
func IsPacked(arr unsafe.Pointer) bool {
if arr == nil {
return false
}
return htIsPacked((*C.zend_array)(arr))
}
// htIsPacked checks if a zend_array is a list (packed) or hashmap (not packed).
func htIsPacked(ht *C.zend_array) bool {
flags := *(*C.uint32_t)(unsafe.Pointer(&ht.u[0]))
return (flags & C.HASH_FLAG_PACKED) != 0
}
// extractZvalValue returns a pointer to the zval value cast to the expected type
func extractZvalValue(zval *C.zval, expectedType C.uint8_t) (unsafe.Pointer, error) {
if zval == nil {
if expectedType == C.IS_NULL {
return nil, nil
}
return nil, fmt.Errorf("zval type mismatch: expected %d, got nil", expectedType)
}
if zType := C.zval_get_type(zval); zType != expectedType {
return nil, fmt.Errorf("zval type mismatch: expected %d, got %d", expectedType, zType)
}
v := unsafe.Pointer(&zval.value[0])
switch expectedType {
case C.IS_LONG, C.IS_DOUBLE:
return v, nil
case C.IS_STRING:
return unsafe.Pointer(*(**C.zend_string)(v)), nil
case C.IS_ARRAY:
return unsafe.Pointer(*(**C.zend_array)(v)), nil
}
return nil, fmt.Errorf("unsupported zval type %d", expectedType)
}
func zendStringRelease(p unsafe.Pointer) {
zs := (*C.zend_string)(p)
C.zend_string_release(zs)
}
func zendHashDestroy(p unsafe.Pointer) {
ht := (*C.zend_array)(p)
C.zend_hash_destroy(ht)
}
// EXPERIMENTAL: CallPHPCallable executes a PHP callable with the given parameters.
// Returns the result of the callable as a Go interface{}, or nil if the call failed.
func CallPHPCallable(cb unsafe.Pointer, params []interface{}) interface{} {
if cb == nil {
return nil
}
callback := (*C.zval)(cb)
if callback == nil {
return nil
}
if C.__zend_is_callable__(callback) == 0 {
return nil
}
paramCount := len(params)
var paramStorage *C.zval
if paramCount > 0 {
paramStorage = (*C.zval)(C.__emalloc__(C.size_t(paramCount) * C.size_t(unsafe.Sizeof(C.zval{}))))
defer func() {
for i := 0; i < paramCount; i++ {
targetZval := (*C.zval)(unsafe.Pointer(uintptr(unsafe.Pointer(paramStorage)) + uintptr(i)*unsafe.Sizeof(C.zval{})))
C.zval_ptr_dtor(targetZval)
}
C.__efree__(unsafe.Pointer(paramStorage))
}()
for i, param := range params {
targetZval := (*C.zval)(unsafe.Pointer(uintptr(unsafe.Pointer(paramStorage)) + uintptr(i)*unsafe.Sizeof(C.zval{})))
sourceZval := phpValue(param)
*targetZval = *sourceZval
C.__efree__(unsafe.Pointer(sourceZval))
}
}
var retval C.zval
result := C.__call_user_function__(callback, &retval, C.uint32_t(paramCount), paramStorage)
if result != C.SUCCESS {
return nil
}
goResult, err := goValue[any](&retval)
C.zval_ptr_dtor(&retval)
if err != nil {
return nil
}
return goResult
}