feat(native-class): enable direct binding of fixed properties to typed references

- Allow int, float, bool, string, and array Native Class properties to be passed
  directly to matching reference parameters on statically resolved calls
- Generate call-scoped C++ T& bindings for typed reference parameters instead
  of creating Zend references
- Maintain compile-time rooting of Native receiver objects during typed calls
- Add new parseNativeTypedPropertyReferenceArg method to handle typed property
  reference arguments
- Update documentation to reflect new typed reference capabilities for fixed
  Native properties
- Create new test case covering typed property reference functionality
- Modify changelog to document the
master
韩天峰 1 month ago
parent 91f133f17a
commit 011cb31882
  1. 14
      CHANGELOG.md
  2. 6
      docs/en/INCOMPATIBLE_PHP_FEATURES.md
  3. 8
      docs/en/NATIVE_CLASS_OBJECT.md
  4. 62
      src/NativeClass/NativeClassSupportTrait.php
  5. 18
      src/TypeSystem/NativeTypeCompatibilityTrait.php
  6. 10
      tests/compiler/native-class/any-property-reference.phpt
  7. 84
      tests/compiler/native-class/typed-property-reference.phpt

@ -32,6 +32,14 @@ box the value or allocate a Zend reference. Bindings must be unconditional,
one-time, function-local, and non-escaping. Rebinding, `unset`, reference one-time, function-local, and non-escaping. Rebinding, `unset`, reference
capture/return, and storing such a reference into PHP storage are rejected. capture/return, and storing such a reference into PHP storage are rejected.
Fixed `int`, `float`, `bool`, `string`, and `array` Native Class properties can
now be passed directly to an exactly matching reference parameter on a
statically resolved call. This lowers to a call-scoped C++ `T&` while precisely
rooting the Native receiver. It does not enable general PHP references for the
field: `=&`, `std::ref()`, dynamic calls, and escaping reference forms remain
forbidden. Explicit `any` properties continue to use the dynamic Zend reference
model.
Dynamic calls and Closures retain the existing Zend reference path and require Dynamic calls and Closures retain the existing Zend reference path and require
explicit `std::ref()` / `toRef()`. A call-scoped bridge validates the value on explicit `std::ref()` / `toRef()`. A call-scoped bridge validates the value on
write-back and rejects an escaping temporary reference. Use `std::any()` for write-back and rejects an escaping temporary reference. Use `std::any()` for
@ -112,6 +120,12 @@ should review the change log and run their full test suite before upgrading.
不装箱、不创建 Zend reference。绑定必须位于函数顶层、只发生一次且不得逃逸;重新绑定、 不装箱、不创建 Zend reference。绑定必须位于函数顶层、只发生一次且不得逃逸;重新绑定、
`unset`、引用捕获/返回,或把引用存入 PHP 槽位都会在编译期拒绝。 `unset`、引用捕获/返回,或把引用存入 PHP 槽位都会在编译期拒绝。
Native Class 中固定类型为 `int`、`float`、`bool`、`string`、`array` 的属性,现在也可
直接传给静态可解析调用中类型完全匹配的引用参数。编译器将其生成为仅在本次调用期间
有效的 C++ `T&`,同时精确保活 Native 接收对象;这并不会为字段开放通用 PHP 引用,
`=&`、`std::ref()`、动态调用及其他可能逃逸的引用形式仍被禁止。显式声明为 `any` 的
属性继续使用动态 Zend reference 模型。
动态调用与 Closure 继续使用既有 Zend reference 路径,并要求显式使用 `std::ref()` / 动态调用与 Closure 继续使用既有 Zend reference 路径,并要求显式使用 `std::ref()` /
`toRef()`。调用级 bridge 在返回时检查类型并拒绝临时引用逃逸;需要完整 PHP 引用身份时 `toRef()`。调用级 bridge 在返回时检查类型并拒绝临时引用逃逸;需要完整 PHP 引用身份时
应使用 `std::any()`。固定 object/resource/stream、高精度值、Native/typed object、Box 应使用 `std::any()`。固定 object/resource/stream、高精度值、Native/typed object、Box

