Skip to content

Commit b1ff48a

Browse files
authored
PHPC-2214: Int64 improvements (#1417)
* PHPC-1949: Allow construction of Int64 objects * PHPC-2213: Support casting of Int64 objects * PHPC-2212: Support overloaded operators for int64 * Conditionally declare zend_result type on PHP < 8 * Fix exception copypasta * Guard against division by zero for modulo operation * Refactor do_operator handler for Int64 * Support bitwise operators for Int64 * Add tests for inc/dec operations on Int64 * Ensure ZEND_POW yields correct results for Int64 The previous usage of pow() was problematic, as it returns a double which results in a loss of precision compared to int64_t. The new logic implements exponentiation by squaring to ensure that we're returning int64_t values where possible while guarding against overflow and reverting to PHP's default behaviour, which is to return a double. Since PHP's own implementation of this algorithm only supports int32_t on 32-bit platforms, we have to reimplement it to support the full int64_t range on 32-bit platforms. * Incorporate review feedback * Handle overflow on arithmetic operations * Expand tests for bitwise operations * Add test for invalid constructor values for Int64 * Add operator tests for ZEND_BOOL_NOT and ZEND_BOOL_XOR * Add clarifying comments for Int64 arithmetic operations * Support casting Int64 objects to float * Use PHP_INT_MIN constant instead of negation * Harden regular expression to match scientific notation * Create int64 instances directly in tests * Fix return types of increment/decrement operators * Add test for casting to float
1 parent 437512d commit b1ff48a

32 files changed

+897
-48
lines changed

src/BSON/Int64.c

Lines changed: 277 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#include <zend_smart_str.h>
1919
#include <ext/standard/php_var.h>
2020
#include <Zend/zend_interfaces.h>
21+
#include <Zend/zend_exceptions.h>
2122

2223
#include "php_phongo.h"
2324
#include "phongo_error.h"
@@ -86,7 +87,25 @@ HashTable* php_phongo_int64_get_properties_hash(phongo_compat_object_handler_typ
8687
return props;
8788
}
8889

89-
PHONGO_DISABLED_CONSTRUCTOR(MongoDB_BSON_Int64)
90+
static PHP_METHOD(MongoDB_BSON_Int64, __construct)
91+
{
92+
php_phongo_int64_t* intern;
93+
zval* value;
94+
95+
intern = Z_INT64_OBJ_P(getThis());
96+
97+
PHONGO_PARSE_PARAMETERS_START(1, 1)
98+
Z_PARAM_ZVAL(value);
99+
PHONGO_PARSE_PARAMETERS_END();
100+
101+
if (Z_TYPE_P(value) == IS_STRING) {
102+
php_phongo_int64_init_from_string(intern, Z_STRVAL_P(value), Z_STRLEN_P(value));
103+
} else if (Z_TYPE_P(value) == IS_LONG) {
104+
php_phongo_int64_init(intern, Z_LVAL_P(value));
105+
} else {
106+
phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT, "Expected value to be integer or string, %s given", PHONGO_ZVAL_CLASS_OR_TYPE_NAME_P(value));
107+
}
108+
}
90109

91110
/* Return the Int64's value as a string. */
92111
static PHP_METHOD(MongoDB_BSON_Int64, __toString)
@@ -244,6 +263,261 @@ static int php_phongo_int64_compare_objects(zval* o1, zval* o2)
244263
return 0;
245264
}
246265

