diff --git a/phpunit/src/AssignTest.php b/phpunit/src/AssignTest.php index 62511fb8..f721709a 100644 --- a/phpunit/src/AssignTest.php +++ b/phpunit/src/AssignTest.php @@ -19,4 +19,12 @@ class AssignTest extends \BaseTest 'std-container-static-class-mismatch.php' ); } + + public function testStdUnsafeCastRequiresUnsafePtr() + { + $this->exec( + 'std::unsafe_cast() expects second argument to be declared as UnsafePtr', + 'std-unsafe-cast-requires-unsafe-ptr.php' + ); + } } diff --git a/src/Php/ArgInfo.php b/src/Php/ArgInfo.php index 54dde567..88a09e3e 100644 --- a/src/Php/ArgInfo.php +++ b/src/Php/ArgInfo.php @@ -21,4 +21,5 @@ class ArgInfo public bool $variadic = false; public bool $nullable = false; public bool $property = false; + public bool $unsafePtr = false; } diff --git a/src/Php/CompilerBase.php b/src/Php/CompilerBase.php index 73c9b68e..ec73e3b8 100644 --- a/src/Php/CompilerBase.php +++ b/src/Php/CompilerBase.php @@ -144,6 +144,8 @@ class CompilerBase extends \PhpAot\Core\Translator 'callable' => self::TYPE_VAR, // iterable 类型,可以是数组或者对象 'iterable' => self::TYPE_VAR, + // 编译器符号类型,C++ 层仍使用 php::Var 承载 null zval 中的 value.ptr + 'UnsafePtr' => self::TYPE_VAR, ]; protected array $globalHeaders = [ 'phpx.h', @@ -1044,6 +1046,9 @@ class CompilerBase extends \PhpAot\Core\Translator } foreach ($this->functionDef->argInfoList as $argInfo) { $this->addArgument($argInfo->name, $argInfo->type); + if ($argInfo->unsafePtr) { + $this->addUnsafePtr($argInfo->name); + } if ($argInfo->class and !$this->isAbstractClass($argInfo->class) and !$this->hasInterface($argInfo->class) @@ -1532,7 +1537,15 @@ class CompilerBase extends \PhpAot\Core\Translator } elseif ($this->isStaticCall($right) and $this->isNameExpr($right->class) and $this->isIdExpr($right->name)) { $class = $this->parseIdentifier($right->class); if ($class === 'std') { - if (in_array($right->name->toString(), ['array', 'vector', 'map', 'unordered_map'], true)) { + if ($right->name->toString() === 'unsafe_cast') { + if ($this->hasVar($var)) { + $this->fatalError($left, "Cannot re-assign `\${$var}` to std::unsafe_cast()"); + } + if ($this->context->scopeLevel > 1) { + $this->fatalError($left, 'Must use std::unsafe_cast() in the top-level scope of the function'); + } + return $this->parseStdUnsafeCastAssign($var, $right); + } elseif (in_array($right->name->toString(), ['array', 'vector', 'map', 'unordered_map'], true)) { if ($this->hasVar($var)) { $this->fatalError($left, "Cannot re-assign `\${$var}` to std::{$right->name->toString()}"); } @@ -1558,6 +1571,9 @@ class CompilerBase extends \PhpAot\Core\Translator if (!$this->hasVar($var)) { $this->addLocalVar($var, $right->getAttribute('nativeType')); } + if ($right->getAttribute('unsafePtr')) { + $this->addUnsafePtr($var); + } return $var . ' = ' . $valueExpr; } } @@ -1876,6 +1892,16 @@ class CompilerBase extends \PhpAot\Core\Translator $this->context->localVars[$name] = $type; } + protected function addUnsafePtr(string $name): void + { + $this->context->unsafePtrs[$name] = true; + } + + protected function isUnsafePtr(string $name): bool + { + return isset($this->context->unsafePtrs[$name]); + } + protected function addTmpVar(string $type): string { $var = $this->genTmpVarName(); @@ -2283,6 +2309,10 @@ class CompilerBase extends \PhpAot\Core\Translator if ($param->byRef) { return self::TYPE_REF; } + if ($this->isUnsafePtrTypeDecl($param->type)) { + $argInfo->unsafePtr = true; + return self::TYPE_VAR; + } $class = ''; $type = $this->parseTypeDecl($param->type, self::DECL_TYPE_OF_PARAM, $class); if ($class) { @@ -5380,11 +5410,22 @@ class CompilerBase extends \PhpAot\Core\Translator $expr->setAttribute('nativeType', $type); $valueExpr = $this->parseExpr($expr->args[0]->value); return $this->convertExprFromType($type, $valueExpr); + } elseif ($func === 'unsafe_ptr') { + $expr->setAttribute('nativeType', self::TYPE_VAR); + return $this->parseStdUnsafePtr($expr); } else { $this->fatalError($expr, 'Unknown std method: ' . $func); } } + protected function isUnsafePtrTypeDecl(?NodeAbstract $type): bool + { + return $type !== null + and !$type instanceof UnionType + and !$type instanceof NullableType + and $this->parseIdentifier($type) === 'UnsafePtr'; + } + protected function parseParentMethodCall(Expr\StaticCall $expr): string { $methodStr = $this->classDef->name . '::' . $this->parseIdentifier($expr->name); @@ -5426,7 +5467,9 @@ class CompilerBase extends \PhpAot\Core\Translator $code .= $this->getIndent(); if ($type === self::TYPE_STD_ARRAY) { $info = $this->context->stdArrays[$name]; - if ($info['bytes'] > self::MAX_BYTES_IN_STACK) { + if (isset($info['unsafePtr'])) { + $code .= 'auto &' . $name . ' = *reinterpret_cast<' . $info['decl'] . '*>(Z_PTR_P(' . $info['unsafePtr'] . '.ptr()));'; + } elseif ($info['bytes'] > self::MAX_BYTES_IN_STACK) { $code .= "auto {$name}_unique_ptr = std::make_unique<{$info['decl']}>();\n"; $code .= $this->getIndent() . ' auto &' . $name . ' = *' . $name . '_unique_ptr;'; } else { @@ -5434,16 +5477,24 @@ class CompilerBase extends \PhpAot\Core\Translator } } elseif ($type === self::TYPE_STD_VECTOR) { $info = $this->context->stdContainers[$name]; - $code .= $info['decl'] . ' ' . $name; - if ($info['size'] !== null) { - $code .= '(' . $info['size'] . ')'; + if (isset($info['unsafePtr'])) { + $code .= 'auto &' . $name . ' = *reinterpret_cast<' . $info['decl'] . '*>(Z_PTR_P(' . $info['unsafePtr'] . '.ptr()));'; } else { - $code .= '{}'; + $code .= $info['decl'] . ' ' . $name; + if ($info['size'] !== null) { + $code .= '(' . $info['size'] . ')'; + } else { + $code .= '{}'; + } + $code .= ';'; } - $code .= ';'; } elseif ($type === self::TYPE_STD_MAP || $type === self::TYPE_STD_UNORDERED_MAP) { $info = $this->context->stdContainers[$name]; - $code .= $info['decl'] . ' ' . $name . '{};'; + if (isset($info['unsafePtr'])) { + $code .= 'auto &' . $name . ' = *reinterpret_cast<' . $info['decl'] . '*>(Z_PTR_P(' . $info['unsafePtr'] . '.ptr()));'; + } else { + $code .= $info['decl'] . ' ' . $name . '{};'; + } } else { $code .= $type . ' ' . $name; if ($type === self::TYPE_INT or $type === self::TYPE_FLOAT or $type === self::TYPE_BOOL) { diff --git a/src/Php/Context/FunctionContext.php b/src/Php/Context/FunctionContext.php index d5554821..3fce125c 100644 --- a/src/Php/Context/FunctionContext.php +++ b/src/Php/Context/FunctionContext.php @@ -24,6 +24,10 @@ class FunctionContext * @var array */ public array $stdContainers = []; + /** + * @var array + */ + public array $unsafePtrs = []; public array $localVars = []; public array $staticVars = []; public array $globalVars = []; @@ -58,6 +62,7 @@ class FunctionContext $this->objects = []; $this->stdArrays = []; $this->stdContainers = []; + $this->unsafePtrs = []; $this->objectProps = []; $this->ceWrappers = []; $this->tmpVarIndex = 0; diff --git a/src/Php/Parser/StdContainerParser.php b/src/Php/Parser/StdContainerParser.php index 708fb926..69486f36 100644 --- a/src/Php/Parser/StdContainerParser.php +++ b/src/Php/Parser/StdContainerParser.php @@ -344,6 +344,72 @@ trait StdContainerParser return 'php::toObject(' . $valueExpr . ', ' . $this->getClassEntryPtr($class) . ', true)'; } + protected function parseStdUnsafePtr(Expr\StaticCall $expr): string + { + if (count($expr->args) !== 1) { + $this->fatalError($expr, 'std::unsafe_ptr() expects one argument'); + } + $arg = $expr->args[0]->value; + if (!$this->isVarExpr($arg)) { + $this->fatalError($expr, 'std::unsafe_ptr() expects a std container variable'); + } + $container = $this->parseVariable($arg); + if (!$this->isStdContainer($container)) { + $this->fatalError($expr, 'std::unsafe_ptr() only supports std container variables'); + } + + $tmpVar = $this->addTmpVar(self::TYPE_VAR); + $this->addUnsafePtr($tmpVar); + $expr->setAttribute('unsafePtr', true); + $this->context->beforeStmtLines[] = 'Z_PTR_P(' . $tmpVar . '.ptr()) = &' . $container . ';'; + return $tmpVar; + } + + protected function parseStdUnsafeCastAssign(string $var, Expr\StaticCall $expr): string + { + if (count($expr->args) !== 2) { + $this->fatalError($expr, 'std::unsafe_cast() expects two arguments'); + } + $typeExpr = $expr->args[0]->value; + if (!$this->isStaticCall($typeExpr) || !$this->isNameExpr($typeExpr->class) || !$this->isIdExpr($typeExpr->name) || $typeExpr->class->toString() !== 'std') { + $this->fatalError($expr->args[0]->value, 'std::unsafe_cast() expects first argument to be a std container type expression'); + } + $containerType = $typeExpr->name->toString(); + if (!in_array($containerType, ['array', 'vector', 'map', 'unordered_map'], true)) { + $this->fatalError($expr->args[0]->value, 'std::unsafe_cast() expects first argument to be a std container type expression'); + } + if (!$this->isVarExpr($expr->args[1]->value)) { + $this->fatalError($expr->args[1]->value, 'std::unsafe_cast() expects second argument to be an UnsafePtr variable'); + } + $unsafePtr = $this->parseVariable($expr->args[1]->value); + if (!$this->hasVar($unsafePtr)) { + $this->fatalError($expr->args[1]->value, 'Undefined variable `$' . $unsafePtr . '`'); + } + if (!$this->isUnsafePtr($unsafePtr)) { + $this->fatalError($expr->args[1]->value, 'std::unsafe_cast() expects second argument to be declared as UnsafePtr'); + } + + if ($containerType === 'array') { + $this->addLocalVar($var, self::TYPE_STD_ARRAY); + $this->parseStdArray($var, $typeExpr); + $this->context->stdArrays[$var]['unsafePtr'] = $unsafePtr; + return '// reinterpret_cast<' . $this->context->stdArrays[$var]['decl'] . '*>(' . $unsafePtr . ')'; + } + + if ($containerType === 'vector') { + $this->addLocalVar($var, self::TYPE_STD_VECTOR); + $this->parseStdVector($var, $typeExpr); + } elseif ($containerType === 'map') { + $this->addLocalVar($var, self::TYPE_STD_MAP); + $this->parseStdMap($var, $typeExpr); + } else { + $this->addLocalVar($var, self::TYPE_STD_UNORDERED_MAP); + $this->parseStdUnorderedMap($var, $typeExpr); + } + $this->context->stdContainers[$var]['unsafePtr'] = $unsafePtr; + return '// reinterpret_cast<' . $this->context->stdContainers[$var]['decl'] . '*>(' . $unsafePtr . ')'; + } + protected function parseStdMapKeyType(NodeAbstract $expr, string $owner): string { if (!$this->isClassConstFetch($expr) || !$this->isNameExpr($expr->class) || !$this->isIdExpr($expr->name)) { diff --git a/src/polyfills.php b/src/polyfills.php index 438ada7d..cb0ac106 100644 --- a/src/polyfills.php +++ b/src/polyfills.php @@ -60,6 +60,16 @@ class std return []; } + public static function unsafe_ptr(mixed &$value): mixed + { + return null; + } + + public static function unsafe_cast(mixed $type, mixed $ptr): mixed + { + return $ptr; + } + public static function fill(array $array, mixed $value): void { for ($i = 0; $i < count($array); $i++) { diff --git a/tests/aot/std-array/007.phpt b/tests/aot/std-array/007.phpt new file mode 100644 index 00000000..b38bc562 --- /dev/null +++ b/tests/aot/std-array/007.phpt @@ -0,0 +1,25 @@ +--TEST-- +std array: UnsafePtr unsafe_cast +--FILE-- + +--EXPECT-- +int(7) +int(9) diff --git a/tests/aot/std-map/005.phpt b/tests/aot/std-map/005.phpt new file mode 100644 index 00000000..40bcd809 --- /dev/null +++ b/tests/aot/std-map/005.phpt @@ -0,0 +1,25 @@ +--TEST-- +std map: UnsafePtr unsafe_cast +--FILE-- + +--EXPECT-- +int(7) +int(9) diff --git a/tests/aot/std-unordered-map/005.phpt b/tests/aot/std-unordered-map/005.phpt new file mode 100644 index 00000000..244b312e --- /dev/null +++ b/tests/aot/std-unordered-map/005.phpt @@ -0,0 +1,25 @@ +--TEST-- +std unordered map: UnsafePtr unsafe_cast +--FILE-- + +--EXPECT-- +int(7) +int(9) diff --git a/tests/aot/std-vector/004.phpt b/tests/aot/std-vector/004.phpt new file mode 100644 index 00000000..929c344b --- /dev/null +++ b/tests/aot/std-vector/004.phpt @@ -0,0 +1,32 @@ +--TEST-- +std vector: exact class value type checks typed parameter at runtime +--FILE-- +getMessage(), "\n"; + } +} + +function main() { + std_vector_runtime_class_value(new StdVectorRuntimeClassValueChild(1)); +} +?> +--EXPECT-- +The parameter `object` must be instance of class `StdVectorRuntimeClassValue`, object of `StdVectorRuntimeClassValueChild` given diff --git a/tests/aot/std-vector/005.phpt b/tests/aot/std-vector/005.phpt new file mode 100644 index 00000000..e84d69c3 --- /dev/null +++ b/tests/aot/std-vector/005.phpt @@ -0,0 +1,32 @@ +--TEST-- +std vector: exact class value type rejects non-object value +--FILE-- +getMessage(), "\n"; + } + + try { + $vector[] = std_vector_runtime_scalar_mixed(null); + } catch (Throwable $e) { + echo $e->getMessage(), "\n"; + } +} +?> +--EXPECT-- +The parameter `object` must be `object`, got `int` +The parameter `object` must be `object`, got `null` diff --git a/tests/aot/std-vector/006.phpt b/tests/aot/std-vector/006.phpt new file mode 100644 index 00000000..aa7a9c0c --- /dev/null +++ b/tests/aot/std-vector/006.phpt @@ -0,0 +1,25 @@ +--TEST-- +std vector: UnsafePtr unsafe_cast +--FILE-- + +--EXPECT-- +int(7) +int(9)