[WIP]支持函数参数的类型检查 #44

Closed
NathanFreeman wants to merge 2 commits from feat/params-type-check into master
  1. 57
      src/Generator/ParamTypes/ParamParser.php
  2. 198
      src/Generator/ParamTypes/ZendParamMacros.php
  3. 140
      src/Generator/ParamTypes/ZendVariables.php
  4. 65
      src/Translator.php

@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace TypePhp\Generator\ParamTypes;
use TypePhp\Entity\ArgInfo;
final class ParamParser
{
/**
* ZEND_PARSE_PARAMETERS_NONE cannot be used here, because it hardcodes a return statement,
* which would result in a type mismatch.
*/
private const string NONE_PARAMS_TEMPLATE = <<<EOL
ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_QUIET, 0, 0)
ZEND_PARSE_PARAMETERS_END_EX(php::throwErrorIfOccurred());
EOL;
/**
* @param array<ArgInfo> $argInfos
* @return string
*/
public function createZendParamTypeCheck(array $argInfos): string
{
// Return the default macro if no parameter information is available.
if (empty($argInfos)) {
return sprintf("do { %s } while(false);", self::NONE_PARAMS_TEMPLATE);
}
[$minNumArgs, $maxNumArgs] = $this->getRequiredAndAllParamCounts($argInfos);
$zendVariables = (new ZendVariables())->createZendVariables($argInfos);
$zendParamMarcos = (new ZendParamMacros())->createZendParamMacros($argInfos, $minNumArgs, $maxNumArgs);
return sprintf(
"do { %s %s } while(false);",
implode(PHP_EOL, $zendVariables),
implode(PHP_EOL, $zendParamMarcos));
}
/**
* Calculate the count of required parameters and total parameters.
* @param array<ArgInfo> $argInfos
* @return array<int>
*/
private function getRequiredAndAllParamCounts(array $argInfos): array
{
$argCount = count($argInfos);
$lastIndex = $argCount - 1;
if ($argCount > 0 && $argInfos[$lastIndex]->variadic) {
return [$lastIndex, -1];
}
$requiredParamCount = count(array_filter($argInfos, fn($argInfo) => $argInfo->defaultValue === null));
return [$requiredParamCount, $argCount];
}
}