266+
static zend_result php_phongo_int64_cast_object(phongo_compat_object_handler_type* readobj, zval* retval, int type)
267+
{
268+
php_phongo_int64_t* intern;
269+
270+
intern = Z_OBJ_INT64(PHONGO_COMPAT_GET_OBJ(readobj));
271+
272+
switch (type) {
273+
case IS_DOUBLE:
274+
ZVAL_DOUBLE(retval, (double) intern->integer);
275+
276+
return SUCCESS;
277+
278+
case IS_LONG:
279+
#if PHP_VERSION_ID >= 70300
280+
case _IS_NUMBER:
281+
#endif
282+
#if SIZEOF_ZEND_LONG == 4
283+
if (intern->integer > INT32_MAX || intern->integer < INT32_MIN) {
284+
zend_error(E_WARNING, "Truncating 64-bit integer value %" PRId64 " to 32 bits", intern->integer);
285+
}
286+
#endif
287+
288+
ZVAL_LONG(retval, intern->integer);
289+
290+
return SUCCESS;
291+
292+
case _IS_BOOL:
293+
ZVAL_BOOL(retval, intern->integer != 0);
294+
295+
return SUCCESS;
296+
297+
default:
298+
return zend_std_cast_object_tostring(readobj, retval, type);
299+
}
300+
}
301+
302+
/* Computes the power of two int64_t values by using the exponentiation by
303+
* squaring algorithm. This is necessary because in case the result exceeds
304+
* the range of a int64_t, we want PHP to return a float as it would when
305+
* using 64-bit values directly. We can't use anything involving zend_long
306+
* here as this would limit us to 32 bits on a 32-bit platform. This also
307+
* prohibits us from falling back to PHP's default functions after unwrapping
308+
* the int64_t from the php_phongo_int64_t instance. */
309+
static int64_t phongo_pow_int64(int64_t base, int64_t exp)
310+
{
311+
if (exp == 0) {
312+
return 1;
313+
}
314+
315+
if (exp % 2) {
316+
return base * phongo_pow_int64(base * base, (exp - 1) / 2);
317+
}
318+
319+
return phongo_pow_int64(base * base, exp / 2);
320+
}
321+
322+
#define OPERATION_RESULT_INT64(value) ZVAL_INT64_OBJ(result, value);
323+
324+
#define PHONGO_GET_INT64(int64, zval) \
325+
if (Z_TYPE_P((zval)) == IS_LONG) { \
326+
(int64) = Z_LVAL_P((zval)); \
327+
} else if (Z_TYPE_P((zval)) == IS_OBJECT && Z_OBJCE_P((zval)) == php_phongo_int64_ce) { \
328+
(int64) = Z_INT64_OBJ_P((zval))->integer; \
329+
} else { \
330+
return FAILURE; \
331+
}
332+
333+
#define INT64_SIGN_MASK INT64_MIN
334+
335+
/* Overload arithmetic operators for computation on int64_t values.
336+
* This ensures that any computation involving at least one php_phongo_int64_t
337+
* results in a php_phongo_int64_t value, regardless of whether the result
338+
* would fit in an int32_t or not. Results that exceed the 64-bit integer
339+
* range are returned as float as PHP would do when using 64-bit integers.
340+
* Note that ZEND_(PRE|POST)_(INC|DEC) are not handled here: when checking for
341+
* a do_operation handler for inc/dec, PHP calls the handler with a ZEND_ADD
342+
* or ZEND_SUB opcode and the same pointer for result and op1, and a ZVAL_LONG
343+
* of 1 for op2. */
344+
static zend_result php_phongo_int64_do_operation_ex(zend_uchar opcode, zval* result, zval* op1, zval* op2)
345+
{
346+
int64_t value1, value2, lresult;
347+
348+
PHONGO_GET_INT64(value1, op1);
349+
350+
switch (opcode) {
351+
case ZEND_ADD:
352+
PHONGO_GET_INT64(value2, op2);
353+
354+
lresult = value1 + value2;
355+
356+
/* The following is based on the logic in fast_long_add_function() in PHP.
357+
* If the result sign differs from the first operand sign, we have an overflow if:
358+
* - adding a positive to a positive yields a negative, or
359+
* - adding a negative to a negative (i.e. subtraction) yields a positive */
360+
if ((value1 & INT64_SIGN_MASK) != (lresult & INT64_SIGN_MASK) && (value1 & INT64_SIGN_MASK) == (value2 & INT64_SIGN_MASK)) {
361+
ZVAL_DOUBLE(result, (double) value1 + (double) value2);
362+
} else {
363+
OPERATION_RESULT_INT64(lresult);
364+
}
365+
366+
return SUCCESS;
367+
368+
case ZEND_SUB:
369+
PHONGO_GET_INT64(value2, op2);
370+
371+
lresult = value1 - value2;
372+
373+
/* The following is based on the logic in fast_long_sub_function() in PHP.
374+
* If the result sign differs from the first operand sign, we have an overflow if:
375+
* - subtracting a positive from a negative yields a positive, or
376+
* - subtracting a negative from a positive (i.e. addition) yields a negative */
377+
if ((value1 & INT64_SIGN_MASK) != (lresult & INT64_SIGN_MASK) && (value1 & INT64_SIGN_MASK) != (value2 & INT64_SIGN_MASK)) {
378+
ZVAL_DOUBLE(result, (double) value1 - (double) value2);
379+
} else {
380+
OPERATION_RESULT_INT64(lresult);
381+
}
382+
383+
return SUCCESS;
384+
385+
case ZEND_MUL:
386+
PHONGO_GET_INT64(value2, op2);
387+
388+
/* The following is based on the C-native implementation of
389+
* ZEND_SIGNED_MULTIPLY_LONG() in PHP if no other methods (e.g. ASM
390+
* or _builtin_smull_overflow) can be used. */
391+
{
392+
int64_t lres = value1 * value2;
393+
long double dres = (long double) value1 * (long double) value2;
394+
long double delta = (long double) lres - dres;
395+
396+
if ((dres + delta) != dres) {
397+
ZVAL_DOUBLE(result, dres);
398+
} else {
399+
OPERATION_RESULT_INT64(lres);
400+
}
401+
}
402+
403+
return SUCCESS;
404+
405+
case ZEND_DIV:
406+
PHONGO_GET_INT64(value2, op2);
407+
if (value2 == 0) {
408+
zend_throw_exception(zend_ce_division_by_zero_error, "Division by zero", 0);
409+
return FAILURE;
410+
}
411+
412+
/* The following is based on div_function_base() in PHP.
413+
* - INT64_MIN / 1 exceeds the int64 range -> return double
414+
* - if division has a remainder, return double as result can't be
415+
* an int */
416+
if ((value1 == INT64_MIN && value2 == -1) || (value1 % value2 != 0)) {
417+
ZVAL_DOUBLE(result, (double) value1 / value2);
418+
} else {
419+
OPERATION_RESULT_INT64(value1 / value2);
420+
}
421+
422+
return SUCCESS;
423+
424+
case ZEND_MOD:
425+
PHONGO_GET_INT64(value2, op2);
426+
if (value2 == 0) {
427+
zend_throw_exception(zend_ce_division_by_zero_error, "Division by zero", 0);
428+
return FAILURE;
429+
}
430+
431+
OPERATION_RESULT_INT64(value1 % value2);
432+
return SUCCESS;
433+
434+
case ZEND_SL:
435+
PHONGO_GET_INT64(value2, op2);
436+
OPERATION_RESULT_INT64(value1 << value2);
437+
return SUCCESS;
438+
439+
case ZEND_SR:
440+
PHONGO_GET_INT64(value2, op2);
441+
OPERATION_RESULT_INT64(value1 >> value2);
442+
return SUCCESS;
443+
444+
case ZEND_POW:
445+
PHONGO_GET_INT64(value2, op2);
446+
447+
// Negative exponents always yield floats, leave them for PHP to handle
448+
if (value2 < 0) {
449+
return FAILURE;
450+
}
451+
452+
// Handle 0 separately to distinguish between base 0 and
453+
// phongo_pow_int64 overflowing
454+
if (value1 == 0) {
455+
OPERATION_RESULT_INT64(0);
456+
return SUCCESS;
457+
}
458+
459+
{
460+
int64_t pow_result = phongo_pow_int64(value1, value2);
461+
462+
// If the result would overflow an int64_t, we let PHP fall back
463+
// to its default pow() implementation which returns a float.
464+
if (pow_result == 0) {
465+
return FAILURE;
466+
}
467+
468+
OPERATION_RESULT_INT64(pow_result);
469+
}
470+
471+
return SUCCESS;
472+
473+
case ZEND_BW_AND:
474+
PHONGO_GET_INT64(value2, op2);
475+
OPERATION_RESULT_INT64(value1 & value2);
476+
return SUCCESS;
477+
478+
case ZEND_BW_OR:
479+
PHONGO_GET_INT64(value2, op2);
480+
OPERATION_RESULT_INT64(value1 | value2);
481+
return SUCCESS;
482+
483+
case ZEND_BW_XOR:
484+
PHONGO_GET_INT64(value2, op2);
485+
OPERATION_RESULT_INT64(value1 ^ value2);
486+
return SUCCESS;
487+
488+
case ZEND_BW_NOT:
489+
OPERATION_RESULT_INT64(~value1);
490+
return SUCCESS;
491+
492+
default:
493+
return FAILURE;
494+
}
495+
}
496+
497+
static zend_result php_phongo_int64_do_operation(zend_uchar opcode, zval* result, zval* op1, zval* op2)
498+
{
499+
zval op1_copy;
500+
int retval;
501+
502+
// Copy op1 for unary operations (e.g. $int64++) to ensure correct return values
503+
if (result == op1) {
504+
ZVAL_COPY_VALUE(&op1_copy, op1);
505+
op1 = &op1_copy;
506+
}
507+
508+
retval = php_phongo_int64_do_operation_ex(opcode, result, op1, op2);
509+
510+
if (retval == SUCCESS && op1 == &op1_copy) {
511+
zval_ptr_dtor(op1);
512+
}
513+
514+
return retval;
515+
}
516+
517+
#undef OPERATION_RESULT_INT64
518+
#undef PHONGO_GET_INT64
519+
#undef INT64_SIGN_MASK
520+
247521
static HashTable* php_phongo_int64_get_debug_info(phongo_compat_object_handler_type* object, int* is_temp)
248522
{
249523
*is_temp = 1;
@@ -271,4 +545,6 @@ void php_phongo_int64_init_ce(INIT_FUNC_ARGS)
271545
php_phongo_handler_int64.get_properties = php_phongo_int64_get_properties;
272546
php_phongo_handler_int64.free_obj = php_phongo_int64_free_object;
273547
php_phongo_handler_int64.offset = XtOffsetOf(php_phongo_int64_t, std);
548+
php_phongo_handler_int64.cast_object = php_phongo_int64_cast_object;
549+
php_phongo_handler_int64.do_operation = php_phongo_int64_do_operation;
274550
}

