Skip to content

Commit 798c6b2

Browse files
dktappsAkmalFairuz
andauthored
PHP 8.4 support (#148)
Co-authored-by: Akmal Fairuz <[email protected]>
1 parent 5011e76 commit 798c6b2

15 files changed

+697
-89
lines changed

.github/workflows/main.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ jobs:
1515
- 8.1.33
1616
- 8.2.29
1717
- 8.3.25
18+
- 8.4.12
1819

1920
uses: ./.github/workflows/main-php-matrix.yml
2021
with:
@@ -33,6 +34,8 @@ jobs:
3334
vs-crt: vs16
3435
- php: 8.3.25
3536
vs-crt: vs16
37+
- php: 8.4.12
38+
vs-crt: vs17
3639

3740
uses: ./.github/workflows/main-php-matrix-windows.yml
3841
with:

src/copy.c

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,13 @@ static zval* pmmpthread_copy_literals(const pmmpthread_ident_t* owner, zval *old
351351

352352
memcpy(memory, old, sizeof(zval) * last);
353353
while (literal < end) {
354-
if (pmmpthread_copy_zval(owner, literal, old_literal) == FAILURE) {
354+
if (Z_TYPE_P(old_literal) == IS_UNDEF) {
355+
/*
356+
* Literals may have unused holes in 8.4 due to compiler optimizations
357+
* See https://github.com/php/php-src/commit/1e7aac315ef1 (zend_compile_rope_finalize)
358+
*/
359+
ZVAL_UNDEF(literal);
360+
} else if (pmmpthread_copy_zval(owner, literal, old_literal) == FAILURE) {
355361
zend_error_at_noreturn(
356362
E_CORE_ERROR,
357363
filename,
@@ -437,6 +443,9 @@ static zend_op* pmmpthread_copy_opcodes(zend_op_array *op_array, zval *literals,
437443
case ZEND_JMP_NULL:
438444
#if PHP_VERSION_ID >= 80300
439445
case ZEND_BIND_INIT_STATIC_OR_JMP:
446+
#endif
447+
#if PHP_VERSION_ID >= 80400
448+
case ZEND_JMP_FRAMELESS:
440449
#endif
441450
opline->op2.jmp_addr = &copy[opline->op2.jmp_addr - op_array->opcodes];
442451
break;

src/handlers.c

Lines changed: 225 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,11 @@ HashTable* pmmpthread_read_debug(PMMPTHREAD_READ_DEBUG_PASSTHRU_D) {
4848
HashTable* pmmpthread_read_properties(PMMPTHREAD_READ_PROPERTIES_PASSTHRU_D) {
4949
pmmpthread_zend_object_t* threaded = PMMPTHREAD_FETCH_FROM(object);
5050

51+
#if PHP_VERSION_ID >= 80400
52+
zend_std_get_properties_ex(&threaded->std);
53+
#else
5154
rebuild_object_properties(&threaded->std);
55+
#endif
5256

5357
pmmpthread_store_tohash(
5458
&threaded->std, threaded->std.properties);
@@ -75,6 +79,81 @@ zval* pmmpthread_read_dimension(PMMPTHREAD_READ_DIMENSION_PASSTHRU_D) {
7579
return rv;
7680
}
7781

82+
#if PHP_VERSION_ID >= 80400
83+
/* {{{
84+
* Copy pasta from zend_object_handlers.c because the equivalent over there had to be static...
85+
*/
86+
static bool zend_is_in_hook(const zend_property_info *prop_info)
87+
{
88+
zend_execute_data *execute_data = EG(current_execute_data);
89+
if (!execute_data || !EX(func) || !EX(func)->common.prop_info) {
90+
return false;
91+
}
92+
93+
const zend_property_info *parent_info = EX(func)->common.prop_info;
94+
ZEND_ASSERT(prop_info->prototype && parent_info->prototype);
95+
return prop_info->prototype == parent_info->prototype;
96+
} /* }}} */
97+
98+
/* {{{
99+
* Copy pasta from zend_object_handlers.c because the equivalent over there had to be static...
100+
*/
101+
static bool zend_should_call_hook(const zend_property_info *prop_info, const zend_object *obj)
102+
{
103+
if (!zend_is_in_hook(prop_info)) {
104+
return true;
105+
}
106+
107+
/* execute_data and This are guaranteed to be set if zend_is_in_hook() returns true. */
108+
zend_object *parent_obj = Z_OBJ(EG(current_execute_data)->This);
109+
if (parent_obj == obj) {
110+
return false;
111+
}
112+
113+
/* pmmpthread objects can't be lazy */
114+
/*
115+
if (zend_object_is_lazy_proxy(parent_obj)
116+
&& zend_lazy_object_initialized(parent_obj)
117+
&& zend_lazy_object_get_instance(parent_obj) == obj) {
118+
return false;
119+
}
120+
*/
121+
122+
return true;
123+
} /* }}} */
124+
125+
/* {{{
126+
* Copy pasta from zend_object_handlers.c because the equivalent over there had to be static...
127+
*/
128+
static void zend_throw_no_prop_backing_value_access(zend_string *class_name, zend_string *prop_name, bool is_read)
129+
{
130+
zend_throw_error(NULL, "Must not %s virtual property %s::$%s",
131+
is_read ? "read from" : "write to",
132+
ZSTR_VAL(class_name), ZSTR_VAL(prop_name));
133+
} /* }}} */
134+
135+
/* {{{
136+
* Copy pasta from zend_object_handlers.c because the equivalent over there had to be static...
137+
*/
138+
static bool zend_call_get_hook(
139+
const zend_property_info *prop_info, zend_string *prop_name,
140+
zend_function *get, zend_object *zobj, zval *rv)
141+
{
142+
if (!zend_should_call_hook(prop_info, zobj)) {
143+
if (UNEXPECTED(prop_info->flags & ZEND_ACC_VIRTUAL)) {
144+
zend_throw_no_prop_backing_value_access(zobj->ce->name, prop_name, /* is_read */ true);
145+
}
146+
return false;
147+
}
148+
149+
GC_ADDREF(zobj);
150+
zend_call_known_instance_method_with_0_params(get, zobj, rv);
151+
OBJ_RELEASE(zobj);
152+
153+
return true;
154+
}
155+
#endif //PHP_VERSION_ID >= 80400
156+
78157
zval* pmmpthread_read_property(PMMPTHREAD_READ_PROPERTY_PASSTHRU_D) {
79158
zval zmember;
80159
zend_guard* guard;
@@ -87,9 +166,40 @@ zval* pmmpthread_read_property(PMMPTHREAD_READ_PROPERTY_PASSTHRU_D) {
87166
(*guard) &= ~IN_GET;
88167
} else {
89168
zend_property_info* info = zend_get_property_info(object->ce, member, 0);
169+
90170
if (info == ZEND_WRONG_PROPERTY_INFO) {
91-
rv = &EG(uninitialized_zval);
92-
} else if (info == NULL || (info->flags & ZEND_ACC_STATIC) != 0) { //dynamic property
171+
return &EG(uninitialized_zval);
172+
}
173+
174+
if (info != NULL) {
175+
if (info->flags & ZEND_ACC_STATIC) {
176+
info = NULL;
177+
#if PHP_VERSION_ID >= 80400
178+
} else if (info->hooks != NULL) {
179+
zend_function* get = info->hooks[ZEND_PROPERTY_HOOK_GET];
180+
if (!get) {
181+
if (info->flags & ZEND_ACC_VIRTUAL) {
182+
zend_throw_error(NULL, "Property %s::$%s is write-only",
183+
ZSTR_VAL(object->ce->name), ZSTR_VAL(member));
184+
return &EG(uninitialized_zval);
185+
}
186+
//php-src has some checks for indirection here - we don't need these
187+
//since we don't allow indirect modification on ThreadSafe objects anyway
188+
189+
//fallthru to pmmpthread_store_read() below
190+
} else {
191+
if (zend_call_get_hook(info, member, get, object, rv)) {
192+
return rv;
193+
}
194+
if (EG(exception)) {
195+
return &EG(uninitialized_zval);
196+
}
197+
}
198+
#endif
199+
}
200+
}
201+
202+
if (info == NULL) { //dynamic property
93203
if (pmmpthread_store_read(object, &zmember, type, rv) == FAILURE) {
94204
if (type != BP_VAR_IS) {
95205
zend_error(E_WARNING, "Undefined property: %s::$%s", ZSTR_VAL(object->ce->name), ZSTR_VAL(member));
@@ -137,11 +247,11 @@ void pmmpthread_write_dimension(PMMPTHREAD_WRITE_DIMENSION_PASSTHRU_D) {
137247

138248
zval* pmmpthread_write_property(PMMPTHREAD_WRITE_PROPERTY_PASSTHRU_D) {
139249
zval zmember;
140-
zval tmp;
250+
zval coerced_value;
141251
zend_guard* guard;
142252

143253
ZVAL_STR(&zmember, member);
144-
ZVAL_UNDEF(&tmp);
254+
ZVAL_UNDEF(&coerced_value);
145255

146256
if (object->ce->__set && (guard = zend_get_property_guard(object, member)) && !((*guard) & IN_SET)) {
147257
zval rv;
@@ -154,42 +264,83 @@ zval* pmmpthread_write_property(PMMPTHREAD_WRITE_PROPERTY_PASSTHRU_D) {
154264
if (Z_TYPE(rv) != IS_UNDEF)
155265
zval_dtor(&rv);
156266
} else {
157-
bool ok = true;
267+
bool write_store = true;
158268
zend_property_info* info = zend_get_property_info(object->ce, member, 0);
159-
if (info != ZEND_WRONG_PROPERTY_INFO) {
160-
if (info != NULL && (info->flags & ZEND_ACC_STATIC) == 0) {
161-
ZVAL_STR(&zmember, info->name); //use mangled name to avoid private member shadowing issues
269+
if (info == ZEND_WRONG_PROPERTY_INFO) {
270+
return &EG(error_zval);
271+
}
162272

273+
//zend_verify_property_type() might modify the value
274+
//value is not copied before we receive it, so it might be
275+
//from opcache protected memory which we can't modify
276+
ZVAL_COPY(&coerced_value, value);
277+
278+
if (info != NULL && (info->flags & ZEND_ACC_STATIC) == 0) {
279+
ZVAL_STR(&zmember, info->name); //use mangled name to avoid private member shadowing issues
280+
281+
#if PHP_VERSION_ID >= 80400
282+
if (info->hooks != NULL) {
283+
zend_function* set = info->hooks[ZEND_PROPERTY_HOOK_SET];
284+
285+
if (!set) {
286+
if (info->flags & ZEND_ACC_VIRTUAL) {
287+
zend_throw_error(NULL, "Property %s::$%s is read-only", ZSTR_VAL(object->ce->name), ZSTR_VAL(member));
288+
value = &EG(error_zval);
289+
write_store = false;
290+
}
291+
//fallthru to normal write
292+
} else if (!zend_should_call_hook(info, object)) {
293+
if (info->flags & ZEND_ACC_VIRTUAL) {
294+
zend_throw_no_prop_backing_value_access(object->ce->name, member, /* is_read */ false);
295+
value = &EG(error_zval);
296+
write_store = false;
297+
}
298+
//fallthru to normal write
299+
} else {
300+
//call hook or die trying
301+
write_store = false;
302+
if (UNEXPECTED(info->flags & ZEND_ACC_PPP_SET_MASK
303+
&& !zend_asymmetric_property_has_set_access(info))) {
304+
zend_asymmetric_visibility_property_modification_error(info, "modify");
305+
value = &EG(error_zval);
306+
} else {
307+
GC_ADDREF(object);
308+
zend_call_known_instance_method_with_1_params(set, object, NULL, value);
309+
OBJ_RELEASE(object);
310+
}
311+
}
312+
}
313+
#endif
314+
if (write_store) { //if hooked, the setter will do the type verification, so we can skip this
163315
zend_execute_data* execute_data = EG(current_execute_data);
164316
bool strict = execute_data
165317
&& execute_data->func
166318
&& ZEND_CALL_USES_STRICT_TYPES(EG(current_execute_data));
167319

168-
//zend_verify_property_type() might modify the value
169-
//value is not copied before we receive it, so it might be
170-
//from opcache protected memory which we can't modify
171-
ZVAL_COPY(&tmp, value);
172-
value = &tmp;
173-
174-
if (ZEND_TYPE_IS_SET(info->type) && !zend_verify_property_type(info, value, strict)) {
175-
ok = false;
320+
if (ZEND_TYPE_IS_SET(info->type) && !zend_verify_property_type(info, &coerced_value, strict)) {
321+
write_store = false;
176322
}
177323
}
324+
}
178325

179-
if (ok && pmmpthread_store_write(object, &zmember, value, PMMPTHREAD_STORE_NO_COERCE_ARRAY) == FAILURE && !EG(exception)) {
180-
zend_throw_error(
181-
pmmpthread_ce_nts_value_error,
182-
"Cannot assign non-thread-safe value of type %s to thread-safe class property %s::$%s",
183-
zend_zval_type_name(value),
184-
ZSTR_VAL(object->ce->name),
185-
ZSTR_VAL(member)
186-
);
187-
}
326+
if (write_store && pmmpthread_store_write(object, &zmember, &coerced_value, PMMPTHREAD_STORE_NO_COERCE_ARRAY) == FAILURE && !EG(exception)) {
327+
zend_throw_error(
328+
pmmpthread_ce_nts_value_error,
329+
"Cannot assign non-thread-safe value of type %s to thread-safe class property %s::$%s",
330+
zend_zval_type_name(value),
331+
ZSTR_VAL(object->ce->name),
332+
ZSTR_VAL(member)
333+
);
188334
}
189335
}
190336

191-
zval_ptr_dtor(&tmp);
337+
zval_ptr_dtor(&coerced_value);
192338

339+
//technically, this should return the coercedValue, but PHP doesn't give us a way to do that
340+
//and we can't return a ptr to a stack variable
341+
//so instead this will mirror the behaviour of a hooked property and return the original, non-coerced value
342+
//this is an unfortunate deviation from the standard behaviour, but since we should only see coercion in
343+
//weak mode anyway, it shouldn't cause anyone any serious headaches.
193344
return EG(exception) ? &EG(error_zval) : value;
194345
}
195346
/* }}} */
@@ -222,16 +373,55 @@ int pmmpthread_has_property(PMMPTHREAD_HAS_PROPERTY_PASSTHRU_D) {
222373
(*guard) &= ~IN_ISSET;
223374

224375
if (Z_TYPE(rv) != IS_UNDEF) {
376+
//TODO: this doesn't account for ZEND_PROPERTY_NOT_EMPTY
225377
isset = zend_is_true(&rv);
226378
zval_dtor(&rv);
227379
}
228380
} else {
229381
zend_property_info* info = zend_get_property_info(object->ce, member, 1);
382+
bool read_store = true;
230383
if (info != ZEND_WRONG_PROPERTY_INFO) {
231384
if (info != NULL && (info->flags & ZEND_ACC_STATIC) == 0) {
232-
ZVAL_STR(&zmember, info->name); //defined property, use mangled name
385+
ZVAL_STR(&zmember, info->name);
386+
#if PHP_VERSION_ID >= 80400
387+
if (info->hooks != NULL) {
388+
zend_function* get = info->hooks[ZEND_PROPERTY_HOOK_GET];
389+
390+
if (has_set_exists == ZEND_PROPERTY_EXISTS) {
391+
isset = 1;
392+
read_store = false;
393+
} else if (get == NULL) {
394+
if (info->flags & ZEND_ACC_VIRTUAL) {
395+
zend_throw_error(NULL, "Property %s::$%s is write-only",
396+
ZSTR_VAL(object->ce->name), ZSTR_VAL(member));
397+
isset = 0;
398+
read_store = false;
399+
}
400+
401+
//fallthru to store read
402+
} else {
403+
zval rv;
404+
if (zend_call_get_hook(info, member, get, object, &rv)) {
405+
read_store = false;
406+
if (has_set_exists == ZEND_PROPERTY_NOT_EMPTY) {
407+
isset = zend_is_true(&rv);
408+
} else {
409+
ZEND_ASSERT(has_set_exists == ZEND_PROPERTY_ISSET);
410+
isset = Z_TYPE(rv) != IS_NULL
411+
&& (Z_TYPE(rv) != IS_REFERENCE || Z_TYPE_P(Z_REFVAL(rv)) != IS_NULL);
412+
}
413+
zval_ptr_dtor(&rv);
414+
} else if (EG(exception)) {
415+
read_store = false;
416+
}
417+
}
418+
}
419+
#endif
420+
}
421+
422+
if (read_store) {
423+
isset = pmmpthread_store_isset(object, &zmember, has_set_exists);
233424
}
234-
isset = pmmpthread_store_isset(object, &zmember, has_set_exists);
235425
} else isset = 0;
236426
}
237427
return isset;
@@ -270,6 +460,13 @@ void pmmpthread_unset_property(PMMPTHREAD_UNSET_PROPERTY_PASSTHRU_D) {
270460
if (info != ZEND_WRONG_PROPERTY_INFO) {
271461
if (info != NULL && (info->flags & ZEND_ACC_STATIC) == 0) {
272462
ZVAL_STR(&zmember, info->name); //defined property, use mangled name
463+
#if PHP_VERSION_ID >= 80400
464+
if (info->hooks != NULL) {
465+
zend_throw_error(NULL, "Cannot unset hooked property %s::$%s",
466+
ZSTR_VAL(object->ce->name), ZSTR_VAL(member));
467+
return;
468+
}
469+
#endif
273470
}
274471
pmmpthread_store_delete(object, &zmember);
275472
}

src/object.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ static inline void pmmpthread_base_write_property_defaults(pmmpthread_zend_objec
248248
zval* value;
249249
int result;
250250

251-
if (info->flags & ZEND_ACC_STATIC) {
251+
if (!PMMPTHREAD_OBJECT_PROPERTY(info)) {
252252
continue;
253253
}
254254

src/pmmpthread.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,12 @@ typedef struct _pmmpthread_call_t {
163163

164164
#define PMMPTHREAD_CALL_EMPTY {empty_fcall_info, empty_fcall_info_cache}
165165

166+
#if PHP_VERSION_ID >= 80400
167+
#define PMMPTHREAD_OBJECT_PROPERTY(prop_info) ((prop_info->flags & (ZEND_ACC_STATIC | ZEND_ACC_VIRTUAL)) == 0)
168+
#else
169+
#define PMMPTHREAD_OBJECT_PROPERTY(prop_info) ((prop_info->flags & ZEND_ACC_STATIC) == 0)
170+
#endif
171+
166172
/* this is a copy of the same struct in zend_closures.c, which unfortunately isn't exported */
167173
typedef struct _zend_closure {
168174
zend_object std;

0 commit comments

Comments
 (0)