@ -122,6 +122,12 @@ incompatible with or more restrictive than standard PHP.
slot would weaken the type system. Typed object/static properties remain slot would weaken the type system. Typed object/static properties remain
reference-capable because Zend attaches property type sources; PHP array reference-capable because Zend attaches property type sources; PHP array
elements remain dynamic reference-capable slots. elements remain dynamic reference-capable slots.
- A fixed `int`, `float`, `bool`, `string`, or `array` Native Class property may
be passed directly to an exactly matching reference parameter on a statically
resolved call. This is a call-scoped C++ `T&`, not a PHP reference: `=&`,
`std::ref()`, dynamic calls, reference returns, and other escaping forms remain
forbidden for fixed Native properties. Only a Native property explicitly
declared `any` supports the ordinary dynamic PHP reference model.
- A call that uses argument unpacking followed by named arguments falls back to - A call that uses argument unpacking followed by named arguments falls back to
dynamic dispatch and cannot use the native call path. dynamic dispatch and cannot use the native call path.

@ -274,10 +274,12 @@ Allowing fields to hold ZendVM values does not mean the Native Class Object itse
Whether a Native property may be taken by reference must be decided entirely at compile time from declaration metadata, without generating runtime type branches: Whether a Native property may be taken by reference must be decided entirely at compile time from declaration metadata, without generating runtime type branches:
- A fixed `bool`, `int`, `float`, `string`, or `array` field may be passed directly to an exactly matching typed-reference parameter on a statically resolved call. The compiler binds the field as a call-scoped C++ `T&`; no `zval` or Zend reference is created, and the receiver remains precisely rooted through the complete statement.
- This typed-call path does not make general PHP reference acquisition legal. Fixed fields still reject `$ref =& $object->property`, `std::ref($object->property)`, returning by reference, dynamic calls, and every other form in which the reference could escape the statically resolved call.
- Only `any` properties allow `$ref =& $object->property`; this is an explicit choice to allow Zend dynamic code to replace the slot value. - Only `any` properties allow `$ref =& $object->property`; this is an explicit choice to allow Zend dynamic code to replace the slot value.
- `mixed` also uses `php::Var` storage but still rejects taking references; in Native Class all declared types except `any` must maintain compile-time type constraints. - `mixed` also uses `php::Var` storage but still rejects taking references; in Native Class all declared types except `any` must maintain compile-time type constraints.
- `bool`, `int`, `float`, and other fixed-layout fields cannot represent PHP references and are rejected at compile time. - `bool`, `int`, `float`, `string`, and `array` fields cannot represent general PHP references; their only additional reference form is the exact, statically resolved typed-call path above.
- `string`, `array`, `object`, Stream, and high-precision types have PHPX wrapper layers but are still fixed declared types; reference writes would bypass type constraints, so they are rejected at compile time. - `object`, Stream, and high-precision types have PHPX wrapper layers but are still fixed declared types; reference writes would bypass type constraints, so they are rejected at compile time.
- nullable, union, and intersection constrained `php::Var` fields also reject references; they cannot be allowed merely because the underlying storage is also `php::Var`. - nullable, union, and intersection constrained `php::Var` fields also reject references; they cannot be allowed merely because the underlying storage is also `php::Var`.
- Properties with Property Hook have no physical slot to expose, and always reject references. - Properties with Property Hook have no physical slot to expose, and always reject references.
@ -1201,7 +1203,7 @@ Explicit conversion makes the allocation cost and the object-graph conversion bo
| Nullable Native parameters/returns | Supports `?NativeClass`, denoted by `nullptr`; member access must check or first prove non-null | | Nullable Native parameters/returns | Supports `?NativeClass`, denoted by `nullptr`; member access must check or first prove non-null |
| `&` on Native parameters/returns | Not supported; compile-time FatalError | | `&` on Native parameters/returns | Not supported; compile-time FatalError |
| Taking references to Native Object variables | Not supported; ordinary assignment already shares object identity | | Taking references to Native Object variables | Not supported; ordinary assignment already shares object identity |
| Taking references to Native properties | Only fields explicitly declared `any` are supported; all other fields including `mixed` are compile-time FatalError | | Taking references to Native properties | Fixed `bool`/`int`/`float`/`string`/`array` fields can be passed directly to an exact typed-reference parameter on a statically resolved call; only explicit `any` supports general PHP references; all other forms are compile-time FatalError |
| Native variadic, union/intersection | Not supported; compile-time FatalError | | Native variadic, union/intersection | Not supported; compile-time FatalError |
| `__construct()` | Supported | | `__construct()` | Supported |
| `clone` / `__clone()` | Supported | | `clone` / `__clone()` | Supported |