src/BSON/Int64.stub.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@
99

1010
final class Int64 implements \JsonSerializable, Type, \Serializable
1111
{
12-
final private function __construct() {}
12+
#if PHP_VERSION_ID >= 80000
13+
final public function __construct(int|string $value) {}
14+
#else
15+
/** @param int|string $value */
16+
final public function __construct($value) {}
17+
#endif
1318

1419
final public function __toString(): string {}
1520

src/BSON/Int64_arginfo.h

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
11
/* This is a generated file, edit the .stub.php file instead.
2-
* Stub hash: a0fc48411f2ce18661e1e837b79f878f2d80ebd3 */
2+
* Stub hash: 68e80f37219bd526046f559b0c00d55dd6727a37 */
33

4-
ZEND_BEGIN_ARG_INFO_EX(arginfo_class_MongoDB_BSON_Int64___construct, 0, 0, 0)
4+
#if PHP_VERSION_ID >= 80000
5+
ZEND_BEGIN_ARG_INFO_EX(arginfo_class_MongoDB_BSON_Int64___construct, 0, 0, 1)
6+
ZEND_ARG_TYPE_MASK(0, value, MAY_BE_LONG|MAY_BE_STRING, NULL)
7+
ZEND_END_ARG_INFO()
8+
#endif
9+
10+
#if !(PHP_VERSION_ID >= 80000)
11+
ZEND_BEGIN_ARG_INFO_EX(arginfo_class_MongoDB_BSON_Int64___construct, 0, 0, 1)
12+
ZEND_ARG_INFO(0, value)
513
ZEND_END_ARG_INFO()
14+
#endif
615

716
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_MongoDB_BSON_Int64___toString, 0, 0, IS_STRING, 0)
817
ZEND_END_ARG_INFO()
@@ -31,7 +40,12 @@ ZEND_END_ARG_INFO()
3140
#endif
3241

