| name | php-ext-internals |
| description | Comprehensive Zend Engine API reference for writing PHP 8.x extensions in C23.
Covers module lifecycle, parameter parsing, zvals, zend_string, HashTable, object
handlers, memory management, globals, INI settings, arginfo/stub workflow, and
PHP 8 breakage traps derived from real codebase audit findings.
Use whenever implementing or reviewing PHP extension C code.
|
| allowed-tools | Read Grep Glob Bash |
You are acting as a PHP Internals expert. Apply the knowledge below precisely when
writing or reviewing C23 extension code. Prefer source reality over memory:
when unsure about a specific macro signature, grep php/8.5-debug-nts/src/Zend/.
1. Module Entry & Lifecycle
Every extension exposes one zend_module_entry with a get_module() symbol:
#include <php.h>
ZEND_BEGIN_MODULE_GLOBALS(myext)
zend_long request_count;
zend_string *last_error;
ZEND_END_MODULE_GLOBALS(myext)
ZEND_DECLARE_MODULE_GLOBALS(myext)
#define MYEXT_G(v) ZEND_MODULE_GLOBALS_ACCESSOR(myext, v)
PHP_GINIT_FUNCTION(myext) {
myext_globals->request_count = 0;
myext_globals->last_error = nullptr;
}
PHP_MINIT_FUNCTION(myext) {
REGISTER_INI_ENTRIES();
php_driver_define_MyClass();
return SUCCESS;
}
PHP_MSHUTDOWN_FUNCTION(myext) {
UNREGISTER_INI_ENTRIES();
return SUCCESS;
}
PHP_RINIT_FUNCTION(myext) {
MYEXT_G(request_count)++;
return SUCCESS;
}
PHP_RSHUTDOWN_FUNCTION(myext) {
if (MYEXT_G(last_error)) {
zend_string_release(MYEXT_G(last_error));
MYEXT_G(last_error) = nullptr;
}
return SUCCESS;
}
PHP_MINFO_FUNCTION(myext) {
php_info_print_table_start();
php_info_print_table_row(2, "MyExt support", "enabled");
php_info_print_table_end();
DISPLAY_INI_ENTRIES();
}
static const zend_function_entry myext_functions[] = {
PHP_FE(my_function, arginfo_my_function)
PHP_FE_END
};
zend_module_entry myext_module_entry = {
STANDARD_MODULE_HEADER,
"myext",
myext_functions,
PHP_MINIT(myext),
PHP_MSHUTDOWN(myext),
PHP_RINIT(myext),
PHP_RSHUTDOWN(myext),
PHP_MINFO(myext),
PHP_MYEXT_VERSION,
PHP_MODULE_GLOBALS(myext),
PHP_GINIT(myext),
nullptr,
nullptr,
STANDARD_MODULE_PROPERTIES_EX
};
#ifdef COMPILE_DL_MYEXT
# ifdef ZTS
ZEND_TSRMLS_CACHE_DEFINE()
# endif
ZEND_GET_MODULE(myext)
#endif
Lifecycle order (startup → shutdown):
GINIT → MINIT → [RINIT → RSHUTDOWN]* → MSHUTDOWN → GSHUTDOWN
2. Stub → Arginfo Workflow (preferred over manual arginfo)
Never write ZEND_BEGIN_ARG_INFO_EX by hand. Write a stub, generate arginfo.
Write stub (MyClass.stub.php)
<?php
declare(strict_types=1);
namespace Cassandra\MyModule {
/**
* @strict-properties
*/
final class MyClass {
public function withPort(int $port): static {}
public function withContactPoints(string ...$hosts): static {}
public function getPort(): int {}
public function getLabel(): ?string {}
}
}
Generate (_arginfo.h)
php php/8.5-debug-nts/src/build/gen_stub.php src/MyModule/MyClass.stub.php
Commit both MyClass.stub.php and MyClass_arginfo.h.
Include in implementation
BEGIN_EXTERN_C()
#include "MyClass_arginfo.h"
END_EXTERN_C()
Wiring a stub to an existing implementation
When a stub + generated arginfo exist but the .cpp still uses legacy patterns:
- Add
#include "MyClass_arginfo.h" inside BEGIN_EXTERN_C().
- Delete all
ZEND_BEGIN_ARG_INFO_EX / ZEND_END_ARG_INFO blocks.
- Delete the
PHP_ME(...) table and replace with the stub-generated method table
(named class_Cassandra_MyModule_MyClass_methods in the generated header).
- Replace
INIT_CLASS_ENTRY(ce, ...) + zend_register_internal_class(...) with
register_class_Cassandra_MyModule_MyClass().
- Change every
PHP_METHOD(ClassName, method) body header to
ZEND_METHOD(Cassandra_MyModule_ClassName, method).
3. Function & Method Declaration
PHP_FUNCTION(my_function) { }
ZEND_METHOD(Cassandra_MyModule_MyClass, withPort) { }
ZEND_THIS vs getThis() — always use ZEND_THIS in new code:
php_driver_foo_t *self = PHP_DRIVER_GET_FOO(getThis());
php_driver_foo_t *self = PHP_DRIVER_GET_FOO(ZEND_THIS);
4. Parameter Parsing (modern API only)
ZEND_METHOD(Cassandra_MyModule_MyClass, withPort) {
zend_long port;
ZEND_PARSE_PARAMETERS_START(1, 1)
Z_PARAM_LONG(port)
ZEND_PARSE_PARAMETERS_END();
if (port < 1 || port > 65535) {
zend_argument_value_error(1, "must be between 1 and 65535, %ld given", port);
RETURN_THROWS();
}
PHP_DRIVER_GET_MY_CLASS(ZEND_THIS)->port = (uint16_t)port;
RETURN_ZVAL(ZEND_THIS, 1, 0);
}
ZEND_PARSE_PARAMETERS_NONE();
Complete Z_PARAM_* macro list
| Macro | C type | PHP type |
|---|
Z_PARAM_BOOL(dest) | bool | bool |
Z_PARAM_BOOL_OR_NULL(dest, is_null) | bool, bool | ?bool |
Z_PARAM_LONG(dest) | zend_long | int |
Z_PARAM_LONG_OR_NULL(dest, is_null) | zend_long, bool | ?int |
Z_PARAM_DOUBLE(dest) | double | float |
Z_PARAM_DOUBLE_OR_NULL(dest, is_null) | double, bool | ?float |
Z_PARAM_STR(dest) | zend_string * | string — prefer over Z_PARAM_STRING |
Z_PARAM_STR_OR_NULL(dest) | zend_string * | ?string |
Z_PARAM_STRING(dest, dest_len) | char *, size_t | string — only when you need a raw char* |
Z_PARAM_ARRAY(dest) | zval * | array |
Z_PARAM_ARRAY_OR_NULL(dest) | zval * | ?array |
Z_PARAM_ARRAY_HT(dest) | HashTable * | array — use when you only need the HashTable |
Z_PARAM_ARRAY_HT_OR_NULL(dest) | HashTable * | ?array |
Z_PARAM_OBJ(dest) | zend_object * | object |
Z_PARAM_OBJ_OR_NULL(dest) | zend_object * | ?object |
Z_PARAM_OBJ_OF_CLASS(dest, ce) | zend_object * | ClassName — replaces Z_PARAM_OBJECT + manual instanceof |
Z_PARAM_OBJ_OF_CLASS_OR_NULL(dest, ce) | zend_object * | ?ClassName |
Z_PARAM_ZVAL(dest) | zval * | mixed — use sparingly; prefer typed variants |
Z_PARAM_FUNC(fci, fcc) | zend_fcall_info, zend_fcall_info_cache | callable |
Z_PARAM_FUNC_OR_NULL(fci, fcc) | same | ?callable |
Z_PARAM_VARIADIC('*', dest, count) | zval *, uint32_t | mixed... |
Z_PARAM_OPTIONAL | — | separates required/optional |
Z_PARAM_STR vs Z_PARAM_STRING: Z_PARAM_STR gives you zend_string* directly — prefer it. Only use Z_PARAM_STRING when you genuinely need a raw char* (e.g. passing to a C library). Using Z_PARAM_STRING then calling zend_string_init() on the result is a double-allocation.
5. Return Value Macros
RETURN_* sets return_value and returns from the C function.
RETVAL_* sets return_value without returning — use when you need cleanup after.
RETURN_NULL()
RETURN_TRUE / RETURN_FALSE
RETURN_BOOL(b)
RETURN_LONG(l)
RETURN_DOUBLE(d)
RETURN_STR(zend_string *)
RETURN_STR_COPY(zend_string *)
RETURN_STRING(const char *)
RETURN_STRINGL(char *, len)
RETURN_EMPTY_STRING()
RETURN_ARR(HashTable *)
RETURN_OBJ(zend_object *)
RETURN_OBJ_COPY(zend_object *)
RETURN_ZVAL(zv, copy, dtor)
RETURN_THROWS()
Fluent builder methods always end with:
RETURN_ZVAL(ZEND_THIS, 1, 0);
6. Zval Types & Operations
Zvals are never individually heap-allocated — they live on the stack or embedded in larger structures.
Type constants
IS_UNDEF, IS_NULL, IS_FALSE, IS_TRUE,
IS_LONG, IS_DOUBLE, IS_STRING, IS_ARRAY,
IS_OBJECT, IS_RESOURCE, IS_REFERENCE
Initialization macros
ZVAL_UNDEF(&z)
ZVAL_NULL(&z)
ZVAL_BOOL(&z, b)
ZVAL_LONG(&z, l)
ZVAL_DOUBLE(&z, d)
ZVAL_STR(&z, s)
ZVAL_STR_COPY(&z, s)
ZVAL_STRING(&z, "literal")
ZVAL_STRINGL(&z, str, len)
ZVAL_ARR(&z, ht)
ZVAL_EMPTY_ARRAY(&z)
ZVAL_OBJ(&z, obj)
ZVAL_OBJ_COPY(&z, obj)
Copying & moving
ZVAL_COPY_VALUE(&dst, &src)
ZVAL_COPY(&dst, &src)
ZVAL_COPY_DEREF(&dst, &src)
ZVAL_DEREF(&z)
Destruction
zval_ptr_dtor(&z)
zval_ptr_dtor_nogc(&z)
i_zval_ptr_dtor(&z)
Always call zval_ptr_dtor() before overwriting a live zval.
Type checking & reading
Z_TYPE(z)
Z_ISUNDEF(z)
Z_LVAL(z)
Z_DVAL(z)
Z_STR(z)
Z_STRVAL(z)
Z_STRLEN(z)
Z_ARR(z)
Z_OBJ(z)
Z_LVAL_P(zv) etc.
References
Z_ISREF_P(zv)
ZVAL_DEREF(zv)
ZVAL_UNREF(zv)
7. zend_string API
zend_string is a refcounted, length-prefixed, NUL-terminated string. Never use raw char* for PHP strings.
Accessors
ZSTR_VAL(s)
ZSTR_LEN(s)
ZSTR_HASH(s)
Allocation
zend_string *s = zend_string_init("hello", 5, false);
zend_string *s = ZSTR_INIT_LITERAL("hello", false);
zend_string *copy = zend_string_copy(s);
zend_string *dup = zend_string_dup(s, false);
Release
zend_string_release(s);
zend_string_release_ex(s, persistent);
Comparison
zend_string_equals(s1, s2)
zend_string_equals_ci(s1, s2)
zend_string_equals_literal(s, "literal")
zend_string_equals_literal_ci(s, "literal")
Interned strings
Interned strings are immutable, deduplicated, and shared across requests. Use for known constant identifiers:
zend_string *key = zend_string_init_interned("status", 6, true);
Smart string (building strings dynamically)
#include <ext/standard/php_smart_string.h>
smart_string buf = {0};
smart_string_appends(&buf, "hello ");
smart_string_append_long(&buf, 42);
smart_string_0(&buf);
smart_string_free(&buf);
8. HashTable / Array API
HashTable is the underlying type for PHP arrays. Keys are either zend_string* (string keys) or zend_ulong (integer keys).
Creation
array_init(return_value);
array_init_size(return_value, 8);
HashTable *ht = pemalloc(sizeof(HashTable), false);
zend_hash_init(ht, 8, nullptr, ZVAL_PTR_DTOR, false);
Adding values
add_assoc_null(arr, "key");
add_assoc_bool(arr, "key", 1);
add_assoc_long(arr, "key", 42L);
add_assoc_double(arr, "key", 3.14);
add_assoc_string(arr, "key", "value");
add_assoc_stringl(arr, "key", str, len);
add_assoc_zval(arr, "key", &zv);
add_index_long(arr, 0, 42L);
add_index_string(arr, 0, "value");
add_next_index_long(arr, 42L);
add_next_index_string(arr, "value");
add_next_index_zval(arr, &zv);
zend_hash_add(ht, key_str, &zv);
zend_hash_update(ht, key_str, &zv);
zend_hash_add_or_update(ht, key_str, &zv, HASH_ADD | HASH_UPDATE);
zend_hash_index_add(ht, idx, &zv);
zend_hash_index_update(ht, idx, &zv);
Lookup
zval *val = zend_hash_find(ht, key_str);
zval *val = zend_hash_str_find(ht, "key", sizeof("key")-1);
zval *val = zend_hash_index_find(ht, 0);
uint32_t n = zend_hash_num_elements(ht);
Iteration
zval *val;
ZEND_HASH_FOREACH_VAL(ht, val) {
} ZEND_HASH_FOREACH_END();
zend_string *key;
zval *val;
ZEND_HASH_FOREACH_STR_KEY_VAL(ht, key, val) {
} ZEND_HASH_FOREACH_END();
zend_ulong idx;
zend_string *key;
zval *val;
ZEND_HASH_FOREACH_KEY_VAL(ht, idx, key, val) {
} ZEND_HASH_FOREACH_END();
9. Object System
Internal struct layout (zendObject must be last)
typedef struct {
zend_long port;
zend_string *host;
zval callback;
zend_object zendObject;
} php_driver_my_class_t;
static inline php_driver_my_class_t *php_driver_my_class_fetch(zend_object *obj) {
return (php_driver_my_class_t *)((char *)obj - XtOffsetOf(php_driver_my_class_t, zendObject));
}
#define PHP_DRIVER_GET_MY_CLASS(zv) php_driver_my_class_fetch(Z_OBJ_P(zv))
Object handlers (MyClassHandlers.c)
The handler variable is a plain zend_object_handlers — not a wrapper struct. Access fields directly, never via .std.:
static zend_object_handlers php_driver_my_class_handlers;
void php_driver_initialize_my_class_handlers(void) {
memcpy(&php_driver_my_class_handlers,
zend_get_std_object_handlers(),
sizeof(zend_object_handlers));
php_driver_my_class_handlers.offset = XtOffsetOf(php_driver_my_class_t, zendObject);
php_driver_my_class_handlers.free_obj = php_driver_my_class_free;
php_driver_my_class_handlers.get_gc = php_driver_my_class_gc;
php_driver_my_class_handlers.get_properties = php_driver_my_class_properties;
php_driver_my_class_handlers.compare = php_driver_my_class_compare;
}
Constructor
zend_object *php_driver_my_class_new(zend_class_entry *ce) {
php_driver_my_class_t *self =
ecalloc(1, sizeof(php_driver_my_class_t) + zend_object_properties_size(ce));
ZVAL_UNDEF(&self->callback);
self->port = 9042;
zend_object_std_init(&self->zendObject, ce);
object_properties_init(&self->zendObject, ce);
self->zendObject.handlers = &php_driver_my_class_handlers;
return &self->zendObject;
}
Destructor
static void php_driver_my_class_free(zend_object *obj) {
php_driver_my_class_t *self = php_driver_my_class_fetch(obj);
if (self->host) {
zend_string_release(self->host);
self->host = nullptr;
}
if (!Z_ISUNDEF(self->callback)) {
zval_ptr_dtor(&self->callback);
ZVAL_UNDEF(&self->callback);
}
zend_object_std_dtor(obj);
}
GC handler
static HashTable *php_driver_my_class_gc(zend_object *obj, zval **table, int *n) {
php_driver_my_class_t *self = php_driver_my_class_fetch(obj);
zend_get_gc_buffer *gc_buf = zend_get_gc_buffer_create();
zend_get_gc_buffer_add_zval(gc_buf, &self->callback);
zend_get_gc_buffer_use(gc_buf, table, n);
return zend_std_get_properties(obj);
}
Properties handler
static HashTable *php_driver_my_class_properties(zend_object *obj) {
php_driver_my_class_t *self = php_driver_my_class_fetch(obj);
HashTable *props = zend_std_get_properties(obj);
zval zv;
ZVAL_LONG(&zv, self->port);
zend_hash_str_update(props, "port", sizeof("port")-1, &zv);
if (self->host) {
ZVAL_STR_COPY(&zv, self->host);
zend_hash_str_update(props, "host", sizeof("host")-1, &zv);
}
return props;
}
Compare handler
static int php_driver_my_class_compare(zval *a, zval *b) {
if (Z_TYPE_P(a) != IS_OBJECT || Z_TYPE_P(b) != IS_OBJECT
|| Z_OBJCE_P(a) != Z_OBJCE_P(b)) {
return 1;
}
php_driver_my_class_t *left = PHP_DRIVER_GET_MY_CLASS(a);
php_driver_my_class_t *right = PHP_DRIVER_GET_MY_CLASS(b);
return left->port != right->port;
}
Class registration (in MyClass.c)
zend_class_entry *php_driver_my_class_ce = nullptr;
void php_driver_define_MyClass(void) {
php_driver_my_class_ce = register_class_Cassandra_MyModule_MyClass();
php_driver_initialize_my_class_handlers();
php_driver_my_class_ce->create_object = php_driver_my_class_new;
}
Calling PHP from C (call_user_function)
zval retval;
zval args[2];
ZVAL_LONG(&args[0], 42);
ZVAL_STRING(&args[1], "hello");
if (call_user_function(nullptr, nullptr, &callable_zval,
&retval, 2, args) == SUCCESS
&& !EG(exception)) {
zval_ptr_dtor(&retval);
}
zval_ptr_dtor(&args[1]);
10. Memory Management
Allocators
| Lifetime | Alloc | Free |
|---|
| Request-bound | emalloc(n) | efree(p) |
| Request-bound, zeroed | ecalloc(1, n) | efree(p) |
| Request-bound, safe (overflow check) | safe_emalloc(n, size, extra) | efree(p) |
| Request-bound, string dup | estrdup(s) / estrndup(s,n) | efree(p) |
| Persistent (survives requests) | pemalloc(n, 1) | pefree(p, 1) |
| External C library | malloc(n) | free(p) |
Never mix allocators. emalloc memory freed with free() = heap corruption.
ecalloc argument order: ecalloc(count, size) — for objects always ecalloc(1, sizeof(T) + zend_object_properties_size(ce)). Do not swap the arguments.
Rules
zend_string* → zend_string_release() (not efree)
zend_object* → zend_object_release() or let Zend free via free_obj
- Zvals embedded in structs →
zval_ptr_dtor() before struct free
USE_ZEND_ALLOC=0 in env disables ZendMM for valgrind/asan runs
11. INI Settings
PHP_INI_BEGIN()
STD_PHP_INI_ENTRY("myext.connect_timeout", "5000", PHP_INI_ALL,
OnUpdateLong, connect_timeout,
zend_myext_globals, myext_globals)
STD_PHP_INI_BOOLEAN("myext.persistent", "1", PHP_INI_SYSTEM,
OnUpdateBool, use_persistent,
zend_myext_globals, myext_globals)
PHP_INI_END()
12. Constants & Error Handling
Constants
PHP_MINIT_FUNCTION(myext) {
REGISTER_LONG_CONSTANT("MYEXT_OPT_SSL", 1, CONST_CS | CONST_PERSISTENT);
REGISTER_STRING_CONSTANT("MYEXT_VERSION", PHP_MYEXT_VERSION, CONST_CS | CONST_PERSISTENT);
return SUCCESS;
}
Error reporting
zend_argument_type_error(1, "must be of type int, %s given", zend_zval_value_name(arg));
zend_argument_value_error(1, "must be positive, %ld given", val);
php_error_docref(nullptr, E_WARNING, "could not connect: %s", msg);
php_error_docref(nullptr, E_ERROR, "fatal: %s", msg);
if (EG(exception)) { RETURN_THROWS(); }
13. Object Handler Reference Table
All handlers live in zend_object_handlers (from zend_object_handlers.h).
Only set handlers you actually need; unset ones fall back to std handlers via memcpy.
| Handler | Purpose | When to implement |
|---|
free_obj | Release resources | Always |
get_gc | GC roots | When struct holds zvals/objects |
get_properties | var_dump, print_r | When struct has PHP-visible state |
compare | ==, <=> | When equality is meaningful |
clone_obj | clone $obj | When clone must deep-copy state |
read_property | $obj->prop | Custom property access |
write_property | $obj->prop = x | Custom property assignment |
has_property | isset($obj->prop) | Custom isset |
unset_property | unset($obj->prop) | Custom unset |
read_dimension | $obj[$k] | ArrayAccess-like |
write_dimension | $obj[$k] = v | ArrayAccess-like |
has_dimension | isset($obj[$k]) | ArrayAccess-like |
unset_dimension | unset($obj[$k]) | ArrayAccess-like |
count_elements | count($obj) | Countable |
cast_object | (string)$obj etc. | Type coercions |
do_operation | $a + $b | Operator overload |
get_debug_info | var_dump detail | Custom debug output |
Removed in PHP 8 — do not use:
compare_objects — replaced by compare. Assigning it writes into unrelated memory.
ZEND_ACC_CTOR flag — removed from PHP_ME/ZEND_ACC_*. Use ZEND_ACC_PUBLIC alone for constructors.
14. PHP 8 Breakage Traps (patterns that compiled in PHP 7, corrupt or crash in PHP 8)
These are anti-patterns found in this codebase during audit. Treat every occurrence as a bug.
compare_objects — out-of-bounds write
handlers.std.compare_objects = my_compare_fn;
handlers.compare = my_compare_fn;
Verified: grep -n compare /path/to/php/8.5-debug-nts/src/Zend/zend_object_handlers.h shows only compare, no compare_objects.
.std. handler prefix — wrong struct type
This codebase has a legacy php_driver_value_handlers wrapper:
typedef struct {
zend_object_handlers std;
php_driver_value_hash_t hash_value;
} php_driver_value_handlers;
Legacy modules declare static php_driver_value_handlers foo_handlers and access foo_handlers.std.get_gc. New modules must declare static zend_object_handlers foo_handlers and access foo_handlers.get_gc directly. Never use the wrapper type for new code.
ZEND_ACC_CTOR — removed flag
PHP_ME(Foo, __construct, arginfo__construct, ZEND_ACC_CTOR | ZEND_ACC_PUBLIC)
PHP_METHOD vs ZEND_METHOD
PHP_METHOD(Timeuuid, __construct) { ... }
ZEND_METHOD(Cassandra_DateTime_Timeuuid, __construct) { ... }
PHP_METHOD(Cls, method) expands to zim_Cls_method. ZEND_METHOD(Cassandra_Foo_Bar, method) expands to zim_Cassandra_Foo_Bar_method. The stub-generated method table references the ZEND_METHOD symbol — using PHP_METHOD with a short name produces an undefined reference at link time.
getThis() — unnecessary null check
php_driver_foo_t *self = PHP_DRIVER_GET_FOO(getThis());
php_driver_foo_t *self = PHP_DRIVER_GET_FOO(ZEND_THIS);
C++ casts → C casts
self->persist = static_cast<cass_bool_t>(enabled);
self->consistency = static_cast<CassConsistency>(val);
self->persist = (cass_bool_t)enabled;
self->consistency = (CassConsistency)val;
Double-refcount in get_properties
ZVAL_COPY_VALUE(&zv, &self->type);
Z_ADDREF_P(&zv);
add_next_index_zval(array, &zv);
ZVAL_COPY(&zv, &self->type);
add_next_index_zval(array, &zv);
15. Quick Patterns
Check argument is an instance of a specific class
zend_object *obj;
ZEND_PARSE_PARAMETERS_START(1, 1)
Z_PARAM_OBJ_OF_CLASS(obj, php_driver_my_class_ce)
ZEND_PARSE_PARAMETERS_END();
php_driver_my_class_t *self = php_driver_my_class_fetch(obj);
Return a new object
object_init_ex(return_value, php_driver_my_class_ce);
php_driver_my_class_t *result = PHP_DRIVER_GET_MY_CLASS(return_value);
result->port = 9042;
Throw a custom exception
zend_throw_exception(php_driver_invalid_argument_exception_ce,
"Port must be between 1 and 65535", 0);
RETURN_THROWS();
Iterate an array argument
HashTable *ht;
ZEND_PARSE_PARAMETERS_START(1, 1)
Z_PARAM_ARRAY_HT(ht)
ZEND_PARSE_PARAMETERS_END();
zval *item;
ZEND_HASH_FOREACH_VAL(ht, item) {
ZVAL_DEREF(item);
if (Z_TYPE_P(item) != IS_STRING) {
zend_argument_type_error(1, "must contain only strings");
RETURN_THROWS();
}
} ZEND_HASH_FOREACH_END();
Thread-safe module globals
ZEND_EXTERN_MODULE_GLOBALS(myext)
#define MYEXT_G(v) ZEND_MODULE_GLOBALS_ACCESSOR(myext, v)
MYEXT_G(connect_timeout) = 5000;
nullptr not NULL in new C23 code
zend_string *s = NULL;
self->host = NULL;
zend_string *s = nullptr;
self->host = nullptr;