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
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
explicit `std::ref()` / `toRef()`. A call-scoped bridge validates the value on
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。绑定必须位于函数顶层、只发生一次且不得逃逸;重新绑定、
`unset`、引用捕获/返回,或把引用存入 PHP 槽位都会在编译期拒绝。
Native Class 中固定类型为 `int`、`float`、`bool`、`string`、`array` 的属性,现在也可
直接传给静态可解析调用中类型完全匹配的引用参数。编译器将其生成为仅在本次调用期间
有效的 C++ `T&`,同时精确保活 Native 接收对象;这并不会为字段开放通用 PHP 引用,
`=&`、`std::ref()`、动态调用及其他可能逃逸的引用形式仍被禁止。显式声明为 `any` 的
属性继续使用动态 Zend reference 模型。
动态调用与 Closure 继续使用既有 Zend reference 路径,并要求显式使用 `std::ref()` /
`toRef()`。调用级 bridge 在返回时检查类型并拒绝临时引用逃逸;需要完整 PHP 引用身份时
应使用 `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
reference-capable because Zend attaches property type sources; PHP array
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
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:
- 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.
- `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.
- `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.
- `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.
- `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`.
- 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 |
| `&` 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 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 |
| `__construct()` | 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.
*
* 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
* only when it was explicitly declared `any`: that field intentionally
* permits arbitrary PHP values. Every other declaration, including
* pointer slot as a PHP reference. A Native property may expose a Zend
* reference only when it was explicitly declared `any`: that field
* 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
* the referenced value with one that violates the Native field contract.
*/

@ -232,6 +232,24 @@ trait NativeTypeCompatibilityTrait
}
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)) {
$inner = $this->unwrapReferenceWrapperCall($arg->value, $arg);
$this->assertNativeObjectReferenceForbidden($inner, $arg);

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