@ -0,0 +1,198 @@
<?php
declare(strict_types=1);
namespace TypePhp\Generator\ParamTypes;
use PhpParser\Node\Identifier;
use PhpParser\Node\NullableType;
use TypePhp\Entity\ArgInfo;
use TypePhp\Type;
final class ZendParamMacros
{
/**
* Generate a macro in the extension to validate/check the input parameters.
* @param array<ArgInfo> $argInfos
* @param int $minNumArgs
* @param int $maxNumArgs
* @return array<string>
*/
public function createZendParamMacros(array $argInfos, int $minNumArgs, int $maxNumArgs): array
{
$marcos[] = $this->genStartParseCode($minNumArgs, $maxNumArgs);
foreach ($argInfos as $index => $argInfo) {
if ($maxNumArgs > $minNumArgs && $index == $minNumArgs) {
$marcos[] = $this->genOptionalParseCode();
}
$marcos[] = match ($argInfo->type) {
Type::BOOL => $this->parseBool($argInfo),
Type::INT => $this->parseInt($argInfo),
Type::FLOAT => $this->parseFloat($argInfo),
Type::ARRAY => $this->parseArray($argInfo),
Type::STR => $this->parseString($argInfo),
Type::REF => $this->parseReference($argInfo),
Type::RESOURCE, Type::STREAM => $this->parseResource($argInfo),
Type::OBJECT => $this->parseObject($argInfo, $argInfo->class),
default => $this->parseVar($argInfo)
};
}
$marcos[] = $this->genEndParseCode();
return $marcos;
}
/**
* ZEND_PARAM_PARSE start
* @param int $minNumArgs
* @param int $maxNumArgs
* @return string
*/
private function genStartParseCode(int $minNumArgs, int $maxNumArgs): string
{
return sprintf("ZEND_PARSE_PARAMETERS_START(%d, %d)", $minNumArgs, $maxNumArgs);
}
/**
* ZEND_PARAM_PARSE end
* @return string
*/
private function genEndParseCode(): string
{
return "ZEND_PARSE_PARAMETERS_END_EX(php::throwErrorIfOccurred());";
}
/**
* Z_PARAM_OPTIONAL
* @return string
*/
private function genOptionalParseCode(): string
{
return "Z_PARAM_OPTIONAL";
}
private function parseBool(ArgInfo $argInfo): string
{
$name = $argInfo->name;
return $argInfo->nullable
? sprintf("Z_PARAM_BOOL_OR_NULL(%s, %s_is_null)", $name, $name)
: "Z_PARAM_BOOL($name)";
}
private function parseInt(ArgInfo $argInfo): string
{
$name = $argInfo->name;
return $argInfo->nullable
? sprintf("Z_PARAM_LONG_OR_NULL(%s, %s_is_null)", $name, $name)
: "Z_PARAM_LONG($name)";
}
private function parseFloat(ArgInfo $argInfo): string
{
$name = $argInfo->name;
return $argInfo->nullable
? sprintf("Z_PARAM_DOUBLE_OR_NULL(%s, %s_is_null)", $name, $name)
: "Z_PARAM_DOUBLE($name)";
}
private function parseArray(ArgInfo $argInfo): string
{
$name = $argInfo->name;
return $argInfo->nullable ? "Z_PARAM_ARRAY_OR_NULL($name)" : "Z_PARAM_ARRAY($name)";
}
private function parseString(ArgInfo $argInfo): string
{
$name = $argInfo->name;
return $argInfo->nullable ? "Z_PARAM_STR_OR_NULL($name)" : "Z_PARAM_STR($name)";
}
private function parseArgs(ArgInfo $argInfo): string
{
$name = $argInfo->name;
return sprintf("Z_PARAM_VARIADIC(*, %s_argv, %s_argc)", $name, $name);
}
private function parseReference(ArgInfo $argInfo): string
{
return "Z_PARAM_ZVAL($argInfo->name)";
}
private function parseResource(ArgInfo $argInfo): string
{
return "Z_PARAM_RESOURCE($argInfo->name)";
}
private function parseObject(ArgInfo $argInfo, string $className): string
{
$name = $argInfo->name;
if ($className) {
return $argInfo->nullable
? sprintf("Z_PARAM_OBJECT_OF_CLASS_OR_NULL(%s_obj, %s_ce)", $name, $name)
: sprintf("Z_PARAM_OBJECT_OF_CLASS(%s_obj, %s_ce)", $name, $name);
}
return $argInfo->nullable ? "Z_PARAM_OBJECT_OR_NULL($name)" : "Z_PARAM_OBJECT($name)";
}
/**
* The compiler does not currently support the callable type.
* For now, we'll implement the method first and fill in the callable support later.
* @param \TypePhp\Entity\ArgInfo $argInfo
* @return string
*/
private function parseCallable(ArgInfo $argInfo): string
{
$name = $argInfo->name;
return $argInfo->nullable
? sprintf("Z_PARAM_FUNC_OR_NULL(%s_fci, %s_fcc)", $name, $name)
: sprintf("Z_PARAM_FUNC(%s_fci, %s_fcc)", $name, $name);
}
/**
* For multiple types, we only handle plain types, mixed, and nullable variables.
* The remaining UnionType and IntersectionType are too complex to be processed via macros.
* @param \TypePhp\Entity\ArgInfo $argInfo
* @return string
*/
private function parseVar(ArgInfo $argInfo): string
{
if ($argInfo->variadic) {
return $this->parseArgs($argInfo);
}
if ($argInfo->byRef) {
return $this->parseArgs($argInfo);
}
$name = $argInfo->name;
if (!$argInfo->typeNode instanceof NullableType) {
return $argInfo->nullable ? "Z_PARAM_ZVAL_OR_NULL($name)" : "Z_PARAM_ZVAL($name)";
}
/**
* Nullable only contains Identifier, FullyQualified, and Relative.
* The Relative option is not supported by the compiler.
*/
$type = $argInfo->typeNode->type;
if ($type instanceof Identifier) {
return match ($type->name) {
"int" => $this->parseInt($argInfo),
"string" => $this->parseString($argInfo),
"array" => $this->parseArray($argInfo),
"bool" => $this->parseBool($argInfo),
"float" => $this->parseFloat($argInfo),
"object" => $this->parseObject($argInfo, $argInfo->class),
/**
* A default case is not strictly required here, since the PHP-supported types are already listed above.
* However, it is included anyway to keep the code robust in case new types are introduced in the future.
*/
default => "Z_PARAM_ZVAL_OR_NULL($name)"
};
}
// FullyQualified
return $this->parseObject($argInfo, $type->toString());
}
}

@ -0,0 +1,140 @@
<?php
declare(strict_types=1);
namespace TypePhp\Generator\ParamTypes;
use PhpParser\Node\Identifier;
use PhpParser\Node\NullableType;
use TypePhp\Entity\ArgInfo;
use TypePhp\Type;
final class ZendVariables
{
/**
* @param array<ArgInfo> $argInfos
* @return array
*/
public function createZendVariables(array $argInfos): array
{
$variables = [];
foreach ($argInfos as $argInfo) {
$variables[] = match ($argInfo->type) {
Type::BOOL => $this->genBoolVariable($argInfo->name),
Type::INT => $this->genIntVariable($argInfo->name),
Type::FLOAT => $this->genFloatVariable($argInfo->name),
Type::ARRAY => $this->genArrayVariable($argInfo->name),
Type::STR => $this->genStringVariable($argInfo->name),
Type::RESOURCE, Type::STREAM => $this->genResourceVariable($argInfo->name),
Type::OBJECT => $this->genObjectVariable($argInfo->name, $argInfo->class),
default => $this->genVarVariable($argInfo),
};
}
return $variables;
}
private function genBoolVariable(string $name): string
{
return sprintf("zend_bool %s = 0; zend_bool %s_is_null = false;", $name, $name);
}
private function genIntVariable(string $name): string
{
return sprintf("zend_long %s = 0; zend_bool %s_is_null = false;", $name, $name);
}
private function genFloatVariable(string $name): string
{
return sprintf("double %s = 0; zend_bool %s_is_null = false;", $name, $name);
}
private function genArrayVariable(string $name): string
{
return sprintf("zval *%s = nullptr;", $name);
}
private function genStringVariable(string $name): string
{
return sprintf("zend_string *%s = nullptr;", $name);
}
private function genArgsVariable(string $name): string
{
return sprintf("zval *%s_argv = nullptr; int %s_argc = 0;", $name, $name);
}
private function genReferenceVariable(string $name): string
{
return sprintf("zval *%s = nullptr;", $name);
}
private function genResourceVariable(string $name): string
{
return sprintf("zval *%s = nullptr;", $name);
}
private function genObjectVariable(string $name, string $className): string
{
if ($className) {
return sprintf(<<<EOL
zend_string *%s_search = zend_string_init("%s", %d, 0);
zend_class_entry *%s_ce = zend_lookup_class(%s_search);
zend_string_release(%s_search);
zval *%s_obj = nullptr;
EOL,
$name, str_replace("\\", "\\\\", $className), strlen($className), $name, $name, $name, $name);
}
return sprintf("zval *%s = nullptr;", $name);
}
/**
* The compiler does not currently support the callable type.
* For now, we'll implement the method first and fill in the callable support later.
* @param string $name
* @return string
*/
private function genCallableVariable(string $name): string
{
return sprintf("zend_fcall_info %s_fci; zend_fcall_info_cache %s_fcc;", $name, $name);
}
private function genVarVariable(ArgInfo $argInfo): string
{
if ($argInfo->variadic) {
return $this->genArgsVariable($argInfo->name);
}
if ($argInfo->byRef) {
return $this->genReferenceVariable($argInfo->name);
}
if (!$argInfo->typeNode instanceof NullableType) {
return sprintf("zval *%s = nullptr;", $argInfo->name);
}
/** @var NullableType $typeNode */
$typeNode = $argInfo->typeNode;
$type = $typeNode->type;
$name = $argInfo->name;
/**
* Nullable only contains Identifier, FullyQualified, and Relative.
* The Relative option is not supported by the compiler.
*/
if ($type instanceof Identifier) {
return match ($type->name) {
'bool' => $this->genBoolVariable($name),
'int' => $this->genIntVariable($name),
'string' => $this->genStringVariable($name),
'float' => $this->genFloatVariable($name),
'array' => $this->genArrayVariable($name),
'object' => $this->genObjectVariable($name, ''),
default => sprintf("zval *%s = nullptr;", $name),
};
}
// FullyQualified
return $this->genObjectVariable($name, $type->toString());
}
}

@ -11,16 +11,21 @@ namespace TypePhp;
use Ajaxray\AnsiKit\AnsiTerminal;
use Ajaxray\AnsiKit\Components\Progressbar;
use MJS\TopSort\Implementations\StringSort;
use PhpParser\Modifiers;
use PhpParser\Node;
use PhpParser\NodeAbstract;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitor\NameResolver;
use TypePhp\Analysis\SsaBuilder;
use TypePhp\Backend\CompilerFactory;
use TypePhp\Build\FileScanner;
use TypePhp\Build\NativeCommandOptionsTrait;
use TypePhp\Build\NativeBuilder;
use TypePhp\Build\NativeCommandOptionsTrait;
use TypePhp\Build\PrecompiledHeaderManager;
use TypePhp\Build\ResourceCompilationTrait;
use TypePhp\Build\SourcePipelineTrait;
use TypePhp\Config\ProjectYamlLoader;
use TypePhp\Diagnostics\CompileTimeAttributeDiagnostic;
use TypePhp\Build\ResourceCompilationTrait;
use TypePhp\Entity\ArgInfo;
use TypePhp\Entity\ClassDef;
use TypePhp\Entity\ClassLikeDef;
@ -34,21 +39,17 @@ use TypePhp\Exception\Skip;
use TypePhp\Exception\SyntaxError;
use TypePhp\Generator\DefaultArgumentGenerator;
use TypePhp\Generator\LibraryImportStubGenerator;
use TypePhp\Generator\ParamTypes\ParamParser;
use TypePhp\Generator\Symbol;
use TypePhp\Metadata\Constants;
use TypePhp\Platform\PlatformFactory;
use TypePhp\Platform\Windows;
use TypePhp\Resolver\Reflection;
use TypePhp\Resolver\ClassConstantValueTrait;
use TypePhp\Transform\Visitor;
use TypePhp\Transform\ConstructorLowering;
use TypePhp\Resolver\Reflection;
use TypePhp\Transform\ConstantExpressionValidationVisitor;
use TypePhp\Transform\ConstructorLowering;
use TypePhp\Transform\RuntimeAttributeFactoryLowering;
use PhpParser\Modifiers;
use PhpParser\Node;
use PhpParser\NodeAbstract;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitor\NameResolver;
use TypePhp\Transform\Visitor;
class Translator extends Preprocessor
{
@ -92,6 +93,7 @@ class Translator extends Preprocessor
*/
protected array $classCeList = [];
protected array $classCeInfo = [];
private bool $declareStrictTypes = false;
protected function isConstructorNativeFunction(FunctionDef $func): bool
{
@ -2339,6 +2341,8 @@ CODE;
$this->resetClass();
$this->resetMethod();
$this->resetFunction();
// 重置$this->declareStrictTypes,并不是每份文件都设置了declare(strict_types=1),防止污染下一份文件
$this->setDeclareStrictTypes(false);
$cppCode = '';
foreach ($stmts as $v) {
@ -2494,6 +2498,9 @@ CODE;
if (!($declare->value instanceof Node\Scalar\Int_) or $declare->value->value !== 1) {
$this->fatalError($v, 'declare(strict_types=0) is not allowed, only strict_types=1 is supported');
}
// 当前文件设置了declare(strict_types=1),我们需要保存这个状态,然后生成对应的代码。
$this->setDeclareStrictTypes(true);
} else {
$this->fatalError($v, 'declare(' . $key . '=' . $value . ') is not supported');
}
@ -3167,9 +3174,15 @@ CODE;
{
$cppCode = '';
$callParams = '';
if ($functionDef->argCountRequired > 0) {
$cppCode .= $this->genWrapperRequiredArgCountCheck($functionDef, $displayName);
}
/**
* 若当前文件声明了 declare(strict_types=1);,则需为当前函数标记 ZEND_ACC_STRICT_TYPES 标志。
* 该标志用于确保函数在通过 ZendVM 调用其他函数时,能够严格按照声明模式校验参数类型。
* 需要注意的是,此机制仅作用于 ZendVM 动态调用的函数,
* 若目标函数为编译后的本地代码,则不会触发类型检查——这是 ZendVM 的设计使然,其哲学是信任已编译内部函数的 C 代码实现。
*/
$cppCode .= $this->genDeclareStrictTypesCode();
foreach ($functionDef->argInfoList as $k => $argInfo) {
$var = 'arg_' . $argInfo->name;
if ($argInfo->variadic) {
@ -3298,6 +3311,10 @@ CODE;
{
$name = $classDef->getNamespacedName();
$cppCode = 'ZEND_METHOD(' . $name . ', ' . $methodDef->name . '){' . PHP_EOL;
$paramParser = new ParamParser();
$cppCode .= $paramParser->createZendParamTypeCheck($methodDef->functionDef->argInfoList);
$cppCode .= $this->getIndent() . Type::OBJECT . ' this_(&execute_data->This);' . PHP_EOL;
$fn = self::PREFIX . $this->getNativeMethodName($classDef, $methodDef);
$implicitMethodArgs = [];
@ -3524,6 +3541,7 @@ CODE;
$this->indentLevel++;
$code .= $this->genScopeVarDecl();
$code .= "\n";
// Runtime union/nullable parameter type checks
foreach ($this->functionDef->argInfoList as $i => $argInfo) {
if (!empty($argInfo->typeCheck)) {
@ -4780,6 +4798,10 @@ CODE;
{
$name = $this->escapeZendFnName($functionDef->getNamespacedName());
$cppCode = 'ZEND_FUNCTION(' . $name . '){' . PHP_EOL;
$paramParser = new ParamParser();
$cppCode .= $paramParser->createZendParamTypeCheck($functionDef->argInfoList);
$fn = self::PREFIX . $this->getNativeName($functionDef->name, $functionDef->namespace);
$cppCode .= $this->genWrapperFunctionArgs($fn, $functionDef, $functionDef->getNamespacedName());
@ -4882,4 +4904,21 @@ CODE;
return $code;
}
private function genDeclareStrictTypesCode(): string
{
/**
* 类型检查仅在二进制模式下启用。由于二进制模式作为独立可执行程序运行,需保持和直接运行的PHP代码一致的行为语义,因此必须保留类型检查以确保安全性。
* 相反,当编译为扩展(如 ext/curl)时,编译端无需强制内置类型检查,其开启与否应由调用方PHP代码自行决定。
*/
return $this->declareStrictTypes && $this->isBuildModeBin()
? "execute_data->func->common.fn_flags |= ZEND_ACC_STRICT_TYPES;" . PHP_EOL
: '';
}
private function setDeclareStrictTypes(bool $type): void
{
$this->declareStrictTypes = $type;
}
}

Loading…
Cancel
Save