3342

43+
#if PHP_VERSION_ID >= 80000
3444
static ZEND_METHOD(MongoDB_BSON_Int64, __construct);
45+
#endif
46+
#if !(PHP_VERSION_ID >= 80000)
47+
static ZEND_METHOD(MongoDB_BSON_Int64, __construct);
48+
#endif
3549
static ZEND_METHOD(MongoDB_BSON_Int64, __toString);
3650
static ZEND_METHOD(MongoDB_BSON_Int64, serialize);
3751
static ZEND_METHOD(MongoDB_BSON_Int64, unserialize);
@@ -46,7 +60,12 @@ static ZEND_METHOD(MongoDB_BSON_Int64, jsonSerialize);
4660

4761

4862
static const zend_function_entry class_MongoDB_BSON_Int64_methods[] = {
49-
ZEND_ME(MongoDB_BSON_Int64, __construct, arginfo_class_MongoDB_BSON_Int64___construct, ZEND_ACC_PRIVATE|ZEND_ACC_FINAL)
63+
#if PHP_VERSION_ID >= 80000
64+
ZEND_ME(MongoDB_BSON_Int64, __construct, arginfo_class_MongoDB_BSON_Int64___construct, ZEND_ACC_PUBLIC|ZEND_ACC_FINAL)
65+
#endif
66+
#if !(PHP_VERSION_ID >= 80000)
67+
ZEND_ME(MongoDB_BSON_Int64, __construct, arginfo_class_MongoDB_BSON_Int64___construct, ZEND_ACC_PUBLIC|ZEND_ACC_FINAL)
68+
#endif
5069
ZEND_ME(MongoDB_BSON_Int64, __toString, arginfo_class_MongoDB_BSON_Int64___toString, ZEND_ACC_PUBLIC|ZEND_ACC_FINAL)
5170
ZEND_ME(MongoDB_BSON_Int64, serialize, arginfo_class_MongoDB_BSON_Int64_serialize, ZEND_ACC_PUBLIC|ZEND_ACC_FINAL)
5271
ZEND_ME(MongoDB_BSON_Int64, unserialize, arginfo_class_MongoDB_BSON_Int64_unserialize, ZEND_ACC_PUBLIC|ZEND_ACC_FINAL)

src/phongo_compat.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#define PHONGO_COMPAT_H
1919

2020
#include <php.h>
21+
#include <Zend/zend_types.h>
2122
#include <Zend/zend_string.h>
2223
#include <Zend/zend_portability.h>
2324

@@ -116,6 +117,7 @@
116117
} \
117118
}
118119

120+
#define ZVAL_INT64_OBJ(_zv, _value) php_phongo_bson_new_int64((_zv), (_value))
119121
#if SIZEOF_ZEND_LONG == 8
120122
#define ADD_INDEX_INT64(_zv, _index, _value) add_index_long((_zv), (_index), (_value))
121123
#define ADD_NEXT_INDEX_INT64(_zv, _value) add_next_index_long((_zv), (_value))
@@ -327,4 +329,8 @@ const char* zend_get_object_type_case(const zend_class_entry* ce, zend_bool uppe
327329
zend_bool zend_array_is_list(zend_array* array);
328330
#endif /* PHP_VERSION_ID < 80100 */
329331

332+
#if PHP_VERSION_ID < 80000
333+
typedef ZEND_RESULT_CODE zend_result;
334+
#endif
335+
330336
#endif /* PHONGO_COMPAT_H */

0 commit comments

Comments
 (0)