Skip to content

Conversation

@AlliBalliBaba
Copy link
Contributor

This PR is inspired by some discussions like #1743. It's currently often not trivial to determine if a PHP request is running in worker mode or to generally access info about FrankenPHP from the PHP side.

This PR adds a frankenphp_info() function to retrieve debug info as an array from the PHP side. Still not sure about all the information that could be relevant.

The extension generator definitely helped building this function 👍 (assuming PHPArray can be safely used already)

*/
function apache_response_headers(): array|bool {}

function frankenphp_info(): array {}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have the array shape already so it can be part of the stub?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not yet, but good point! It might also be nice to add the FrankenPHP version, but I'm currently not sure if it's even available from inside of the process.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the PHPArray function, it might also make sense to instead allow direct convertion of either a slice or map to an array (unless that's something you already considered).

func SliceToPHPArray(slice []interface{})
func MapToPHPArray(map map[string]interface{})

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good idea on the paper. I think that we discussed this with @dunglas and the final word is that we'd like to avoid ending with a clunky API with to many functions. I'm not exactly sure if this was the conclusion of this discussion or another, but this is something that Kevin may confirm I guess

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would save a few functions though, since on the go side you just manipulate the slice or map.

Instead of:

type PHPKeyType int
type PHPKey struct
type Array struct
func (arr *Array) SetInt
func (arr *Array) SetString
func (arr *Array) Append
func (arr *Array) getNextIntKey
func (arr *Array) Len
func (arr *Array) At

you'd just need something like:

type Array = map[string]interface{}
func PHPArray(a Array) unsafe.Pointer
func GoArray(arr unsafe.Pointer) Array

type PackedArray = []interface{}
func PHPPackedArray(a PackedArray) unsafe.Pointer
func GoPackedArray(arr unsafe.Pointer) PackedArray

The unfortunate thing here is that the PHP Array is a union of the 'packed' and 'unpacked' hash table. Like this you leave it up to the user to determine what they need.

Would also solve the problem that currently duplicate keys are possible in the Array struct.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's also the problem that PHP hashmaps are ordered. But maybe this is no big deal as we cannot pass arguments by reference in our case. 🤔

Copy link
Contributor Author

@AlliBalliBaba AlliBalliBaba Jul 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah it potentially looses its order in the unpacked case, but would be cleaner and more performant otherwise. Also I could just do something like this 😄

return PHPArray(&Array{
	"frankenphp_version" : C.GoString(C.frankenphp_get_version().server_version),
	"thread_name":        thread.name(),
	"thread_index":       int(threadIndex),
	"is_worker_thread":   thread.handler.(*workerThread) != nil,
	"num_threads":        mainThread.numThreads,
	"max_threads":        mainThread.maxThreads,
})

Copy link
Contributor Author

@AlliBalliBaba AlliBalliBaba Jul 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Otherwise, if keeping the order is completely necessary, the correct data type on the go side would be an ordered map for the 'unpacked' array (sadly no built in ordered map by go)

The issues with the current implementation are mainly that duplicate keys are allowed on the go side and keys can be a mix of ints and strings. But exactly mirroring PHP Arrays probably cannot be done anyways without it getting too messy. So either mapping an array to a slice (if you need order) or a map (if you need association) seems like it makes most sense IMO.

Not sure though what others think

@alexandre-daubois
Copy link
Member

The extension generator definitely helped building this function 👍 (assuming PHPArray can be safely used already)

It should! If you encounter any blocker in this regard, I'll be happy to have a look

@AlliBalliBaba
Copy link
Contributor Author

Currently also seeing memory leaks with PHPArray in tests, not yet sure where from though.

@alexandre-daubois
Copy link
Member

That's weird, PHPArray uses emalloc(), which should take care of freeing memory automatically

@AlliBalliBaba
Copy link
Contributor Author

AlliBalliBaba commented Jul 25, 2025

I think the issue is that the zend_strings from zend_string_init are already allocated as a zval, so they don't need to be allocated twice, just cast to zval (in convertZvalToGo).

zval := (*C.zval)(C.__emalloc__(C.size_t(unsafe.Sizeof(C.zval{}))))

*(**C.zend_string)(v0) = (*C.zend_string)(PHPString(v, false))

Also I think there are dedicated zvals for true, false, null and an empty string if I remember correctly.

But you are right, if allocated with emalloc, it will be arena-deallocated, so these would only leak until a worker restarts (I assume).

@AlliBalliBaba
Copy link
Contributor Author

AlliBalliBaba commented Jul 25, 2025

Also the array keys in zend_hash_update are not actually registered with the array, just their hash is used. So these zend_strings also end up hidden from the garbage collector:

C.zend_hash_update(zendArray, (*C.zend_string)(keyStr), zval)

When coming from go it would be better to use zend_hash_str_add instead:

C.zend_hash_str_add(zendArray, toUnsafeChar(k.Str), (C.size_t)(len(k.Str)), zval)

@AlliBalliBaba
Copy link
Contributor Author

Hmm I might also be wrong about what specifically leaks though, might have to look into it further

@AlliBalliBaba
Copy link
Contributor Author

After looking over it again, it boils down to calling emalloc manually. Allocating manually like this also means the values have to be freed manually.

The correct way would be for PHP to handle allocation, so the GC has access to the values and can clean them up. An example would be just calling zend_new_array(size) here instead of emalloc and zend_hash_init

frankenphp/types.go

Lines 269 to 270 in 8175ae7

ht := C.__emalloc__(C.size_t(unsafe.Sizeof(C.HashTable{})))
C.__zend_hash_init__((*C.struct__zend_array)(ht), C.uint32_t(size), nil, C._Bool(false))

@alexandre-daubois
Copy link
Member

I'll have a look on Monday. Thank you for investigating!

@AlliBalliBaba
Copy link
Contributor Author

I'll wait with this PR until then 👍, no rush, I don't think many people have tried the extension builder yet.

You might end up needing wrapper functions for all of these macros, since they allocate with the GC in mind:

ZVAL_LONG 
ZVAL_DOUBLE 
ZVAL_TRUE 
ZVAL_FALSE 
ZVAL_NULL 
ZVAL_ARR
ZVAL_STRINGL_FAST 

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants