mirror of
https://github.com/dunglas/frankenphp.git
synced 2025-12-24 13:38:11 +08:00
Compare commits
55 Commits
v1.11.0
...
perf/optim
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff8f864a3d | ||
|
|
fcc9f81986 | ||
|
|
69320d2ee7 | ||
|
|
7ceb485dae | ||
|
|
19d00a08e2 | ||
|
|
f5a9bc3d7a | ||
|
|
2306152fde | ||
|
|
a86533a6ac | ||
|
|
8bdb3de552 | ||
|
|
360d15c2aa | ||
|
|
549cca304a | ||
|
|
c8c7d046ef | ||
|
|
ecf3f0e792 | ||
|
|
57c58faf1c | ||
|
|
25d9cb9600 | ||
|
|
ae97abb897 | ||
|
|
a222fd51cb | ||
|
|
e2976abbeb | ||
|
|
a209d227ef | ||
|
|
4092ecb5b5 | ||
|
|
75ccccf1b2 | ||
|
|
5e139519a3 | ||
|
|
bcee843017 | ||
|
|
703d037ef7 | ||
|
|
af328a3166 | ||
|
|
c749e2bab0 | ||
|
|
dfb018cdd4 | ||
|
|
19c09050c6 | ||
|
|
ae391c4ba9 | ||
|
|
e3994afb78 | ||
|
|
04bca7b847 | ||
|
|
68a9771e65 | ||
|
|
9501c37921 | ||
|
|
360fdfdfd8 | ||
|
|
115d53561f | ||
|
|
8018018bc5 | ||
|
|
c4bce5c782 | ||
|
|
8e09ffe89c | ||
|
|
c3e588b528 | ||
|
|
066f9060c7 | ||
|
|
1d23b41c38 | ||
|
|
2519a2fbda | ||
|
|
3770f09df9 | ||
|
|
15cdac8690 | ||
|
|
bf8529335a | ||
|
|
a5125f5aa8 | ||
|
|
e9d2294649 | ||
|
|
5f1bd59a53 | ||
|
|
bb32911564 | ||
|
|
0bc8de4175 | ||
|
|
acfbe5160a | ||
|
|
cc6eae4aca | ||
|
|
fb9acec3fa | ||
|
|
c87e4c9351 | ||
|
|
3744bf0c1d |
@@ -178,7 +178,11 @@ fi
|
||||
|
||||
# Embed PHP app, if any
|
||||
if [ -n "${EMBED}" ] && [ -d "${EMBED}" ]; then
|
||||
SPC_OPT_BUILD_ARGS="${SPC_OPT_BUILD_ARGS} --with-frankenphp-app=${EMBED}"
|
||||
if [[ "${EMBED}" != /* ]]; then
|
||||
EMBED="${CURRENT_DIR}/${EMBED}"
|
||||
fi
|
||||
# shellcheck disable=SC2089
|
||||
SPC_OPT_BUILD_ARGS="${SPC_OPT_BUILD_ARGS} --with-frankenphp-app='${EMBED}'"
|
||||
fi
|
||||
|
||||
SPC_OPT_INSTALL_ARGS="go-xcaddy"
|
||||
@@ -204,7 +208,7 @@ done
|
||||
# shellcheck disable=SC2086
|
||||
${spcCommand} download --with-php="${PHP_VERSION}" --for-extensions="${PHP_EXTENSIONS}" --for-libs="${PHP_EXTENSION_LIBS}" ${SPC_OPT_DOWNLOAD_ARGS}
|
||||
export FRANKENPHP_SOURCE_PATH="${CURRENT_DIR}"
|
||||
# shellcheck disable=SC2086
|
||||
# shellcheck disable=SC2086,SC2090
|
||||
${spcCommand} build --enable-zts --build-embed --build-frankenphp ${SPC_OPT_BUILD_ARGS} "${PHP_EXTENSIONS}" --with-libs="${PHP_EXTENSION_LIBS}"
|
||||
|
||||
if [ -n "$CI" ]; then
|
||||
|
||||
@@ -1472,3 +1472,31 @@ func TestDd(t *testing.T) {
|
||||
"dump123",
|
||||
)
|
||||
}
|
||||
|
||||
func TestLog(t *testing.T) {
|
||||
tester := caddytest.NewTester(t)
|
||||
tester.InitServer(`
|
||||
{
|
||||
skip_install_trust
|
||||
admin localhost:2999
|
||||
}
|
||||
|
||||
http://localhost:`+testPort+` {
|
||||
log {
|
||||
output stdout
|
||||
format json
|
||||
}
|
||||
|
||||
root ../testdata
|
||||
php_server {
|
||||
worker ../testdata/log-frankenphp_log.php
|
||||
}
|
||||
}
|
||||
`, "caddyfile")
|
||||
|
||||
tester.AssertGetResponse(
|
||||
"http://localhost:"+testPort+"/log-frankenphp_log.php?i=0",
|
||||
http.StatusOK,
|
||||
"",
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ require (
|
||||
github.com/caddyserver/caddy/v2 v2.10.2
|
||||
github.com/caddyserver/certmagic v0.25.0
|
||||
github.com/dunglas/caddy-cbrotli v1.0.1
|
||||
github.com/dunglas/frankenphp v1.11.0
|
||||
github.com/dunglas/frankenphp v1.11.1
|
||||
github.com/dunglas/mercure v0.21.4
|
||||
github.com/dunglas/mercure/caddy v0.21.4
|
||||
github.com/dunglas/vulcain/caddy v1.2.1
|
||||
|
||||
@@ -55,11 +55,8 @@ func (f *FrankenPHPModule) configureHotReload(app *FrankenPHPApp) error {
|
||||
}
|
||||
|
||||
func (f *FrankenPHPModule) unmarshalHotReload(d *caddyfile.Dispenser) error {
|
||||
patterns := d.RemainingArgs()
|
||||
if len(patterns) > 0 {
|
||||
f.HotReload = &hotReloadConfig{
|
||||
Watch: patterns,
|
||||
}
|
||||
f.HotReload = &hotReloadConfig{
|
||||
Watch: d.RemainingArgs(),
|
||||
}
|
||||
|
||||
for d.NextBlock(1) {
|
||||
@@ -81,10 +78,6 @@ func (f *FrankenPHPModule) unmarshalHotReload(d *caddyfile.Dispenser) error {
|
||||
return d.ArgErr()
|
||||
}
|
||||
|
||||
if f.HotReload == nil {
|
||||
f.HotReload = &hotReloadConfig{}
|
||||
}
|
||||
|
||||
f.HotReload.Watch = append(f.HotReload.Watch, patterns...)
|
||||
|
||||
default:
|
||||
|
||||
@@ -554,10 +554,10 @@ PHP_FUNCTION(frankenphp_log) {
|
||||
zend_long level = 0;
|
||||
zval *context = NULL;
|
||||
|
||||
ZEND_PARSE_PARAMETERS_START(2, 3)
|
||||
ZEND_PARSE_PARAMETERS_START(1, 3)
|
||||
Z_PARAM_STR(message)
|
||||
Z_PARAM_LONG(level)
|
||||
Z_PARAM_OPTIONAL
|
||||
Z_PARAM_LONG(level)
|
||||
Z_PARAM_ARRAY(context)
|
||||
ZEND_PARSE_PARAMETERS_END();
|
||||
|
||||
|
||||
@@ -660,10 +660,28 @@ func go_read_cookies(threadIndex C.uintptr_t) *C.char {
|
||||
return C.CString(cookie)
|
||||
}
|
||||
|
||||
func getLogger(threadIndex C.uintptr_t) (*slog.Logger, context.Context) {
|
||||
ctxHolder := phpThreads[threadIndex]
|
||||
if ctxHolder == nil {
|
||||
return globalLogger, globalCtx
|
||||
}
|
||||
|
||||
ctx := ctxHolder.context()
|
||||
if ctxHolder.handler == nil {
|
||||
return globalLogger, ctx
|
||||
}
|
||||
|
||||
fCtx := ctxHolder.frankenPHPContext()
|
||||
if fCtx == nil || fCtx.logger == nil {
|
||||
return globalLogger, ctx
|
||||
}
|
||||
|
||||
return fCtx.logger, ctx
|
||||
}
|
||||
|
||||
//export go_log
|
||||
func go_log(threadIndex C.uintptr_t, message *C.char, level C.int) {
|
||||
ctx := phpThreads[threadIndex].context()
|
||||
logger := phpThreads[threadIndex].frankenPHPContext().logger
|
||||
logger, ctx := getLogger(threadIndex)
|
||||
|
||||
m := C.GoString(message)
|
||||
le := syslogLevelInfo
|
||||
@@ -697,8 +715,7 @@ func go_log(threadIndex C.uintptr_t, message *C.char, level C.int) {
|
||||
|
||||
//export go_log_attrs
|
||||
func go_log_attrs(threadIndex C.uintptr_t, message *C.zend_string, cLevel C.zend_long, cAttrs *C.zval) *C.char {
|
||||
ctx := phpThreads[threadIndex].context()
|
||||
logger := phpThreads[threadIndex].frankenPHPContext().logger
|
||||
logger, ctx := getLogger(threadIndex)
|
||||
|
||||
level := slog.Level(cLevel)
|
||||
|
||||
|
||||
@@ -447,6 +447,7 @@ func testLog_frankenphp_log(t *testing.T, opts *testOptions) {
|
||||
|
||||
logs := buf.String()
|
||||
for _, message := range []string{
|
||||
`level=INFO msg="default level message"`,
|
||||
fmt.Sprintf(`level=DEBUG msg="some debug message %d" "key int"=1`, i),
|
||||
fmt.Sprintf(`level=INFO msg="some info message %d" "key string"=string`, i),
|
||||
fmt.Sprintf(`level=WARN msg="some warn message %d"`, i),
|
||||
|
||||
@@ -738,36 +738,42 @@ func TestCallable(t *testing.T) {
|
||||
|
||||
err = suite.verifyFunctionBehavior(`<?php
|
||||
|
||||
echo "Testing my_array_map([1, 2, 3])\n";
|
||||
$result = my_array_map([1, 2, 3], function($x) { return $x * 2; });
|
||||
if ($result !== [2, 4, 6]) {
|
||||
echo "FAIL: my_array_map with closure expected [2, 4, 6], got " . json_encode($result);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
echo "Testing my_array_map(['hello', 'world'])\n";
|
||||
$result = my_array_map(['hello', 'world'], 'strtoupper');
|
||||
if ($result !== ['HELLO', 'WORLD']) {
|
||||
echo "FAIL: my_array_map with function name expected ['HELLO', 'WORLD'], got " . json_encode($result);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
echo "Testing my_array_map with empty array\n";
|
||||
$result = my_array_map([], function($x) { return $x; });
|
||||
if ($result !== []) {
|
||||
echo "FAIL: my_array_map with empty array expected [], got " . json_encode($result);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
echo "Testing my_filter([1, 2, 3, 4, 5, 6])\n";
|
||||
$result = my_filter([1, 2, 3, 4, 5, 6], function($x) { return $x % 2 === 0; });
|
||||
if ($result !== [2, 4, 6]) {
|
||||
echo "FAIL: my_filter expected [2, 4, 6], got " . json_encode($result);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
echo "Testing my_filter with null callback\n";
|
||||
$result = my_filter([1, 2, 3, 4], null);
|
||||
if ($result !== [1, 2, 3, 4]) {
|
||||
echo "FAIL: my_filter with null callback expected [1, 2, 3, 4], got " . json_encode($result);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
echo "Testing Processor::transform\n";
|
||||
$processor = new Processor();
|
||||
$result = $processor->transform('hello', function($s) { return strtoupper($s); });
|
||||
if ($result !== 'HELLO') {
|
||||
@@ -775,12 +781,14 @@ if ($result !== 'HELLO') {
|
||||
exit(1);
|
||||
}
|
||||
|
||||
echo "Testing Processor::transform with function name\n";
|
||||
$result = $processor->transform('world', 'strtoupper');
|
||||
if ($result !== 'WORLD') {
|
||||
echo "FAIL: Processor::transform with function name expected 'WORLD', got '$result'";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
echo "Testing Processor::transform with trim function\n";
|
||||
$result = $processor->transform(' test ', 'trim');
|
||||
if ($result !== 'test') {
|
||||
echo "FAIL: Processor::transform with trim expected 'test', got '$result'";
|
||||
|
||||
@@ -96,7 +96,7 @@ func TestTransitionThreadsWhileDoingRequests(t *testing.T) {
|
||||
|
||||
var (
|
||||
isDone atomic.Bool
|
||||
wg sync.WaitGroup
|
||||
wg sync.WaitGroup
|
||||
)
|
||||
|
||||
numThreads := 10
|
||||
|
||||
5
testdata/integration/callable.go
vendored
5
testdata/integration/callable.go
vendored
@@ -32,7 +32,8 @@ func my_filter(arr *C.zend_array, callback *C.zval) unsafe.Pointer {
|
||||
}
|
||||
|
||||
if callback == nil {
|
||||
return unsafe.Pointer(arr)
|
||||
//return unsafe.Pointer(arr) // returning the original array requires GC_ADDREF
|
||||
return frankenphp.PHPPackedArray[any](goArray)
|
||||
}
|
||||
|
||||
result := make([]any, 0)
|
||||
@@ -57,7 +58,7 @@ func (p *Processor) Transform(input *C.zend_string, callback *C.zval) unsafe.Poi
|
||||
|
||||
resultStr, ok := callResult.(string)
|
||||
if !ok {
|
||||
return unsafe.Pointer(input)
|
||||
return frankenphp.PHPString(goInput, false)
|
||||
}
|
||||
|
||||
return frankenphp.PHPString(resultStr, false)
|
||||
|
||||
2
testdata/log-frankenphp_log.php
vendored
2
testdata/log-frankenphp_log.php
vendored
@@ -2,6 +2,8 @@
|
||||
|
||||
require_once __DIR__.'/_executor.php';
|
||||
|
||||
frankenphp_log("default level message");
|
||||
|
||||
return function () {
|
||||
frankenphp_log("some debug message {$_GET['i']}", FRANKENPHP_LOG_LEVEL_DEBUG, [
|
||||
"key int" => 1,
|
||||
|
||||
@@ -255,23 +255,18 @@ func (handler *workerThread) waitForWorkerRequest() (bool, any) {
|
||||
// go_frankenphp_worker_handle_request_start is called at the start of every php request served.
|
||||
//
|
||||
//export go_frankenphp_worker_handle_request_start
|
||||
func go_frankenphp_worker_handle_request_start(threadIndex C.uintptr_t) (C.bool, unsafe.Pointer) {
|
||||
func go_frankenphp_worker_handle_request_start(threadIndex C.uintptr_t) (C.bool, *C.zval) {
|
||||
handler := phpThreads[threadIndex].handler.(*workerThread)
|
||||
hasRequest, parameters := handler.waitForWorkerRequest()
|
||||
|
||||
if parameters != nil {
|
||||
var ptr unsafe.Pointer
|
||||
|
||||
switch p := parameters.(type) {
|
||||
case unsafe.Pointer:
|
||||
ptr = p
|
||||
return C.bool(hasRequest), (*C.zval)(p)
|
||||
|
||||
default:
|
||||
ptr = PHPValue(p)
|
||||
return C.bool(hasRequest), (*C.zval)(PHPValue(p))
|
||||
}
|
||||
handler.thread.Pin(ptr)
|
||||
|
||||
return C.bool(hasRequest), ptr
|
||||
}
|
||||
|
||||
return C.bool(hasRequest), nil
|
||||
|
||||
73
types.c
73
types.c
@@ -2,14 +2,14 @@
|
||||
|
||||
zval *get_ht_packed_data(HashTable *ht, uint32_t index) {
|
||||
if (ht->u.flags & HASH_FLAG_PACKED) {
|
||||
return &ht->arPacked[index];
|
||||
return ht->arPacked;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
Bucket *get_ht_bucket_data(HashTable *ht, uint32_t index) {
|
||||
Bucket *get_ht_bucket(HashTable *ht) {
|
||||
if (!(ht->u.flags & HASH_FLAG_PACKED)) {
|
||||
return &ht->arData[index];
|
||||
return ht->arData;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
@@ -23,22 +23,61 @@ void __zend_hash_init__(HashTable *ht, uint32_t nSize, dtor_func_t pDestructor,
|
||||
zend_hash_init(ht, nSize, NULL, pDestructor, persistent);
|
||||
}
|
||||
|
||||
void __zval_null__(zval *zv) { ZVAL_NULL(zv); }
|
||||
|
||||
void __zval_bool__(zval *zv, bool val) { ZVAL_BOOL(zv, val); }
|
||||
|
||||
void __zval_long__(zval *zv, zend_long val) { ZVAL_LONG(zv, val); }
|
||||
|
||||
void __zval_double__(zval *zv, double val) { ZVAL_DOUBLE(zv, val); }
|
||||
|
||||
void __zval_string__(zval *zv, zend_string *str) { ZVAL_STR(zv, str); }
|
||||
|
||||
void __zval_empty_string__(zval *zv) { ZVAL_EMPTY_STRING(zv); }
|
||||
|
||||
void __zval_arr__(zval *zv, zend_array *arr) { ZVAL_ARR(zv, arr); }
|
||||
|
||||
zend_array *__zend_new_array__(uint32_t size) { return zend_new_array(size); }
|
||||
|
||||
zend_array *zend_hash_bulk_insert(zend_array *arr, size_t num_entries,
|
||||
size_t bulk_size, char *key1, char *key2,
|
||||
char *key3, char *key4, size_t key_len1,
|
||||
size_t key_len2, size_t key_len3,
|
||||
size_t key_len4, zval *val1, zval *val2,
|
||||
zval *val3, zval *val4) {
|
||||
if (!arr) {
|
||||
arr = zend_new_array(num_entries);
|
||||
}
|
||||
|
||||
zend_hash_str_update(arr, key1, key_len1, val1);
|
||||
if (bulk_size < 1) {
|
||||
return arr;
|
||||
}
|
||||
zend_hash_str_update(arr, key2, key_len2, val2);
|
||||
if (bulk_size < 2) {
|
||||
return arr;
|
||||
}
|
||||
zend_hash_str_update(arr, key3, key_len3, val3);
|
||||
if (bulk_size < 3) {
|
||||
return arr;
|
||||
}
|
||||
zend_hash_str_update(arr, key4, key_len4, val4);
|
||||
|
||||
return arr;
|
||||
}
|
||||
|
||||
zend_array *zend_hash_bulk_next_index_insert(zend_array *arr,
|
||||
size_t num_entries,
|
||||
size_t bulk_size, zval *val1,
|
||||
zval *val2, zval *val3,
|
||||
zval *val4) {
|
||||
if (!arr) {
|
||||
arr = zend_new_array(num_entries);
|
||||
}
|
||||
|
||||
zend_hash_next_index_insert(arr, val1);
|
||||
if (bulk_size < 1) {
|
||||
return arr;
|
||||
}
|
||||
zend_hash_next_index_insert(arr, val2);
|
||||
if (bulk_size < 2) {
|
||||
return arr;
|
||||
}
|
||||
zend_hash_next_index_insert(arr, val3);
|
||||
if (bulk_size < 3) {
|
||||
return arr;
|
||||
}
|
||||
zend_hash_next_index_insert(arr, val4);
|
||||
|
||||
return arr;
|
||||
}
|
||||
|
||||
int __zend_is_callable__(zval *cb) { return zend_is_callable(cb, 0, NULL); }
|
||||
|
||||
int __call_user_function__(zval *function_name, zval *retval,
|
||||
|
||||
467
types.go
467
types.go
@@ -1,25 +1,14 @@
|
||||
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"
|
||||
*/
|
||||
//#cgo noescape __zend_new_array__
|
||||
//#cgo noescape zend_hash_bulk_insert
|
||||
//#cgo noescape zend_hash_bulk_next_index_insert
|
||||
//#cgo noescape get_ht_bucket
|
||||
//#cgo noescape get_ht_packed_data
|
||||
//#include "zend_API.h"
|
||||
//#include "types.h"
|
||||
import "C"
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
@@ -36,26 +25,30 @@ func GoString(s unsafe.Pointer) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
zendStr := (*C.zend_string)(s)
|
||||
return goString((*C.zend_string)(s))
|
||||
}
|
||||
|
||||
func goString(zendStr *C.zend_string) string {
|
||||
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.
|
||||
// the second mode, it is your repsonsibility to free the allocated memory.
|
||||
func PHPString(s string, persistent bool) unsafe.Pointer {
|
||||
return unsafe.Pointer(phpString(s, persistent))
|
||||
}
|
||||
|
||||
func phpString(s string, persistent bool) *C.zend_string {
|
||||
if s == "" {
|
||||
return nil
|
||||
return C.zend_empty_string
|
||||
}
|
||||
|
||||
zendStr := C.zend_string_init(
|
||||
return 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
|
||||
@@ -65,35 +58,34 @@ type AssociativeArray[T any] struct {
|
||||
}
|
||||
|
||||
func (a AssociativeArray[T]) toZval(zval *C.zval) {
|
||||
C.__zval_arr__(zval, (*C.zend_array)(PHPAssociativeArray[T](a)))
|
||||
*(*uint32)(unsafe.Pointer(&zval.u1)) = C.IS_ARRAY_EX
|
||||
*(**C.zend_array)(unsafe.Pointer(&zval.value)) = phpArray[T](a.Map, a.Order)
|
||||
}
|
||||
|
||||
// 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)
|
||||
entries, order, err := goArray[T]((*C.zend_array)(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)
|
||||
entries, _, err := goArray[T]((*C.zend_array)(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)
|
||||
|
||||
func goArray[T any](array *C.zend_array, ordered bool) (map[string]T, []string, error) {
|
||||
if array == nil {
|
||||
return nil, nil, fmt.Errorf("received a *zval that wasn't a HashTable on array conversion")
|
||||
return nil, nil, fmt.Errorf("received a nil pointer on array conversion")
|
||||
}
|
||||
|
||||
nNumUsed := array.nNumUsed
|
||||
if nNumUsed == 0 {
|
||||
return make(map[string]T), nil, nil
|
||||
}
|
||||
|
||||
entries := make(map[string]T, nNumUsed)
|
||||
var order []string
|
||||
if ordered {
|
||||
@@ -104,45 +96,36 @@ func goArray[T any](arr unsafe.Pointer, ordered bool) (map[string]T, []string, e
|
||||
// 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
|
||||
zvals := unsafe.Slice(C.get_ht_packed_data(array, 0), nNumUsed)
|
||||
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)
|
||||
}
|
||||
v := &zvals[i]
|
||||
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
|
||||
|
||||
buckets := unsafe.Slice(C.get_ht_bucket(array), nNumUsed)
|
||||
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)
|
||||
bucket := &buckets[i]
|
||||
v, err := goValue[T](&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)
|
||||
}
|
||||
keyStr := goString(bucket.key)
|
||||
entries[keyStr] = v
|
||||
|
||||
if ordered {
|
||||
order = append(order, keyStr)
|
||||
@@ -153,7 +136,7 @@ func goArray[T any](arr unsafe.Pointer, ordered bool) (map[string]T, []string, e
|
||||
|
||||
// as fallback convert the bucket index to a string key
|
||||
strIndex := strconv.Itoa(int(bucket.h))
|
||||
entries[strIndex] = v.(T)
|
||||
entries[strIndex] = v
|
||||
if ordered {
|
||||
order = append(order, strIndex)
|
||||
}
|
||||
@@ -164,46 +147,42 @@ func goArray[T any](arr unsafe.Pointer, ordered bool) (map[string]T, []string, e
|
||||
|
||||
// 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)
|
||||
return goPackedArray[T]((*C.zend_array)(arr))
|
||||
}
|
||||
|
||||
func goPackedArray[T any](array *C.zend_array) ([]T, error) {
|
||||
if array == nil {
|
||||
return nil, fmt.Errorf("GoPackedArray received *zval that wasn't a HashTable")
|
||||
return nil, fmt.Errorf("GoPackedArray received nil pointer")
|
||||
}
|
||||
|
||||
nNumUsed := array.nNumUsed
|
||||
result := make([]T, 0, nNumUsed)
|
||||
|
||||
if htIsPacked(array) {
|
||||
zvals := unsafe.Slice(C.get_ht_packed_data(array, 0), nNumUsed)
|
||||
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)
|
||||
v := &zvals[i]
|
||||
goVal, err := goValue[T](v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result = append(result, goVal)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// fallback if ht isn't packed - equivalent to array_values()
|
||||
buckets := unsafe.Slice(C.get_ht_bucket(array), nNumUsed)
|
||||
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)
|
||||
bucket := &buckets[i]
|
||||
v, err := goValue[T](&bucket.val)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result = append(result, v)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
@@ -211,47 +190,142 @@ func GoPackedArray[T any](arr unsafe.Pointer) ([]T, error) {
|
||||
|
||||
// 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)
|
||||
return unsafe.Pointer(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)
|
||||
return unsafe.Pointer(phpArray[T](arr.Map, arr.Order))
|
||||
}
|
||||
|
||||
func phpArray[T any](entries map[string]T, order []string) unsafe.Pointer {
|
||||
var zendArray *C.zend_array
|
||||
func phpArray[T any](entries map[string]T, order []string) *C.zend_array {
|
||||
lenEntries := len(entries)
|
||||
lenOrder := len(order)
|
||||
if lenEntries == 0 && lenOrder == 0 {
|
||||
return createNewArray(0)
|
||||
}
|
||||
|
||||
if len(order) != 0 {
|
||||
zendArray = createNewArray((uint32)(len(order)))
|
||||
// bulk insert zvals 4 by 4
|
||||
// this is currently the most efficient way to avoid cgo overhead
|
||||
var zendArray *C.zend_array
|
||||
var key1 *C.char
|
||||
var keyLen1 C.size_t
|
||||
var zval1 C.zval
|
||||
var key2 *C.char
|
||||
var keyLen2 C.size_t
|
||||
var zval2 C.zval
|
||||
var key3 *C.char
|
||||
var keyLen3 C.size_t
|
||||
var zval3 C.zval
|
||||
var key4 *C.char
|
||||
var keyLen4 C.size_t
|
||||
var zval4 C.zval
|
||||
i := 0
|
||||
|
||||
if lenOrder != 0 {
|
||||
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))
|
||||
mod := i % 4
|
||||
switch mod {
|
||||
case 0:
|
||||
key1 = toUnsafeChar(key)
|
||||
keyLen1 = C.size_t(len(key))
|
||||
phpValue(&zval1, val)
|
||||
case 1:
|
||||
key2 = toUnsafeChar(key)
|
||||
keyLen2 = C.size_t(len(key))
|
||||
phpValue(&zval2, val)
|
||||
case 2:
|
||||
key3 = toUnsafeChar(key)
|
||||
keyLen3 = C.size_t(len(key))
|
||||
phpValue(&zval3, val)
|
||||
case 3:
|
||||
key4 = toUnsafeChar(key)
|
||||
keyLen4 = C.size_t(len(key))
|
||||
phpValue(&zval4, val)
|
||||
}
|
||||
if mod == 3 || i == lenOrder-1 {
|
||||
zendArray = C.zend_hash_bulk_insert(
|
||||
zendArray, C.size_t(lenOrder), C.size_t(mod),
|
||||
key1, key2, key3, key4,
|
||||
keyLen1, keyLen2, keyLen3, keyLen4,
|
||||
&zval1, &zval2, &zval3, &zval4,
|
||||
)
|
||||
}
|
||||
i++
|
||||
}
|
||||
} 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))
|
||||
mod := i % 4
|
||||
switch mod {
|
||||
case 0:
|
||||
key1 = toUnsafeChar(key)
|
||||
keyLen1 = C.size_t(len(key))
|
||||
phpValue(&zval1, val)
|
||||
case 1:
|
||||
key2 = toUnsafeChar(key)
|
||||
keyLen2 = C.size_t(len(key))
|
||||
phpValue(&zval2, val)
|
||||
case 2:
|
||||
key3 = toUnsafeChar(key)
|
||||
keyLen3 = C.size_t(len(key))
|
||||
phpValue(&zval3, val)
|
||||
case 3:
|
||||
key4 = toUnsafeChar(key)
|
||||
keyLen4 = C.size_t(len(key))
|
||||
phpValue(&zval4, val)
|
||||
}
|
||||
if mod == 3 || i == lenEntries-1 {
|
||||
zendArray = C.zend_hash_bulk_insert(
|
||||
zendArray, C.size_t(lenEntries), C.size_t(mod),
|
||||
key1, key2, key3, key4,
|
||||
keyLen1, keyLen2, keyLen3, keyLen4,
|
||||
&zval1, &zval2, &zval3, &zval4,
|
||||
)
|
||||
}
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
return unsafe.Pointer(zendArray)
|
||||
return 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[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(phpPackedArray[T](slice))
|
||||
}
|
||||
|
||||
return unsafe.Pointer(zendArray)
|
||||
func phpPackedArray[T any](slice []T) *C.zend_array {
|
||||
sliceLen := len(slice)
|
||||
if sliceLen == 0 {
|
||||
return createNewArray(0)
|
||||
}
|
||||
var zendArray *C.zend_array
|
||||
var zval1 C.zval
|
||||
var zval2 C.zval
|
||||
var zval3 C.zval
|
||||
var zval4 C.zval
|
||||
for i, val := range slice {
|
||||
|
||||
mod := i % 4
|
||||
switch mod {
|
||||
case 0:
|
||||
phpValue(&zval1, val)
|
||||
case 1:
|
||||
phpValue(&zval2, val)
|
||||
case 2:
|
||||
phpValue(&zval3, val)
|
||||
case 3:
|
||||
phpValue(&zval4, val)
|
||||
}
|
||||
if mod == 3 || i == sliceLen-1 {
|
||||
zendArray = C.zend_hash_bulk_next_index_insert(
|
||||
zendArray, C.size_t(sliceLen), C.size_t(mod),
|
||||
&zval1, &zval2, &zval3, &zval4,
|
||||
)
|
||||
}
|
||||
}
|
||||
return zendArray
|
||||
}
|
||||
|
||||
// EXPERIMENTAL: GoValue converts a PHP zval to a Go value
|
||||
@@ -269,70 +343,34 @@ func goValue[T any](zval *C.zval) (res T, err error) {
|
||||
resAny any
|
||||
resZero T
|
||||
)
|
||||
t := C.zval_get_type(zval)
|
||||
|
||||
switch t {
|
||||
switch zvalGetType(zval) {
|
||||
case C.IS_NULL:
|
||||
resAny = any(nil)
|
||||
resAny = nil
|
||||
case C.IS_FALSE:
|
||||
resAny = any(false)
|
||||
resAny = false
|
||||
case C.IS_TRUE:
|
||||
resAny = any(true)
|
||||
resAny = 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))
|
||||
v := (*C.zend_long)(unsafe.Pointer(&zval.value[0]))
|
||||
resAny = int64(*v)
|
||||
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))
|
||||
v := (*C.double)(unsafe.Pointer(&zval.value[0]))
|
||||
resAny = float64(*v)
|
||||
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))
|
||||
v := *(**C.zend_string)(unsafe.Pointer(&zval.value[0]))
|
||||
resAny = 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) {
|
||||
array := *(**C.zend_array)(unsafe.Pointer(&zval.value[0]))
|
||||
if htIsPacked(array) {
|
||||
typ := reflect.TypeOf(res)
|
||||
if typ == nil || typ.Kind() == reflect.Interface && typ.NumMethod() == 0 {
|
||||
r, e := GoPackedArray[any](unsafe.Pointer(array))
|
||||
r, e := goPackedArray[any](array)
|
||||
if e != nil {
|
||||
return resZero, e
|
||||
}
|
||||
|
||||
resAny = any(r)
|
||||
resAny = r
|
||||
|
||||
break
|
||||
}
|
||||
@@ -340,14 +378,14 @@ func goValue[T any](zval *C.zval) (res T, err error) {
|
||||
return resZero, fmt.Errorf("cannot convert packed array to non-any Go type %s", typ.String())
|
||||
}
|
||||
|
||||
a, err := GoAssociativeArray[T](unsafe.Pointer(array))
|
||||
goMap, order, err := goArray[T](array, true)
|
||||
if err != nil {
|
||||
return resZero, err
|
||||
}
|
||||
|
||||
resAny = any(a)
|
||||
resAny = AssociativeArray[T]{Map: goMap, Order: order}
|
||||
default:
|
||||
return resZero, fmt.Errorf("unsupported zval type %d", t)
|
||||
return resZero, fmt.Errorf("unsupported zval type %d", zvalGetType(zval))
|
||||
}
|
||||
|
||||
if resAny == nil {
|
||||
@@ -367,53 +405,70 @@ func goValue[T any](zval *C.zval) (res T, err error) {
|
||||
// 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))
|
||||
zval := (*C.zval)(C.__emalloc__(C.size_t(unsafe.Sizeof(C.zval{}))))
|
||||
phpValue(zval, value)
|
||||
return unsafe.Pointer(zval)
|
||||
}
|
||||
|
||||
func phpValue(value any) *C.zval {
|
||||
zval := (*C.zval)(C.__emalloc__(C.size_t(unsafe.Sizeof(C.zval{}))))
|
||||
|
||||
func phpValue(zval *C.zval, value any) {
|
||||
if toZvalObj, ok := value.(toZval); ok {
|
||||
toZvalObj.toZval(zval)
|
||||
return zval
|
||||
return
|
||||
}
|
||||
|
||||
switch v := value.(type) {
|
||||
case nil:
|
||||
C.__zval_null__(zval)
|
||||
// equvalent of ZVAL_NULL
|
||||
*(*uint32)(unsafe.Pointer(&zval.u1)) = C.IS_NULL
|
||||
case bool:
|
||||
C.__zval_bool__(zval, C._Bool(v))
|
||||
// equvalent of ZVAL_BOOL
|
||||
if v {
|
||||
*(*uint32)(unsafe.Pointer(&zval.u1)) = C.IS_TRUE
|
||||
} else {
|
||||
*(*uint32)(unsafe.Pointer(&zval.u1)) = C.IS_FALSE
|
||||
}
|
||||
case int:
|
||||
C.__zval_long__(zval, C.zend_long(v))
|
||||
// equvalent of ZVAL_LONG
|
||||
*(*uint32)(unsafe.Pointer(&zval.u1)) = C.IS_LONG
|
||||
*(*C.zend_long)(unsafe.Pointer(&zval.value)) = C.zend_long(v)
|
||||
case int64:
|
||||
C.__zval_long__(zval, C.zend_long(v))
|
||||
// equvalent of ZVAL_LONG
|
||||
*(*uint32)(unsafe.Pointer(&zval.u1)) = C.IS_LONG
|
||||
*(*C.zend_long)(unsafe.Pointer(&zval.value)) = C.zend_long(v)
|
||||
case float64:
|
||||
C.__zval_double__(zval, C.double(v))
|
||||
// equvalent of ZVAL_DOUBLE
|
||||
*(*uint32)(unsafe.Pointer(&zval.u1)) = C.IS_DOUBLE
|
||||
*(*C.double)(unsafe.Pointer(&zval.value)) = C.double(v)
|
||||
case string:
|
||||
if v == "" {
|
||||
C.__zval_empty_string__(zval)
|
||||
// equivalent ZVAL_EMPTY_STRING
|
||||
*(*uint32)(unsafe.Pointer(&zval.u1)) = C.IS_INTERNED_STRING_EX
|
||||
*(**C.zend_string)(unsafe.Pointer(&zval.value)) = C.zend_empty_string
|
||||
break
|
||||
}
|
||||
str := (*C.zend_string)(PHPString(v, false))
|
||||
C.__zval_string__(zval, str)
|
||||
// equvalent of ZVAL_STRING
|
||||
*(*uint32)(unsafe.Pointer(&zval.u1)) = C.IS_STRING_EX
|
||||
*(**C.zend_string)(unsafe.Pointer(&zval.value)) = phpString(v, false)
|
||||
case AssociativeArray[any]:
|
||||
C.__zval_arr__(zval, (*C.zend_array)(PHPAssociativeArray[any](v)))
|
||||
// equvalent of ZVAL_ARR
|
||||
*(*uint32)(unsafe.Pointer(&zval.u1)) = C.IS_ARRAY_EX
|
||||
*(**C.zend_array)(unsafe.Pointer(&zval.value)) = phpArray[any](v.Map, v.Order)
|
||||
case map[string]any:
|
||||
C.__zval_arr__(zval, (*C.zend_array)(PHPMap[any](v)))
|
||||
// equvalent of ZVAL_ARR
|
||||
*(*uint32)(unsafe.Pointer(&zval.u1)) = C.IS_ARRAY_EX
|
||||
*(**C.zend_array)(unsafe.Pointer(&zval.value)) = phpArray[any](v, nil)
|
||||
case []any:
|
||||
C.__zval_arr__(zval, (*C.zend_array)(PHPPackedArray[any](v)))
|
||||
// equvalent of ZVAL_ARR
|
||||
*(*uint32)(unsafe.Pointer(&zval.u1)) = C.IS_ARRAY_EX
|
||||
*(**C.zend_array)(unsafe.Pointer(&zval.value)) = 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))
|
||||
func createNewArray(size int) *C.zend_array {
|
||||
return C.__zend_new_array__(C.uint32_t(size))
|
||||
}
|
||||
|
||||
// IsPacked determines if the given zend_array is a packed array (list).
|
||||
@@ -433,42 +488,26 @@ func htIsPacked(ht *C.zend_array) bool {
|
||||
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)
|
||||
// equivalent of Z_TYPE_P
|
||||
// interpret z->u1 as a 32-bit integer, then take lowest byte
|
||||
func zvalGetType(z *C.zval) C.uint8_t {
|
||||
typeInfo := *(*uint32)(unsafe.Pointer(&z.u1))
|
||||
return C.uint8_t(typeInfo & 0xFF)
|
||||
}
|
||||
|
||||
// used in tests for cleanup
|
||||
func zendStringRelease(p unsafe.Pointer) {
|
||||
zs := (*C.zend_string)(p)
|
||||
C.zend_string_release(zs)
|
||||
C.zend_string_release((*C.zend_string)(p))
|
||||
}
|
||||
|
||||
func zendHashDestroy(p unsafe.Pointer) {
|
||||
ht := (*C.zend_array)(p)
|
||||
C.zend_hash_destroy(ht)
|
||||
// used in tests for cleanup
|
||||
func zendArrayRelease(p unsafe.Pointer) {
|
||||
C.zend_array_release((*C.zend_array)(p))
|
||||
}
|
||||
|
||||
// used in tests for cleanup
|
||||
func efree(p unsafe.Pointer) {
|
||||
C.__efree__(p)
|
||||
}
|
||||
|
||||
// EXPERIMENTAL: CallPHPCallable executes a PHP callable with the given parameters.
|
||||
@@ -501,9 +540,7 @@ func CallPHPCallable(cb unsafe.Pointer, params []interface{}) interface{} {
|
||||
|
||||
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))
|
||||
phpValue(targetZval, param)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
22
types.h
22
types.h
@@ -8,7 +8,7 @@
|
||||
#include <Zend/zend_types.h>
|
||||
|
||||
zval *get_ht_packed_data(HashTable *, uint32_t index);
|
||||
Bucket *get_ht_bucket_data(HashTable *, uint32_t index);
|
||||
Bucket *get_ht_bucket(HashTable *);
|
||||
|
||||
void *__emalloc__(size_t size);
|
||||
void __efree__(void *ptr);
|
||||
@@ -19,13 +19,19 @@ int __zend_is_callable__(zval *cb);
|
||||
int __call_user_function__(zval *function_name, zval *retval,
|
||||
uint32_t param_count, zval params[]);
|
||||
|
||||
void __zval_null__(zval *zv);
|
||||
void __zval_bool__(zval *zv, bool val);
|
||||
void __zval_long__(zval *zv, zend_long val);
|
||||
void __zval_double__(zval *zv, double val);
|
||||
void __zval_string__(zval *zv, zend_string *str);
|
||||
void __zval_empty_string__(zval *zv);
|
||||
void __zval_arr__(zval *zv, zend_array *arr);
|
||||
zend_array *__zend_new_array__(uint32_t size);
|
||||
|
||||
zend_array *zend_hash_bulk_insert(zend_array *arr, size_t num_entries,
|
||||
size_t bulk_size, char *key1, char *key2,
|
||||
char *key3, char *key4, size_t key_len1,
|
||||
size_t key_len2, size_t key_len3,
|
||||
size_t key_len4, zval *val1, zval *val2,
|
||||
zval *val3, zval *val4);
|
||||
|
||||
zend_array *zend_hash_bulk_next_index_insert(zend_array *arr,
|
||||
size_t num_entries,
|
||||
size_t bulk_size, zval *val1,
|
||||
zval *val2, zval *val3,
|
||||
zval *val4);
|
||||
|
||||
#endif
|
||||
|
||||
131
types_test.go
131
types_test.go
@@ -1,6 +1,8 @@
|
||||
package frankenphp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"testing"
|
||||
|
||||
@@ -44,7 +46,7 @@ func TestPHPMap(t *testing.T) {
|
||||
}
|
||||
|
||||
phpArray := PHPMap(originalMap)
|
||||
defer zendHashDestroy(phpArray)
|
||||
defer zendArrayRelease(phpArray)
|
||||
convertedMap, err := GoMap[string](phpArray)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -63,7 +65,7 @@ func TestOrderedPHPAssociativeArray(t *testing.T) {
|
||||
}
|
||||
|
||||
phpArray := PHPAssociativeArray(originalArray)
|
||||
defer zendHashDestroy(phpArray)
|
||||
defer zendArrayRelease(phpArray)
|
||||
convertedArray, err := GoAssociativeArray[string](phpArray)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -76,7 +78,7 @@ func TestPHPPackedArray(t *testing.T) {
|
||||
originalSlice := []string{"bar1", "bar2"}
|
||||
|
||||
phpArray := PHPPackedArray(originalSlice)
|
||||
defer zendHashDestroy(phpArray)
|
||||
defer zendArrayRelease(phpArray)
|
||||
convertedSlice, err := GoPackedArray[string](phpArray)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -93,7 +95,7 @@ func TestPHPPackedArrayToGoMap(t *testing.T) {
|
||||
}
|
||||
|
||||
phpArray := PHPPackedArray(originalSlice)
|
||||
defer zendHashDestroy(phpArray)
|
||||
defer zendArrayRelease(phpArray)
|
||||
convertedMap, err := GoMap[string](phpArray)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -113,7 +115,7 @@ func TestPHPAssociativeArrayToPacked(t *testing.T) {
|
||||
expectedSlice := []string{"bar1", "bar2"}
|
||||
|
||||
phpArray := PHPAssociativeArray(originalArray)
|
||||
defer zendHashDestroy(phpArray)
|
||||
defer zendArrayRelease(phpArray)
|
||||
convertedSlice, err := GoPackedArray[string](phpArray)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -138,10 +140,127 @@ func TestNestedMixedArray(t *testing.T) {
|
||||
}
|
||||
|
||||
phpArray := PHPMap(originalArray)
|
||||
defer zendHashDestroy(phpArray)
|
||||
defer zendArrayRelease(phpArray)
|
||||
convertedArray, err := GoMap[any](phpArray)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, originalArray, convertedArray, "nested mixed array should be equal after conversion")
|
||||
})
|
||||
}
|
||||
|
||||
func benchOnPHPThread(b *testing.B, count int, cb func()) {
|
||||
globalLogger = slog.New(slog.NewTextHandler(io.Discard, nil))
|
||||
_, err := initPHPThreads(1, 1, nil) // boot 1 thread
|
||||
assert.NoError(b, err)
|
||||
handler := convertToTaskThread(phpThreads[0])
|
||||
|
||||
task := newTask(func() {
|
||||
for i := 0; i < count; i++ {
|
||||
cb()
|
||||
}
|
||||
})
|
||||
handler.execute(task)
|
||||
task.waitForCompletion()
|
||||
|
||||
drainPHPThreads()
|
||||
}
|
||||
|
||||
func BenchmarkBool(b *testing.B) {
|
||||
benchOnPHPThread(b, b.N, func() {
|
||||
phpBool := PHPValue(true)
|
||||
_, _ = GoValue[bool](phpBool)
|
||||
efree(phpBool)
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkInt(b *testing.B) {
|
||||
benchOnPHPThread(b, b.N, func() {
|
||||
phpInt := PHPValue(int64(42))
|
||||
_, _ = GoValue[int64](phpInt)
|
||||
efree(phpInt)
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkFloat(b *testing.B) {
|
||||
benchOnPHPThread(b, b.N, func() {
|
||||
phpFloat := PHPValue(3.14)
|
||||
_, _ = GoValue[float64](phpFloat)
|
||||
efree(phpFloat)
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkString(b *testing.B) {
|
||||
message := "Hello, World!"
|
||||
benchOnPHPThread(b, b.N, func() {
|
||||
phpString := PHPString(message, false)
|
||||
_ = GoString(phpString)
|
||||
zendStringRelease(phpString)
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkEmptyMap(b *testing.B) {
|
||||
originalMap := map[string]any{}
|
||||
benchOnPHPThread(b, b.N, func() {
|
||||
phpArray := PHPMap(originalMap)
|
||||
_, _ = GoMap[any](phpArray)
|
||||
zendArrayRelease(phpArray)
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkMap5Entries(b *testing.B) {
|
||||
originalMap := map[string]any{
|
||||
"foo1": "bar1",
|
||||
"foo2": int64(2),
|
||||
"foo3": true,
|
||||
"foo4": 3.14,
|
||||
"foo5": nil,
|
||||
}
|
||||
benchOnPHPThread(b, b.N, func() {
|
||||
phpArray := PHPMap(originalMap)
|
||||
_, _ = GoMap[any](phpArray)
|
||||
zendArrayRelease(phpArray)
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkAssociativeArray5Entries(b *testing.B) {
|
||||
originalArray := AssociativeArray[any]{
|
||||
Map: map[string]any{
|
||||
"foo1": "bar1",
|
||||
"foo2": int64(2),
|
||||
"foo3": true,
|
||||
"foo4": 3.14,
|
||||
"foo5": nil,
|
||||
},
|
||||
Order: []string{"foo3", "foo1", "foo4", "foo2", "foo5"},
|
||||
}
|
||||
benchOnPHPThread(b, b.N, func() {
|
||||
phpArray := PHPAssociativeArray(originalArray)
|
||||
_, _ = GoAssociativeArray[any](phpArray)
|
||||
zendArrayRelease(phpArray)
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkSlice5Entries(b *testing.B) {
|
||||
originalSlice := []any{"bar1", int64(2), true, 3.14, nil}
|
||||
benchOnPHPThread(b, b.N, func() {
|
||||
phpArray := PHPPackedArray(originalSlice)
|
||||
_, _ = GoPackedArray[any](phpArray)
|
||||
zendArrayRelease(phpArray)
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkMap50Entries(b *testing.B) {
|
||||
originalMap := map[string]any{}
|
||||
for i := 0; i < 10; i++ {
|
||||
originalMap[fmt.Sprintf("foo%d", i*5)] = fmt.Sprintf("val%d", i)
|
||||
originalMap[fmt.Sprintf("foo%d", i*5+1)] = "Error" // interned string
|
||||
originalMap[fmt.Sprintf("foo%d", i*5+2)] = true
|
||||
originalMap[fmt.Sprintf("foo%d", i*5+3)] = 3.12
|
||||
originalMap[fmt.Sprintf("foo%d", i*5+4)] = nil
|
||||
}
|
||||
benchOnPHPThread(b, b.N, func() {
|
||||
phpArray := PHPMap(originalMap)
|
||||
_, _ = GoMap[any](phpArray)
|
||||
zendArrayRelease(phpArray)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
type hotReloadOpt struct {
|
||||
hotReload []*watcher.PatternGroup
|
||||
hotReload []*watcher.PatternGroup
|
||||
}
|
||||
|
||||
var restartWorkers atomic.Bool
|
||||
|
||||
Reference in New Issue
Block a user