refactor(scope): optimize callback map handling for preg_replace_callback_array

- Remove makeScopedCallableMap helper function
- Change preg_replace_callback_array strategy from 'map' to 'dynamic-map'
- Add dynamic-map strategy support in CompilerBase for efficient callback processing
- Update documentation to reflect new callback map implementation approach
- Modify tests to verify UserCodeScopeGuard usage instead of makeScopedCallableMap
- Add test case with static closure in preg_replace_callback_array
master
韩天峰 2 weeks ago
parent bca77588b5
commit 96960e9da1
  1. 15
      docs/SCOPE_MANAGEMENT.md
  2. 8
      phpunit/code/scoped-call-context-reuse.php
  3. 2
      phpunit/src/ScopedCallContextTest.php
  4. 15
      src/CompilerBase.php
  5. 1
      src/Generator/CallArgumentGenerator.php
  6. 3
      tests/compiler/callable/scoped-internal-callbacks.phpt

@ -132,12 +132,6 @@ $callback = $this->publicMethod(...);
只有 private/protected 方法、`self` / `parent` / `static` 相对 callback 或 trampoline 才创建 Closure。这避免了循环中每次调用内置函数都无条件分配 fake Closure。
#### `php::makeScopedCallableMap()`
用于 callback map。它逐项调用 `prepareScopedCallback()`,只替换真正依赖作用域的元素。
PHPX `Array` 使用 Zend copy-on-write:若所有 callback 都可直接复用,底层 HashTable 不会复制;首次需要替换时只分离一次,之后在同一结果数组中更新。因此复杂度仍为 O(N) 检查,但没有无条件 O(N) 的 Closure 分配。
### 3.6 为什么仍要运行时识别 `self` / `parent` / `static`
直接语法中的 `self::class` 可以在编译期展开为具体类名,但 PHP callback 也允许动态值:
@ -153,7 +147,7 @@ $callback = [$class, 'method'];
### 4.1 职责与适用范围
`UserCodeScopeGuard` 服务于完全动态的 `call_user_func()` / `call_user_func_array()`,以及编译器无法静态改写 callback 的参数展开场景。
`UserCodeScopeGuard` 服务于完全动态的 `call_user_func()` / `call_user_func_array()`、callback map,以及编译器无法静态改写 callback 的参数展开场景。
```php
$args = [[$this, 'privateMethod'], 1];
@ -170,7 +164,9 @@ call_user_func(...$args);
规范化只复制需要修改的 callback 数组。绝对类名、对象 callback、Closure 和普通函数名保持原值。
`call_user_func*` 外,普通 callback 参数不得使用此 guard;只要 callback 的 AST 参数位置已知,就应使用 `CallableScope` 路径。
`preg_replace_callback_array()` 是 callback map 的特例。Zend 会在函数内部逐项解析 map 中的 callback;若提前包装 map,则每次调用都要执行 O(N) 扫描,并可能触发数组 COW 和多个 Closure 分配。因此编译器保留原始 map,在方法入口创建一次 `UserCodeScopeGuard`,让 Zend 直接按正确作用域解析。
除完全动态调用、callback map 和参数展开外,普通 callback 参数不得使用此 guard;只要单个 callback 的 AST 参数位置已知,就应使用 `CallableScope` 路径。
### 4.2 实现方式
@ -352,8 +348,7 @@ save EG(fake_scope)
| `callScoped()` | `zend_is_callable_at_frame()` 动态解析 | 仅动态调用使用;可解析的 Native Call 不进入此路径 |
| `prepareScopedCallback()` | 一次 callable 解析 | public 绝对 callback 不创建 Closure |
| `makeScopedCallable()` | callable 解析及 Closure 分配 | 仅 first-class callable 使用 |
| `makeScopedCallableMap()` | O(N) 检查 | COW;只包装需要作用域的元素 |
| `UserCodeScopeGuard` | 方法入口一次指针查找、写入和退出恢复 | 只为 `call_user_func*` 或未解析的 unpack callback 生成 |
| `UserCodeScopeGuard` | 方法入口一次指针查找、写入和退出恢复 | 只为 `call_user_func*`、callback map 或未解析的 unpack callback 生成 |
| `FakeScopeGuard` | 两次 executor-global 指针赋值 | 仅包围确实读取 fake scope 的 Zend API |
这套设计刻意让常见的纯 Native Call、无 callback 方法和 public callback 保持最短路径。不要为了统一表面形式而把低频 fallback 下沉到所有调用中。

@ -12,9 +12,17 @@ class ScopedCallContextReuse
foreach ($rows as $row) {
array_map([$this, 'mapValue'], $row);
array_filter($row, [$this, 'mapValue']);
preg_replace_callback_array([
'/[0-9]+/' => [self::class, 'replaceDigits'],
], 'item-123');
}
$callback = self::mapValue(...);
$callback(1);
}
private static function replaceDigits(array $matches): string
{
return '[' . $matches[0] . ']';
}
}

@ -20,5 +20,7 @@ class ScopedCallContextTest extends \BaseTest
);
preg_match('/php::CallableScope (tmp_var_\d+) =/', $cpp, $matches);
$this->assertGreaterThanOrEqual(4, substr_count($cpp, $matches[1]));
$this->assertSame(1, substr_count($cpp, 'php::UserCodeScopeGuard'));
$this->assertStringNotContainsString('makeScopedCallableMap', $cpp);
}
}

@ -3244,7 +3244,10 @@ class CompilerBase implements PropertyAccessContext
'forward_static_call' => [[0, 'callback']],
'forward_static_call_array' => [[0, 'callback']],
'preg_replace_callback' => [[1, 'callback']],
'preg_replace_callback_array' => [[0, 'pattern', 'map']],
// Zend resolves every callback stored in the map. Preserve the
// original array and expose the method scope once at function
// entry instead of scanning/copying it into fake Closures.
'preg_replace_callback_array' => [[0, 'pattern', 'dynamic-map']],
'iterator_apply' => [[1, 'callback']],
'array_udiff' => [[-1, 'value_compare_func']],
'array_udiff_assoc' => [[-1, 'value_compare_func']],
@ -3269,8 +3272,10 @@ class CompilerBase implements PropertyAccessContext
return;
}
$fullyDynamic = ($descriptors[0][2] ?? 'callable') === 'dynamic';
if ($fullyDynamic) {
$firstStrategy = $descriptors[0][2] ?? 'callable';
$fullyDynamic = $firstStrategy === 'dynamic';
$dynamicMap = $firstStrategy === 'dynamic-map';
if ($fullyDynamic || $dynamicMap) {
$this->markUserCodeCallableScope();
}
$argCount = count($args);
@ -3288,14 +3293,14 @@ class CompilerBase implements PropertyAccessContext
foreach ($descriptors as $descriptorIndex => $descriptor) {
[$position, $name] = $descriptor;
$strategy = $descriptor[2] ?? 'callable';
$container = $strategy === 'map';
$scopeProvidedByGuard = $strategy === 'dynamic-map';
$callbackPosition = $position < 0 ? $argCount + $position : $position;
$matches = $arg->name === null
? $index === $callbackPosition
: $arg->name->toString() === $name;
if ($matches) {
$matchedCallbacks[$descriptorIndex] = true;
if ($container || !$this->isScopeIndependentCallableExpr($arg->value)) {
if (!$scopeProvidedByGuard && !$this->isScopeIndependentCallableExpr($arg->value)) {
$mode = $strategy === 'dynamic' ? 'normalize' : $strategy;
$arg->setAttribute(self::ATTR_SCOPED_CALLBACK, $mode);
}

@ -558,7 +558,6 @@ trait CallArgumentGenerator
}
$helper = match ($mode) {
'map' => 'makeScopedCallableMap',
'normalize' => 'normalizeCallableClass',
default => 'prepareScopedCallback',
};

@ -31,6 +31,7 @@ class ScopedInternalCallbacks
var_dump(array_map(array: [4, 5], callback: [$this, 'triple']));
var_dump(preg_replace_callback_array([
'/a+/' => [self::class, 'replace'],
'/b+/' => static fn(array $match): string => '[' . $match[0] . ']',
], 'caaab'));
var_dump(array_udiff_uassoc(
['a' => 1, 'b' => 2],
@ -65,7 +66,7 @@ array(2) {
[1]=>
int(15)
}
string(5) "cAAAb"
string(7) "cAAA[b]"
array(1) {
["b"]=>
int(2)

Loading…
Cancel
Save