@ -919,13 +919,69 @@ trait NativeClassSupportTrait
} }
} }
/**
* Lower a fixed Native property directly to the matching typed-reference
* ABI. The receiver is materialized as a precise Native root before the
* final C++ call so argument evaluation order cannot rebind or collect it.
*
* This is intentionally narrower than PHP reference acquisition: the
* resulting T& exists only for the statically resolved call. It does not
* make `$alias =& $object->property` or std::ref($object->property) legal.
*/
protected function parseNativeTypedPropertyReferenceArg(
NodeAbstract $expr,
string $referenceType,
NodeAbstract $errorNode,
): ?string {
if (!$expr instanceof Node\Expr\PropertyFetch) {
return null;
}
$receiverClass = $this->detectClassOfExpr($expr->var);
if (!$this->isNativeObjectClass($receiverClass)) {
return null;
}
if (!$expr->name instanceof Node\Identifier) {
$this->fatalError($errorNode, 'Dynamic native object property access is not supported');
}
$property = $expr->name->toString();
$resolution = $this->resolveNativeInstanceProperty($expr, $property, $receiverClass);
if ($resolution === null) {
$this->fatalError(
$errorNode,
"Native class `{$receiverClass}` has no property `\${$property}`",
);
}
$this->applyNativePropertyAccessResult($expr, $resolution);
$definition = $resolution->propertyDef;
if ($definition->nullable
|| $definition->getter !== null
|| $definition->setter !== null
|| Type::getReferenceType($definition->type) !== $referenceType
) {
return null;
}
$this->assertReadonlyPropertyReferenceForbidden($expr, $errorNode, false);
$this->assertPropertySetVisibility($expr);
$receiver = $this->materializeNativeObjectReceiver($expr->var, $receiverClass);
$this->setNativePropertyValueSource($expr, self::NATIVE_PROPERTY_VALUE_VAR);
return $this->getNativeObjectMemberReceiver($receiver)
. $this->getNativeObjectPropertyCppName($definition, $resolution->classDef);
}
/** /**
* Validate a Native reference entirely from compile-time metadata. * Validate a Native reference entirely from compile-time metadata.
* *
* A Native object variable is a typed pointer and must never expose its * A Native object variable is a typed pointer and must never expose its
* pointer slot as a PHP reference. A Native property may expose a reference * pointer slot as a PHP reference. A Native property may expose a Zend
* only when it was explicitly declared `any`: that field intentionally * reference only when it was explicitly declared `any`: that field
* permits arbitrary PHP values. Every other declaration, including * intentionally permits arbitrary PHP values. Fixed fields passed directly
* to an exact typed-reference parameter are handled before this check and
* never expose a Zend reference. Every other declaration, including
* `mixed`, must reject references because dynamic Zend code could replace * `mixed`, must reject references because dynamic Zend code could replace
* the referenced value with one that violates the Native field contract. * the referenced value with one that violates the Native field contract.
*/ */

@ -232,6 +232,24 @@ trait NativeTypeCompatibilityTrait
} }
if ($argInfo->byRef) { if ($argInfo->byRef) {
// A fixed Native field already is the exact C++ storage required by
// a typed-reference parameter. Bind the call directly to that field
// instead of attempting to manufacture a Zend reference around it.
// Explicit std::ref()/toRef() remains a dynamic-reference request and
// deliberately follows the ordinary Native reference restrictions.
if (Type::isTypedRefType($argInfo->type)
&& !$this->isReferenceWrapperCall($arg->value)
) {
$nativeProperty = $this->parseNativeTypedPropertyReferenceArg(
$arg->value,
$argInfo->type,
$arg,
);
if ($nativeProperty !== null) {
return $nativeProperty;
}
}
if ($this->isReferenceWrapperCall($arg->value)) { if ($this->isReferenceWrapperCall($arg->value)) {
$inner = $this->unwrapReferenceWrapperCall($arg->value, $arg); $inner = $this->unwrapReferenceWrapperCall($arg->value, $arg);
$this->assertNativeObjectReferenceForbidden($inner, $arg); $this->assertNativeObjectReferenceForbidden($inner, $arg);

@ -15,6 +15,11 @@ function replaceAny(mixed &$value, mixed $replacement): void
$value = $replacement; $value = $replacement;
} }
function appendNativeAnyString(string &$value): void
{
$value .= ':typed';
}
function &getNativeAnyReference(NativeAnyReference $object): mixed function &getNativeAnyReference(NativeAnyReference $object): mixed
{ {
return $object->value; return $object->value;
@ -46,6 +51,10 @@ function main(): void
$returnedReference =& getNativeAnyReference($object); $returnedReference =& getNativeAnyReference($object);
$returnedReference = 'returned reference'; $returnedReference = 'returned reference';
var_dump($source); var_dump($source);
$object->value = 'dynamic';
appendNativeAnyString($object->value);
var_dump($object->value);
} }
?> ?>
@ -61,3 +70,4 @@ array(2) {
string(15) "direct argument" string(15) "direct argument"
string(14) "source changed" string(14) "source changed"
string(18) "returned reference" string(18) "returned reference"
string(13) "dynamic:typed"

@ -0,0 +1,84 @@
--TEST--
Native class fixed properties bind directly to strong typed references
--FILE--
<?php
#[Native]
class NativeTypedReferenceValues
{
public int $number = 1;
public float $decimal = 1.5;
public bool $enabled = false;
public string $text = 'native';
public array $items = [1];
public ?NativeTypedReferenceValues $child;
public function increment(int &$value): void
{
$value++;
}
public function mutateOwnNumber(): void
{
mutateNativeInt($this->number);
}
}
function mutateNativeInt(int &$value): void
{
$value += 10;
}
function mutateNativeFloat(float &$value): void
{
$value += 0.25;
}
function mutateNativeBool(bool &$value): void
{
$value = !$value;
}
function mutateNativeString(string &$value): void
{
$value .= ':changed';
}
function mutateNativeArray(array &$value): void
{
$value[] = 2;
}
function main(): void
{
$object = new NativeTypedReferenceValues();
mutateNativeInt($object->number);
mutateNativeFloat($object->decimal);
mutateNativeBool($object->enabled);
mutateNativeString($object->text);
mutateNativeArray($object->items);
var_dump($object->number, $object->decimal, $object->enabled, $object->text, $object->items);
$object->increment($object->number);
$object->mutateOwnNumber();
var_dump($object->number);
$object->child = new NativeTypedReferenceValues();
mutateNativeInt(value: $object->child->number);
var_dump($object->child->number);
}
?>
--EXPECT--
int(11)
float(1.75)
bool(true)
string(14) "native:changed"
array(2) {
[0]=>
int(1)
[1]=>
int(2)
}
int(22)
int(11)
Loading…
Cancel
Save